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

new(yaml) click to toggle source
# File lib/yaml_editor.rb, line 19
def initialize(yaml)
  @doc = yaml.lines.map(&:chomp)
end

Public Instance Methods

array(spos, epos) click to toggle source

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
bracket(*fields) click to toggle source

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
inner_bracket(field, spos, epos) click to toggle source

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
lines(range) click to toggle source

RAW access :nocov:

# File lib/yaml_editor.rb, line 125
def lines(range)
  @doc[range]
end
to_s()
Alias for: to_yaml
to_yaml() click to toggle source
# File lib/yaml_editor.rb, line 23
def to_yaml
  @doc.join("\n") + "\n"
end
Also aliased as: to_s
update(*fields, val:) click to toggle source

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
value(line) click to toggle source

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

append(fields, val) click to toggle source

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?(line) click to toggle source

Array marker presence

# File lib/yaml_editor.rb, line 202
def array?(line)
  line =~ /\A\s*-/
end
bad_quoting?(val) click to toggle source

Are quotes missing but needed?

# File lib/yaml_editor.rb, line 187
def bad_quoting?(val)
  val.include?(' ') && !QUOTES.include?(val[0]) || false
end
hash_key?(line) click to toggle source

Simple Hash key presence

# File lib/yaml_editor.rb, line 197
def hash_key?(line)
  line =~ /\A\s*-*\s*.+:/
end
indented(line) click to toggle source

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
non_ws_index(line) click to toggle source

Simple position of first non whitespace

# File lib/yaml_editor.rb, line 192
def non_ws_index(line)
  line.index(/[^\s]/)
end
split_noquote(val) click to toggle source

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
split_quoted(val) click to toggle source

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
value_split(val) click to toggle source

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