class YamlEditor
Editor for YAML documents rubocop:disable Metrics/ClassLength
Constants
- COMMENTS
- FLD_ERR
- INDENTS
Legal indent chars
- MIX_ERR
- QUOTES
- SET_ERR
- VAL_ERR
Public Class Methods
# File lib/yaml_editor.rb, line 19 def initialize(yaml) @doc = yaml.lines.map(&:chomp) end
Public Instance Methods
Produce a set of bracketed array lines betweem a line range requires that we start with a known array position containing the hyphen rubocop:disable Metrics/AbcSize ok
# File lib/yaml_editor.rb, line 93 def array(spos, epos) # Get initial indent level ilvl = non_ws_index(@doc[spos]) # Produce start and end pairings res = @doc[spos..epos].each_with_index.map do |line, ind| nlvl = non_ws_index(line) nlvl == ilvl ? spos + ind : nil end # Strip non boundaries, append a final marker and collect range groups res.compact.push(epos + 1).each_cons(2).map do |s, f| # Skip empty boundaries s += 1 until hash_key?(@doc[s]) s >= f ? nil : [s, f.pred] end .compact end
Iterate through a set of fields to obtain the block rubocop:disable all Hookup
# File lib/yaml_editor.rb, line 46 def bracket(*fields) spos = 0 epos = @doc.size - 1 value = nil # Top level document type kind = @doc[1] =~ /\s+-/ ? :array : :hash fields.each do |field| if field.is_a?(Integer) && kind == :array spos, epos = array(spos, epos)[field] else spos, epos, kind, value = inner_bracket(field, spos, epos) end break unless spos end [spos, epos, kind, value] end
Calculate a block start and finish based upon a field rubocop:disable Metrics/MethodLength ok rubocop:disable Metrics/AbcSize ok
# File lib/yaml_editor.rb, line 69 def inner_bracket(field, spos, epos) ind = @doc[spos..epos].index { |v| v =~ /\A\s*-*\s*#{field}\:/ } return unless ind ind += spos start_line = @doc[ind] schar, snum = indented(start_line) val = value(start_line) return [ind, ind, :value, val] if val # Value returned if value ind += 1 kind = array?(@doc[ind]) ? :array : :hash # Loop until we encounter same indent level or end @doc[ind..epos].each_with_index do |line, eind| nchar, nnum = indented(line) raise(*MIX_ERR) if nchar != schar return [ind, ind + eind - 1, kind] if nnum <= snum end [ind, epos, kind] end
RAW access :nocov:
# File lib/yaml_editor.rb, line 125 def lines(range) @doc[range] end
# File lib/yaml_editor.rb, line 23 def to_yaml @doc.join("\n") + "\n" end
High level APIs udpate
# File lib/yaml_editor.rb, line 31 def update(*fields, val:) raise(*FLD_ERR) if bad_quoting?(val) spos, _, kind, v = bracket(*fields) if kind == :value tag, _, comment = v @doc[spos] = "#{tag}#{val}#{comment}" else append(fields, val) end end
Analyse the line for a value
# File lib/yaml_editor.rb, line 112 def value(line) res = line.match(/\A(?<tag>.+\:\s)(?<value>.+)\Z/) return unless res tag = res[:tag] val = res[:value] val, rest = value_split(val) return if val.empty? # Return the split line that could be reassembled [tag, val, rest] end
Private Instance Methods
Used by set in cases where the field does not exist rubocop:disable Metrics/AbcSize ok
# File lib/yaml_editor.rb, line 173 def append(fields, val) parent = fields.dup node = parent.pop spos, epos, = bracket(*parent) # Must inspect inner element for type kind = value(@doc[spos]) ? :hash : :array raise(*SET_ERR) unless kind == :hash ichar, inum = indented(@doc[epos]) pad = ichar * inum @doc.insert(epos.succ, "#{pad}#{node}: #{val}") end
Array marker presence
# File lib/yaml_editor.rb, line 202 def array?(line) line =~ /\A\s*-/ end
Are quotes missing but needed?
# File lib/yaml_editor.rb, line 187 def bad_quoting?(val) val.include?(' ') && !QUOTES.include?(val[0]) || false end
Simple Hash key presence
# File lib/yaml_editor.rb, line 197 def hash_key?(line) line =~ /\A\s*-*\s*.+:/ end
Type and count of indent char
# File lib/yaml_editor.rb, line 161 def indented(line) spaces = line.match(/\A\s*-*\s*/).to_s ichars = INDENTS.map { |c| spaces.count(c) } raise(*MIX_ERR) if ichars.count(&:nonzero?) > 1 # On zero indents we default to space and zero iichar = ichars.index(&:nonzero?) || 0 # Add any hyphens for arrays detected inline [INDENTS[iichar], ichars[iichar] + spaces.count('-')] end
Simple position of first non whitespace
# File lib/yaml_editor.rb, line 192 def non_ws_index(line) line.index(/[^\s]/) end
Simple unquoted values
# File lib/yaml_editor.rb, line 143 def split_noquote(val) pos = val.index(/\s/) pos ? [val[0..pos.pred], val[pos..-1]] : [val, ''] end
Quoted values
# File lib/yaml_editor.rb, line 149 def split_quoted(val) prev = quote = val[0] pos = 1 until val[pos] == quote && prev != '\\' prev = val[pos] pos += 1 raise(*VAL_ERR) if pos > val.length end [val[0..pos], val[pos.succ..-1]] end
Split the value half to capture comments etc.
# File lib/yaml_editor.rb, line 133 def value_split(val) # Comment only ? return ['', val] if val.start_with?(*COMMENTS) # No quotes ? return split_noquote(val) unless val.start_with?(*QUOTES) # Quotes split_quoted(val) end