Class: Rubino::Config::Writer
- Inherits:
-
Object
- Object
- Rubino::Config::Writer
- Defined in:
- lib/rubino/config/writer.rb
Overview
Writes configuration changes back to the YAML file.
Class Method Summary collapse
-
.coerce_array(value) ⇒ Object
ARRAY support for ‘config set` (#420): an array-typed key (e.g. an MCP stdio server’s ‘args`) previously had no CLI syntax — a bare string was written and the schema validator rejected it (expected array), forcing a hand-edit of config.yml.
-
.coerce_value(value) ⇒ Object
The string→typed coercion ‘config set` applies to a CLI-supplied value, exposed as a class method so set-time validation (Config::Validator) compares the SAME coerced type the file will actually store.
Instance Method Summary collapse
-
#get(key_path) ⇒ Object
Returns the value at a dot-notation key path.
-
#initialize(config_path:) ⇒ Writer
constructor
A new instance of Writer.
-
#set(key_path, value) ⇒ Object
Sets a single key (dot-notation) to a value and persists.
-
#unset(key_path) ⇒ Object
Removes a single key (dot-notation) from config.yml so a user can DROP a setting and fall back to the built-in default (F2).
Constructor Details
#initialize(config_path:) ⇒ Writer
Returns a new instance of Writer.
11 12 13 |
# File 'lib/rubino/config/writer.rb', line 11 def initialize(config_path:) @config_path = config_path end |
Class Method Details
.coerce_array(value) ⇒ Object
ARRAY support for ‘config set` (#420): an array-typed key (e.g. an MCP stdio server’s ‘args`) previously had no CLI syntax — a bare string was written and the schema validator rejected it (expected array), forcing a hand-edit of config.yml. Accept an explicit JSON array literal (`config set …args ’[“run”,“server”]‘`) — unambiguous, so a legitimate scalar string is never mis-coerced. Returns the parsed Array, or nil when the value isn’t a JSON array (the caller falls back to the raw string).
100 101 102 103 104 105 106 107 108 |
# File 'lib/rubino/config/writer.rb', line 100 def self.coerce_array(value) str = value.to_s.strip return nil unless str.start_with?("[") && str.end_with?("]") parsed = JSON.parse(str) parsed.is_a?(Array) ? parsed : nil rescue JSON::ParserError nil end |
.coerce_value(value) ⇒ Object
The string→typed coercion ‘config set` applies to a CLI-supplied value, exposed as a class method so set-time validation (Config::Validator) compares the SAME coerced type the file will actually store.
81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/rubino/config/writer.rb', line 81 def self.coerce_value(value) case value when "true" then true when "false" then false when "nil", "null" then nil when /\A\d+\z/ then value.to_i when /\A\d+\.\d+\z/ then value.to_f else coerce_array(value) || value end end |
Instance Method Details
#get(key_path) ⇒ Object
Returns the value at a dot-notation key path
111 112 113 114 115 116 117 118 119 |
# File 'lib/rubino/config/writer.rb', line 111 def get(key_path) raw = load_raw keys = key_path.split(".") # A scalar intermediate node (e.g. a String) has no #dig; treat such a # path as "not found" rather than crashing with a TypeError. raw.dig(*keys) rescue TypeError nil end |
#set(key_path, value) ⇒ Object
Sets a single key (dot-notation) to a value and persists.
The whole read-modify-write runs under an exclusive lock (and the write is temp-file + atomic rename), so two concurrent ‘config set` of different keys can’t lose one another’s update or tear config.yml into unparseable YAML that bricks every later command.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/rubino/config/writer.rb', line 21 def set(key_path, value) Util::AtomicFile.update(@config_path) do |current| raw = parse_raw(current) keys = key_path.split(".") reject_scalar_over_section!(key_path, keys, raw, value) hash = raw keys[0..-2].each_with_index do |k, i| hash[k] ||= {} hash = hash[k] next if hash.is_a?(Hash) traversed = keys[0..i].join(".") raise ConfigurationError, "cannot set '#{key_path}': '#{traversed}' is a scalar value, not a section" end # Set-time schema validation (#327): runs AFTER the structural # scalar-over-section / scalar-intermediate checks above (so those # keep their more specific messages) but BEFORE the write — an unknown # key or a value whose type/format can't match the schema is rejected # with a clear ConfigurationError and a non-zero exit, instead of being # written with a green ✓ and only surfacing later as a runtime crash or # a deterministic provider 4xx the agent then retries for ~85s. Validator.validate!(key_path, keys, value) hash[keys.last] = self.class.coerce_value(value) raw.to_yaml end end |
#unset(key_path) ⇒ Object
Removes a single key (dot-notation) from config.yml so a user can DROP a setting and fall back to the built-in default (F2). Same locked, atomic-rename write as #set. Returns true when the key existed and was removed, false when it was already absent (a no-op — never an error, and the file is left untouched). A scalar intermediate on the path (e.g. ‘unset foo.bar` where `foo` is a scalar) is “not present” → false, not a crash.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/rubino/config/writer.rb', line 59 def unset(key_path) removed = false Util::AtomicFile.update(@config_path) do |current| raw = parse_raw(current) keys = key_path.split(".") parent = keys[0..-2].reduce(raw) do |node, k| node.is_a?(Hash) && node[k].is_a?(Hash) ? node[k] : (break nil) end if parent.is_a?(Hash) && parent.key?(keys.last) parent.delete(keys.last) removed = true raw.to_yaml end # Key absent / unreachable path: the if returns nil, so AtomicFile # skips the write entirely — a true no-op, file untouched. end removed end |