Class: Kettle::Jem::Appraisals::ConfigFile
- Inherits:
-
Object
- Object
- Kettle::Jem::Appraisals::ConfigFile
- Defined in:
- lib/kettle/jem/appraisals/config_file.rb,
sig/kettle/jem/appraisals.rbs
Overview
Reads and surgically updates the project's kettle-jem configuration file.
The appraisal_matrix lives in the same file kettle-jem uses
(+.structuredmerge/kettle-jem.yml+). That file is heavily commented and
holds many other settings, so writes only replace the top-level sections
whose values changed, leaving comments and every other section intact.
Constant Summary collapse
- CANONICAL_PATH =
Returns canonical config path, relative to the project root.
Kettle::Jem::KETTLE_CONFIG_PATH
- LEGACY_PATH =
Returns legacy config path, read only when the canonical file is absent.
Kettle::Jem::LEGACY_KETTLE_CONFIG_PATH
Instance Attribute Summary collapse
-
#project_dir ⇒ String
readonly
Absolute path to the project root.
Instance Method Summary collapse
-
#initialize(project_dir:) ⇒ ConfigFile
constructor
A new instance of ConfigFile.
-
#load ⇒ Hash
The parsed config, or an empty Hash when the file does not exist.
-
#path ⇒ String
The config file to read and write: the canonical file when present, otherwise an existing legacy file, otherwise the canonical location.
-
#relative_path ⇒ String
#path relative to the project root, for messages.
-
#section_range(content, key) ⇒ Range[Integer]?
Line range (0-based, end-exclusive) of the top-level
keysection. -
#trailing_trivia?(line) ⇒ Boolean
Whether
lineis blank or a comment (at any indentation). - #upsert_section(content, key, value) ⇒ String
-
#write(config) ⇒ void
Writes
config, replacing only the top-level sections that differ from the file on disk.
Constructor Details
#initialize(project_dir:) ⇒ ConfigFile
Returns a new instance of ConfigFile.
32 33 34 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 32 def initialize(project_dir:) @project_dir = project_dir end |
Instance Attribute Details
#project_dir ⇒ String (readonly)
Returns absolute path to the project root.
29 30 31 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 29 def project_dir @project_dir end |
Instance Method Details
#load ⇒ Hash
Returns the parsed config, or an empty Hash when the file does not exist.
56 57 58 59 60 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 56 def load return {} unless File.exist?(path) YAML.safe_load_file(path, permitted_classes: [Symbol]) || {} end |
#path ⇒ String
The config file to read and write: the canonical file when present, otherwise an existing legacy file, otherwise the canonical location.
40 41 42 43 44 45 46 47 48 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 40 def path canonical = File.join(project_dir, CANONICAL_PATH) return canonical if File.exist?(canonical) legacy = File.join(project_dir, LEGACY_PATH) return legacy if File.exist?(legacy) canonical end |
#relative_path ⇒ String
Returns #path relative to the project root, for messages.
51 52 53 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 51 def relative_path File.(path).delete_prefix("#{File.(project_dir)}/") end |
#section_range(content, key) ⇒ Range[Integer]?
Line range (0-based, end-exclusive) of the top-level key section.
A parsed section runs up to the next top-level key, so trailing blank
lines and comment lines (commented-out settings, or comments that
introduce the next section) are trimmed back out of the range and kept.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 100 def section_range(content, key) require "yaml/merge" analysis = Yaml::Merge::FileAnalysis.new(content) raise ArgumentError, "could not parse #{relative_path} as YAML" unless analysis.valid? body = analysis.documents.first&.body_node return unless body&.mapping? lines = content.lines pairs = body.mapping_pairs pairs.each_with_index do |pair, index| next unless pair.key_name == key start_index = pair.start_line.to_i - 1 next_pair = pairs[index + 1] end_index = next_pair ? next_pair.start_line.to_i - 1 : lines.length end_index -= 1 while end_index > start_index + 1 && trailing_trivia?(lines[end_index - 1]) return start_index...end_index end nil end |
#trailing_trivia?(line) ⇒ Boolean
Returns whether line is blank or a comment (at any indentation).
125 126 127 128 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 125 def trailing_trivia?(line) stripped = line.strip stripped.empty? || stripped.start_with?("#") end |
#upsert_section(content, key, value) ⇒ String
87 88 89 90 91 92 93 94 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 87 def upsert_section(content, key, value) block = YAML.dump({key => value}).delete_prefix("---\n") range = section_range(content, key) return "#{content.rstrip}\n\n#{block}" unless range lines = content.lines [*lines[0...range.begin], block, *lines[range.end..]].join end |
#write(config) ⇒ void
This method returns an undefined value.
Writes config, replacing only the top-level sections that differ from
the file on disk. Creates the file (and its directory) when missing.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/kettle/jem/appraisals/config_file.rb', line 67 def write(config) target = path unless File.exist?(target) FileUtils.mkdir_p(File.dirname(target)) File.write(target, YAML.dump(config)) return end content = File.read(target) current = YAML.safe_load(content, permitted_classes: [Symbol]) || {} config.each do |key, value| next if current.key?(key) && current[key] == value content = upsert_section(content, key.to_s, value) end File.write(target, content) end |