Class: RepoTender::Config::Store
- Inherits:
-
Object
- Object
- RepoTender::Config::Store
- Defined in:
- lib/repo_tender/config/store.rb
Overview
Load/validate/write-back the YAML config file.
The store is the only place that touches the disk. It does not preserve unknown keys or YAML comments on write — that’s a known limitation per the PRD §2 (YAML comment loss accepted) and is documented in the Slice 1 lane report. Managed fields round-trip byte-identically (gate G1).
Constant Summary collapse
- DEFAULT_BASE_DIR =
RepoTender::Paths::DEFAULT_BASE_DIR
- DEFAULT_REFRESH_INTERVAL =
6 * 3600
- DEFAULT_CONCURRENCY =
8
Class Method Summary collapse
- .defaults ⇒ Object
-
.emit(hash) ⇒ Object
Hash → YAML string.
- .load(path) ⇒ Object
- .read_yaml(path) ⇒ Object
- .symbolize(value) ⇒ Object
- .update(path) ⇒ Object
-
.with(config, **changes) ⇒ Object
dry-struct update idiom: pass a hash of attributes to ‘Config#new` to get a new struct with the overrides applied (existing fields are kept).
-
.with_defaults(hash) {|filled| ... } ⇒ Object
Fill in missing top-level defaults before validation, so an empty YAML produces a fully-populated Config.
- .write(path, config) ⇒ Object
Class Method Details
.defaults ⇒ Object
123 124 125 126 127 128 129 130 131 |
# File 'lib/repo_tender/config/store.rb', line 123 def self.defaults { base_dir: DEFAULT_BASE_DIR, refresh_interval: DEFAULT_REFRESH_INTERVAL, concurrency: DEFAULT_CONCURRENCY, repos: [], orgs: [] } end |
.emit(hash) ⇒ Object
Hash → YAML string. Stable key order for diff-ability.
83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/repo_tender/config/store.rb', line 83 def self.emit(hash) # Order: base_dir, refresh_interval, concurrency, repos, orgs. ordered = {} ordered[:base_dir] = hash[:base_dir] if hash.key?(:base_dir) ordered[:refresh_interval] = hash[:refresh_interval] if hash.key?(:refresh_interval) ordered[:concurrency] = hash[:concurrency] if hash.key?(:concurrency) ordered[:repos] = hash[:repos] if hash.key?(:repos) && !hash[:repos].nil? ordered[:orgs] = hash[:orgs] if hash.key?(:orgs) && !hash[:orgs].nil? # Ruby's Psych has a default flow style and key order that is # good enough — we don't customize it. YAML.dump(ordered, line_width: -1) end |
.load(path) ⇒ Object
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 51 52 53 |
# File 'lib/repo_tender/config/store.rb', line 25 def self.load(path) raw = read_yaml(path) hash = symbolize(raw) # CF1: normalize `refresh_interval` from a human-duration # string ("6h", "90m", "45s", "30d") or bare integer string # ("21600") into integer seconds BEFORE the contract runs. # The contract stays integer-typed (:integer, gt?: 0); this # is a load-layer normalization that lets a hand-edited # config.yaml round-trip without rejecting "6h" as a # non-integer. See lib/repo_tender/config/duration.rb. if hash.key?(:refresh_interval) result = Duration.parse(hash[:refresh_interval]) return result if result.failure? hash[:refresh_interval] = result.success end with_defaults(hash) do |filled| result = Contract.new.call(filled) if result.success? Success(Config.new(result.success)) else Failure(result.failure) end end rescue Errno::ENOENT # Missing file is treated as an empty config (load defaults). # The store does not create the file on read — that is the # writer's job (write() is idempotent and always validates first). Success(Config.new(defaults)) end |
.read_yaml(path) ⇒ Object
97 98 99 100 101 102 103 |
# File 'lib/repo_tender/config/store.rb', line 97 def self.read_yaml(path) return {} unless File.exist?(path) # Symbol permitted because some YAML files use :symbol keys; the # store's symbolize() then re-keys consistently anyway. We still # disallow arbitrary classes. YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false) || {} end |
.symbolize(value) ⇒ Object
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/repo_tender/config/store.rb', line 105 def self.symbolize(value) case value when Hash value.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = symbolize(v) } when Array value.map { |v| symbolize(v) } else value end end |
.update(path) ⇒ Object
69 70 71 72 73 |
# File 'lib/repo_tender/config/store.rb', line 69 def self.update(path) config = load(path).success new_config = yield(config) write(path, new_config) end |
.with(config, **changes) ⇒ Object
dry-struct update idiom: pass a hash of attributes to ‘Config#new` to get a new struct with the overrides applied (existing fields are kept).
78 79 80 |
# File 'lib/repo_tender/config/store.rb', line 78 def self.with(config, **changes) config.new(**changes) end |
.with_defaults(hash) {|filled| ... } ⇒ Object
Fill in missing top-level defaults before validation, so an empty YAML produces a fully-populated Config.
118 119 120 121 |
# File 'lib/repo_tender/config/store.rb', line 118 def self.with_defaults(hash) filled = defaults.merge(hash) yield(filled) end |
.write(path, config) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/repo_tender/config/store.rb', line 55 def self.write(path, config) # Always re-validate the struct's contents before writing. This # guards against a caller constructing a Config with a # constraint-violating value via Struct.new (which does not run # the same checks as the contract). hash = config.to_h result = Contract.new.call(hash) return result if result.failure? FileUtils.mkdir_p(File.dirname(path)) File.write(path, emit(hash)) Success(config) end |