Class: Incline::CliHelpers::Yaml::YamlContents

Inherits:
Object
  • Object
show all
Defined in:
lib/incline/cli/helpers/yaml.rb

Overview

Helper class to process the YAML file contents easily.

Instance Method Summary collapse

Constructor Details

#initialize(content) ⇒ YamlContents

Creates a new YAML contents.



18
19
20
# File 'lib/incline/cli/helpers/yaml.rb', line 18

def initialize(content)
  @content = content.to_s.gsub("\r\n", "\n").strip + "\n"
end

Instance Method Details

#=~(regexp) ⇒ Object

Allows comparing the contents against a regular expression.



518
519
520
# File 'lib/incline/cli/helpers/yaml.rb', line 518

def =~(regexp)
  @content =~ regexp
end

#add_key(key, value, make_safe_value = true) ⇒ Object

Adds a key to the YAML contents if it is missing. Does nothing to the key if it exists.

add_key [ "default", "name" ], "george"

The ‘key’ should be an array defining the path.

Value can be nil, a string, a symbol, a number, or a boolean.

The ‘make_safe_value’ option can be used to provide an explicit text value. This can be useful if you want to add a specific value, like an ERB command.

add_key [ "default", "name" ], "<%= ENV[\"DEFAULT_USER\"] %>", false

You can also use a hash for the value to specify advanced options. Currently only three advanced options are recognized.

The first option, :value, simply sets the value. If this is the only hash key provided, then the value supplied is treated as if it was the original value. In other words, only setting :value is the same as not using a hash and just passing in the value, so the value must be nil, a string, a symbol, a number, or a boolean.

The second option, :safe, works the opposite of the ‘make_safe_value’ parameter. If :safe is a non-false value, then it is like ‘make_safe_value’ is set to false. If :safe is a false value, then it is like ‘make_safe_value’ is set to true. The :safe value can be set to true and the :value option can set the value, or the :safe value can be set to the value directly since all strings are non-false.

The third option, :before_section, tells add_key to insert the section before the named section (if the new section doesn’t exist). This can be useful if the named section is going to be referencing the key you are adding. Otherwise, when a section needs to be added, it gets added to the end of the file.

Returns the contents object.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/incline/cli/helpers/yaml.rb', line 64

def add_key(key, value, make_safe_value = true)

  # ensure the parent structure exists!
  if key.count > 1
    add_key(key[0...-1], nil)
  else
    # If the base key already exists, no need to look further.
    return self if @content =~ /^#{key.first}:/

    unless @content[0] == '#'
      @content = "# File modified by Incline v#{Incline::VERSION}.\n" + @content
    end
  end

  val_name = key.last

  # construct a regular expression to find the parent group and value.
  rex_str = '^('
  rex_prefix = /\A./
  key.each_with_index do |attr,level|
    lev = (level < 1 ? '' : ('\\s\\s' * (level)))
    if lev != ''
      rex_str += '(?:' + lev + '[^\\n]*\\n)*'
    end
    if level == key.count - 1
      if level == 0
        # At level 0 we cheat and use a very simple regular expression to confirm the section exists.
        rex_str = "^#{attr}:"
        # If it doesn't exists, the prefix regex will usually want to put the new section at the end of the
        # file.  However if the :before_section option is set, and the other section exists, then we
        # want to ensure that we are putting the new section before it.
        #
        # Down below we take care to reverse the replacement string when key.count == 1.
        rex_prefix =
          if value.is_a?(::Hash) && value[:before_section] && @content =~ /^#{value[:before_section]}:/
            /(^#{value[:before_section]}:)/
          else
            /(\z)/   # match the end of the contents.
          end
      else
        rex_str += ')'
        rex_prefix = Regexp.new(rex_str)
        rex_str += '(' + lev + attr + ':.*\\n)'
      end
    else
      rex_str += lev + attr + ':.*\\n'
    end
  end

  rex = Regexp.new(rex_str)

  if @content =~ rex
    # all good.
  elsif @content =~ rex_prefix
    if make_safe_value
      value = safe_value(value)
      value = add_value_offset(key, value)
    elsif value.is_a?(::Hash)
      value = value[:value]
    end
    value = '' if value =~ /\A\s*\z/
    # Should be true thanks to first step in this method.
    # Capture 1 would be the parent group.
    # When key.count == 1 then we want to put our new value before capture 1.
    # Otherwise we put our new value after capture 1.
    rep = if key.count == 1
            "\n#{val_name}:#{value}\n\\1"
          else
            "\\1#{'  ' * (key.count - 1)}#{val_name}:#{value}\n"
          end

    @content.gsub! rex_prefix, rep
  else
    raise ::Incline::CliHelpers::Yaml::YamlError, "Failed to create parent group for '#{key.join('/')}'."
  end

  self
end

#add_key_with_comment(key, value, comment) ⇒ Object

Adds a key to the YAML contents if it is missing. Does nothing to the key if it exists.

add_key_with_comment [ "default", "name" ], "george", "this is the name of the default user"

The ‘key’ should be an array defining the path. If the ‘comment’ is blank (nil or ”), then it will not modify the comment. Use a whitespace string (‘ ’) to indicate that you want a blank comment added.

Value can be nil, a string, a symbol, a number, or a boolean. Value can also be a hash according to #add_key.

Returns the contents object.



158
159
160
161
162
163
164
# File 'lib/incline/cli/helpers/yaml.rb', line 158

def add_key_with_comment(key, value, comment)
  if comment.to_s == ''
    add_key key, value
  else
    add_key key, value_with_comment(key, value, comment), false
  end
end

#append_comment(text) ⇒ Object

Appends a comment to the end of the contents.



531
532
533
534
535
536
537
# File 'lib/incline/cli/helpers/yaml.rb', line 531

def append_comment(text)
  text = '# ' + text.gsub("\r\n", "\n").gsub("\n", "\n# ") + "\n"
  unless @content[-1] == "\n"
    @content += "\n"
  end
  @content += text
end

#insert_comment(text) ⇒ Object

Inserts a comment to the beginning of the contents.



524
525
526
527
# File 'lib/incline/cli/helpers/yaml.rb', line 524

def insert_comment(text)
  text = '# ' + text.gsub("\r\n", "\n").gsub("\n", "\n# ") + "\n"
  @content = @content.insert(0, text)
end

#level_comment_offset(level) ⇒ Object

Gets the comment offset for the specified level.

The offset is based on the beginning of the level in question. There will always be at least one whitespace before the value and at least one whitespace between the value and a comment. For instance an offset of 15 for level 2 would be like this:

one:
  two: value     # comment
  some_long_name: value # comment
# 012345678901234^


495
496
497
# File 'lib/incline/cli/helpers/yaml.rb', line 495

def level_comment_offset(level)
  comment_offsets[level] || 0
end

#level_value_offset(level) ⇒ Object

Gets the value offset for the specified level.

The offset is based on the beginning of the level in question. There will always be at least one whitespace before the value. For instance an offset of 10 for level 2 would be like this:

one:
  two:      value
  some_long_name: value
# 0123456789^


462
463
464
# File 'lib/incline/cli/helpers/yaml.rb', line 462

def level_value_offset(level)
  value_offsets[level] || 0
end

#realign!Object

Realigns the file.

All values and comments will line up at each level when complete.



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/incline/cli/helpers/yaml.rb', line 393

def realign!
  lines = extract_to_array(@content)

  # reset the offsets.
  value_offsets.clear
  comment_offsets.clear

  # get value offsets.
  lines.each do |line|
    level = line[:level]
    if level > 0 && line[:key]
      key_len = line[:key].length + 2   # include the colon and a space
      if key_len > level_value_offset(level)
        set_level_value_offset level, key_len
      end
    end
  end

  # get comment offsets.
  lines.each do |line|
    level = line[:level]
    if level > 0 && line[:value]
      voff = level_value_offset(level)
      val_len = line[:value] ? line[:value].length : 0
      coff = voff + val_len + 1         # add a space after the value.
      if coff > level_comment_offset(level)
        set_level_comment_offset level, coff
      end
    end
  end

  # convert the lines back into strings with proper spacing.
  lines = lines.map do |line|
    level = line[:level]
    if level > 0
      if line[:key]
        # a key: value line.
        key = line[:key] + ':'
        key = key.ljust(level_value_offset(level), ' ') unless line[:value].to_s == '' && line[:comment].to_s == ''
        val = line[:value].to_s
        val = val.ljust(level_comment_offset(level) - level_value_offset(level), ' ') unless line[:comment].to_s == ''
        comment = line[:comment] ? "# #{line[:comment]}" : ''
        ('  ' * (level - 1)) + key + val + comment
      else
        # just a comment line.
        ('  ' * (level - 1)) +
            (' ' * level_comment_offset(level)) +
            "# #{line[:comment]}"
      end
    else
      line[:value]  # return the original value
    end
  end

  @content = lines.join("\n") + "\n"
end

#remove_key(key) ⇒ Object

Removes the specified key from the contents.

Returns an array containing the contents of the key. The first element will be for the key itself. If the key had child keys, then they will also be included in the array.

The returned array will contain hashes for each removed key.

data = remove_key %w(pet dog)

[
  {
    :key => [ "pet", "dog" ],
    :value => "",
    :safe => true,
    :comment => "This list has the family dogs."
  },
  {
    :key => [ "pet", "dog", "sadie" ],
    :value => "",
    :safe => true,
    :comment => ""
  },
  {
    :key => [ "pet", "dog", "sadie", "breed" ],
    :value => "boxer",
    :safe => true,
    :comment => ""
  },
  {
    :key => [ "pet", "dog", "sadie", "dob" ],
    :value => "\"2016-06-01\"",
    :safe => true,
    :comment => "Estimated date of birth since she was a rescue."
  }
]

The returned hashes can be fired right back into #add_key.

data.each do |item|
  add_key_with_comment item[:key], item, item[:comment]
end

This method can be used to move a section within the file.

# remove the 'familes' section from the file.
section = remove_key [ "families" ]
item = section.delete(section.first)

# add the 'familes' section back in before the 'pets' section.
add_key_with_comment item[:key], { before_section: "pets" }.merge(item), item[:comment]

# add the data back into the 'familes' section.
section.each do |item|
  add_key_with_comment item[:key], item, item[:comment]
end


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/incline/cli/helpers/yaml.rb', line 305

def remove_key(key)
  rex_str = '(^'
  key.each_with_index do |attr,level|
    lev = (level < 1 ? '' : ('\\s\\s' * level))
    if lev != ''
      rex_str += '(?:' + lev + '.*\\n)*'
    end
    if level == key.count - 1
      if level == 0
        rex_str = '(^)(' + attr + ':[^\\n]*\\n(?:\\s\\s[^\\n]*\\n)*)'
      else
        rex_str += ')(' + lev + attr + ':[^\\n]*\\n(?:' + lev + '\\s\\s[^\\n]*\\n)*)'
      end
    else
      rex_str += lev + attr + ':[^\\n]*\\n'
    end
  end

  # match result 1 is the parent key structure leading up to the key to be extracted.
  # match result 2 is the key with all child elements to be extracted.

  rex = Regexp.new(rex_str)

  if @content =~ rex

    # cache the key contents
    key_content = $2

    # remove the key from the main contents.
    @content.gsub!(rex, "\\1")

    # and separate into lines.
    lines = extract_to_array(key_content)

    ret = []

    base_key = key.length == 1 ? [] : key[0...-1]

    last_line = nil

    lines.each do |line|
      level = line[:level]
      if level > 0 && line[:key]
        # go from base 1 to base 0
        level -= 1

        # make sure the base key is the right length for the current key.
        while level > base_key.length
          base_key.push '?' # hopefully this never occurs.
        end
        while level < base_key.length
          base_key.pop
        end

        # add our key to the base key.
        # if the next key is below it, this ensures the parent structure is correct.
        # if the next key is higher or at the same level the above loops should make it correct.
        base_key << line[:key]

        last_line = {
            key: base_key.dup,
            value: line[:value].to_s,
            comment: line[:comment],
            safe: true
        }

        ret << last_line
      elsif level > 0 && line[:comment]
        if last_line && last_line[:key].length == level
          if last_line[:comment]
            last_line[:comment] += "\n" + line[:comment]
          else
            last_line[:comment] = "\n" + line[:comment]
          end
        end
      end
    end

    ret
  else
    []
  end
end

#set_key(key, value, make_safe_value = true) ⇒ Object

Sets a key in the YAML contents. Adds the key if it is missing, replaces it if it already exists.

set_key [ "default", "name" ], "george"

The ‘key’ should be an array defining the path.

Value can be nil, a string, a symbol, a number, or a boolean. Value can also be a hash according to #add_key.

The ‘make_safe_value’ option can be used to provide an explicit text value. This can be useful if you want to add a specific value, like an ERB command.

set_key [ "default", "name" ], "<%= ENV[\"DEFAULT_USER\"] %>", false

Returns the contents object.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/incline/cli/helpers/yaml.rb', line 183

def set_key(key, value, make_safe_value = true)

  # construct a regular expression to find the value and not confuse it with any other value in the file.
  rex_str = '^('
  key.each_with_index do |attr,level|
    lev = (level < 1 ? '' : ('\\s\\s' * (level)))
    if lev != ''
      rex_str += '(?:' + lev + '.*\\n)*'
    end
    if level == key.count - 1
      rex_str += lev + attr + ':)\\s*([^#\\n]*)?(#[^\\n]*)?\\n'
    else
      rex_str += lev + attr + ':.*\\n'
    end
  end

  rex = Regexp.new(rex_str)

  if @content =~ rex
    if make_safe_value
      value = safe_value(value)
      value = add_value_offset(key, value)
    elsif value.is_a?(::Hash)
      value = value[:value]
    end
    value = '' if value =~ /\A\s*\z/
    # Capture 1 is everything before the value.
    # Capture 2 is going to be just the value.
    # Capture 3 is the comment (if any).  This allows us to propagate comments if we change a value.
    if $2 != value
      rep = "\\1#{value}\\3\n"
      @content.gsub! rex, rep
    end

    self
  else
    add_key(key, value, make_safe_value)
  end
end

#set_key_with_comment(key, value, comment) ⇒ Object

Sets a key in the YAML contents. Adds the key if it is missing, replaces it if it already exists.

set_key_with_comment [ "default", "name" ], "george", "this is the name of the default user"

The ‘key’ should be an array defining the path. If the ‘comment’ is blank (nil or ”), then it will not modify the comment. Use a whitespace string (‘ ’) to indicate that you want a blank comment added.

Value can be nil, a string, a symbol, a number, or a boolean. Value can also be a hash according to #add_key.

Returns the contents object.



237
238
239
240
241
242
243
# File 'lib/incline/cli/helpers/yaml.rb', line 237

def set_key_with_comment(key, value, comment)
  if comment.to_s == ''
    set_key key, value
  else
    set_key key, value_with_comment(key, value, comment), false
  end
end

#set_level_comment_offset(level, offset) ⇒ Object

Sets the comment offset for the specified level.

The offset is based on the beginning of the level in question. There will always be at least one whitespace before the value and at least one whitespace between the value and a comment. For instance an offset of 15 for level 2 would be like this:

one:
  two: value     # comment
  some_long_name: value # comment
# 012345678901234^


512
513
514
# File 'lib/incline/cli/helpers/yaml.rb', line 512

def set_level_comment_offset(level, offset)
  comment_offsets[level] = offset
end

#set_level_value_offset(level, offset) ⇒ Object

Sets the value offset for the specified level.

The offset is based on the beginning of the level in question. There will always be at least one whitespace before the value. For instance an offset of 10 for level 2 would be like this:

one:
  two:      value
  some_long_name: value
# 0123456789^


478
479
480
# File 'lib/incline/cli/helpers/yaml.rb', line 478

def set_level_value_offset(level, offset)
  value_offsets[level] = offset
end

#to_sObject

Returns the YAML contents.



24
25
26
# File 'lib/incline/cli/helpers/yaml.rb', line 24

def to_s
  @content
end