Exception: CLI::Kit::Config::ConfigWriteError

Inherits:
SystemCallError
  • Object
show all
Defined in:
lib/cli/kit/config.rb

Overview

Raised when the config file cannot be written (e.g. read-only directory, read-only filesystem, nix/home-manager-managed symlink pointing into /nix/store).

Inherits from SystemCallError so existing rescue SystemCallError handlers around Config#set continue to match. The message contains only the keys that actually changed; unchanged keys (which may include sensitive values such as API tokens) are intentionally excluded so the failure message can never leak secrets through stderr or exception reports.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_path, old_content, new_content, cause) ⇒ ConfigWriteError

rubocop:disable Lint/MissingSuper SystemCallError#initialize has a factory-style signature that cannot be invoked from a plain subclass (it expects a matching Errno constant on the class). We bypass it deliberately and initialize via Exception so we just get a message-only exception that rescue SystemCallError still catches via inheritance. super would not work here. : (String config_path, String old_content, String new_content, SystemCallError cause) -> void



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/cli/kit/config.rb', line 69

def initialize(config_path, old_content, new_content, cause)
  @config_path = config_path
  @old_content = old_content
  @new_content = new_content
  @wrapped_errno = cause.errno
  message = <<~MSG.rstrip
    Could not write to #{config_path}: #{cause.message}

    Attempted changes (unchanged keys omitted):
    #{diff}
  MSG
  Exception.instance_method(:initialize).bind(self).call(message)
end

Instance Attribute Details

#config_pathObject (readonly)

: String



53
54
55
# File 'lib/cli/kit/config.rb', line 53

def config_path
  @config_path
end

#new_contentObject (readonly)

: String



59
60
61
# File 'lib/cli/kit/config.rb', line 59

def new_content
  @new_content
end

#old_contentObject (readonly)

: String



56
57
58
# File 'lib/cli/kit/config.rb', line 56

def old_content
  @old_content
end

Class Method Details

.===(other) ⇒ Object

Ruby's SystemCallError.=== uses errno-based matching, which means rescue ConfigWriteError would otherwise fail to catch a ConfigWriteError instance (it would match Errno::EACCES or Errno::EPERM instead, based on the wrapped errno). Override with plain class-hierarchy matching so rescue ConfigWriteError works as callers expect. rescue SystemCallError still matches via normal inheritance. : (untyped other) -> bool



33
34
35
# File 'lib/cli/kit/config.rb', line 33

def ===(other)
  other.is_a?(self)
end

.new(config_path, old_content, new_content, cause) ⇒ Object

SystemCallError.new has a factory-style signature defined in Sorbet's stdlib RBI (+(String | Integer?) -> SystemCallError+) which doesn't match this subclass's four-argument constructor. Override .new here so type checkers and callers see the real signature, and allocate the instance manually to bypass +SystemCallError+'s factory behaviour. : (String config_path, String old_content, String new_content, SystemCallError cause) -> ConfigWriteError



45
46
47
48
49
# File 'lib/cli/kit/config.rb', line 45

def new(config_path, old_content, new_content, cause)
  instance = allocate
  instance.__send__(:initialize, config_path, old_content, new_content, cause)
  instance
end

Instance Method Details

#diffObject

A line-by-line diff of only the sections/keys that changed between old_content and new_content. Unchanged keys are omitted so that sensitive values stored elsewhere in the config are never included in the failure message. : -> String



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/cli/kit/config.rb', line 96

def diff
  old_ini = CLI::Kit::Ini.new(config: @old_content).tap(&:parse).ini
  new_ini = CLI::Kit::Ini.new(config: @new_content).tap(&:parse).ini

  lines = []
  (old_ini.keys | new_ini.keys).each do |section|
    old_section = old_ini[section] || {}
    new_section = new_ini[section] || {}

    changes = []
    (old_section.keys | new_section.keys).each do |key|
      old_val = old_section[key]
      new_val = new_section[key]
      next if old_val == new_val

      changes << "- #{key} = #{old_val}" if old_val
      changes << "+ #{key} = #{new_val}" if new_val
    end
    next if changes.empty?

    prefix = if old_section.empty?
      '+ '
    elsif new_section.empty?
      '- '
    else
      '  '
    end
    lines << "#{prefix}#{section}"
    lines.concat(changes)
  end
  lines.join("\n")
end

#errnoObject

Preserve the original errno from the wrapped Errno so callers that inspect errno continue to see the real underlying code. : -> Integer?



87
88
89
# File 'lib/cli/kit/config.rb', line 87

def errno
  @wrapped_errno
end