Class: Sycl::Hash

Inherits:
Hash
  • Object
show all
Includes:
Comparable
Defined in:
lib/sycl.rb

Overview

A Sycl::Hash is like a Hash, but creating one from an hash blesses any child Array or Hash objects into Sycl::Array or Sycl::Hash objects. All the normal Hash methods are supported, and automatically promote any inputs into Sycl equivalents. The following example illustrates this:

h = Sycl::Hash.new
h['a'] = { 'b' => { 'c' => 'Hello, world!' } }

puts h.a.b.c   # outputs 'Hello, world!'

Hash contents can be accessed via “dot notation” (h.foo.bar means the same as h[‘bar’]). However, h.foo.bar dies if h does not exist, so get() and set() methods exist: h.get(‘foo.bar’) will return nil instead of dying if h does not exist. There is also a convenient deep_merge() that is like Hash#merge(), but also descends into and merges child nodes of the new hash.

A Sycl::Hash supports YAML preprocessing and postprocessing, and having individual nodes marked as being rendered in inline style. YAML output is also always sorted by key.

h = Sycl::Hash.from_hash({'b' => 'bravo', 'a' => 'alpha'})
h.render_inline!
h.yaml_preprocessor { |x| x.values.each { |e| e.capitalize! } }
h.yaml_postprocessor { |yaml| yaml.sub(/\A---\s+/, '') }

puts h['a']        # outputs 'alpha'
puts h.keys.first  # outputs 'a' or 'b' depending on Hash order
puts h.to_yaml     # outputs '{a: Alpha, b: Bravo}'

Defined Under Namespace

Classes: MockNativeType

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Hash

:nodoc:



412
413
414
415
416
417
# File 'lib/sycl.rb', line 412

def initialize(*args)  # :nodoc:
  @yaml_preprocessor = nil
  @yaml_postprocessor = nil
  @yaml_style = nil
  super
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_symbol, *args, &block) ⇒ Object

Allow method call syntax: h.foo.bar.baz == h[‘bar’].

Accessing hash keys whose names overlap with names of Ruby Object built-in methods (id, type, etc.) will still need to be passed in with bracket notation (h instead of h.type).



475
476
477
478
479
480
481
482
483
484
485
# File 'lib/sycl.rb', line 475

def method_missing(method_symbol, *args, &block)
  key = method_symbol.to_s
  set = key.chomp!('=')
  if set
    self[key] = args.first
  elsif self.key?(key)
    self[key]
  else
    nil
  end
end

Class Method Details

.[](*args) ⇒ Object

:nodoc:



419
420
421
# File 'lib/sycl.rb', line 419

def self.[](*args)  # :nodoc:
  Sycl::Hash.from_hash super
end

.from_hash(h) ⇒ Object

Create a Sycl::Array from a normal Hash or Hash-like object. Every child Array or Hash gets promoted to a Sycl::Array or Sycl::Hash.



433
434
435
436
437
# File 'lib/sycl.rb', line 433

def self.from_hash(h)
  retval = Sycl::Hash.new
  h.each { |k, v| retval[k] = Sycl::from_object(v) }
  retval
end

.load_file(f) ⇒ Object

Like Sycl::load_file(), a shortcut method to create a Sycl::Hash from loading and parsing YAML from a file.



426
427
428
# File 'lib/sycl.rb', line 426

def self.load_file(f)
  Sycl::Hash.from_hash YAML::load_file f
end

Instance Method Details

#<=>(another) ⇒ Object

:nodoc:



550
551
552
# File 'lib/sycl.rb', line 550

def <=>(another)  # :nodoc:
  self.to_str <=> another.to_str
end

#[]=(k, v) ⇒ Object

Make sure that if we write to this hash, we promote any inputs to their Sycl equivalents. This lets dot notation, styled YAML, and other Sycl goodies continue.



444
445
446
447
448
449
# File 'lib/sycl.rb', line 444

def []=(k, v)  # :nodoc:
  unless v.is_a?(Sycl::Hash) || v.is_a?(Sycl::Array)
    v = Sycl::from_object(v)
  end
  super
end

#deep_merge(h) ⇒ Object

Deep merge two hashes (the new hash wins on conflicts). Hash or and Array objects in the new hash are promoted to Sycl variants.



532
533
534
535
536
537
538
539
540
541
542
# File 'lib/sycl.rb', line 532

def deep_merge(h)
  self.merge(h) do |key, v1, v2|
    if v1.is_a?(::Hash) && v2.is_a?(Sycl::Hash)
      self[key].deep_merge(v2)
    elsif v1.is_a?(::Hash) && v2.is_a?(::Hash)
      self[key].deep_merge(Sycl::Hash.from_hash(v2))
    else
      self[key] = Sycl::from_object(v2)
    end
  end
end

#encode_with(coder) ⇒ Object

:nodoc:



669
670
671
672
# File 'lib/sycl.rb', line 669

def encode_with(coder)  # :nodoc:
  coder.style = Psych::Nodes::Mapping::FLOW if @yaml_style == :inline
  coder.represent_map nil, sort
end

#get(path) ⇒ Object

Safe dotted notation reads: h.get(‘foo.bar’) == h[‘bar’].

This will return nil instead of dying if h does not exist.



492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/sycl.rb', line 492

def get(path)
  path = path.split(/\./) if path.is_a?(String)
  candidate = self
  while !path.empty?
    key = path.shift
    if candidate[key]
      candidate = candidate[key]
    else
      candidate = nil
      last
    end
  end
  candidate
end

#merge!(h) ⇒ Object

:nodoc:



458
459
460
461
# File 'lib/sycl.rb', line 458

def merge!(h)  # :nodoc:
  h = Sycl::Hash.from_hash(h) unless h.is_a?(Sycl::Hash)
  super
end

#method(sym) ⇒ Object

:nodoc:



638
639
640
# File 'lib/sycl.rb', line 638

def method(sym)  # :nodoc:
  sym == :to_yaml ? MockNativeType.new : super
end

#render_inline!Object

Make this hash, and its children, rendered in inline/flow style. The default is to render arrays in block (multi-line) style.



562
563
564
# File 'lib/sycl.rb', line 562

def render_inline!
  @yaml_style = :inline
end

#render_values_inline!Object

Keep rendering this hash in block (multi-line) style, but, make this array’s children rendered in inline/flow style.

Example:

h = Sycl::Hash.new
h['one'] = 'two'
h['three'] = %w{four five}
h.yaml_postprocessor { |yaml| yaml.sub(/\A---\s+/, '') }

h.render_values_inline!
puts h.to_yaml  # output: "one: two\nthree: [five four]"
h.render_inline!
puts h.to_yaml  # output: '{one: two, three: [five four]}'


581
582
583
584
585
# File 'lib/sycl.rb', line 581

def render_values_inline!
  self.values.each do |v|
    v.render_inline! if v.respond_to?(:render_inline!)
  end
end

#set(path, value) ⇒ Object

Dotted writes: h.set(‘foo.bar’ => ‘baz’) means h[‘bar’] = ‘baz’.

This will auto-vivify any missing intervening hash keys, and also promote Hash and Array objects in the input to Scyl variants.



513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/sycl.rb', line 513

def set(path, value)
  path = path.split(/\./) if path.is_a?(String)
  target = self
  while path.size > 1
    key = path.shift
    if !(target.key?(key) && target[key].is_a?(::Hash))
      target[key] = Sycl::Hash.new
    else
      target[key] = Sycl::Hash.from_hash(target[key])
    end
    target = target[key]
  end
  target[path.first] = value
end

#store(k, v) ⇒ Object

:nodoc:



451
452
453
454
455
456
# File 'lib/sycl.rb', line 451

def store(k, v)  # :nodoc:
  unless v.is_a?(Sycl::Hash) || v.is_a?(Sycl::Array)
    v = Sycl::from_object(v)
  end
  super
end

#to_strObject

:nodoc:



554
555
556
# File 'lib/sycl.rb', line 554

def to_str  # :nodoc:
  self.keys.sort.first
end

#to_yaml(opts = {}) ⇒ Object

Render this object as YAML. Before rendering, run the object through any yaml_preprocessor() code block. After rendering, filter the YAML text through any yaml_postprocessor() code block.

Nodes marked with render_inline!() or render_values_inline!() will be output in flow/inline style, all hashes and arrays will be sorted, and we set a long line width to more or less support line wrap under the Psych library.



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/sycl.rb', line 652

def to_yaml(opts = {})
  yaml_preprocess!
  if defined?(YAML::ENGINE) && YAML::ENGINE.yamler == 'psych'
    opts ||= {}
    opts[:line_width] ||= 999999  # Psych doesn't let you disable line wrap
    yaml = super
  else
    yaml = YAML::quick_emit(self, opts) do |out|
      out.map(nil, @yaml_style || to_yaml_style) do |map|
        sort.each { |k, v| map.add(k, v) }
      end
    end
  end
  yaml_postprocess yaml
end

#update(h) ⇒ Object

:nodoc:



463
464
465
466
# File 'lib/sycl.rb', line 463

def update(h)  # :nodoc:
  h = Sycl::Hash.from_hash(h) unless h.is_a?(Sycl::Hash)
  super
end

#yaml_postprocess(yaml) ⇒ Object

:nodoc:



623
624
625
# File 'lib/sycl.rb', line 623

def yaml_postprocess(yaml)  # :nodoc:
  @yaml_postprocessor ? @yaml_postprocessor.call(yaml) : yaml
end

#yaml_postprocessor(&block) ⇒ Object

Set a postprocessor hook which runs after YML is dumped, for example, via to_yaml() or Sycl::dump(). The hook is a block that gets the YAML text string as an argument, and returns a new, possibly different, YAML text string.

A common example use case is to suppress the initial document separator, which is just visual noise when humans are viewing or editing a single YAML file:

a.yaml_postprocessor { |yaml| yaml.sub(/\A---\s+/, '') }

Your conventions might also prohibit trailing whitespace, which at least the Syck library will tack on the end of YAML hash keys:

a.yaml_postprocessor { |yaml| yaml.gsub(/:\s+$/, '') }


615
616
617
# File 'lib/sycl.rb', line 615

def yaml_postprocessor(&block)
  @yaml_postprocessor = block if block_given?
end

#yaml_preprocess!Object

:nodoc:



619
620
621
# File 'lib/sycl.rb', line 619

def yaml_preprocess!  # :nodoc:
  @yaml_preprocessor.call(self) if @yaml_preprocessor
end

#yaml_preprocessor(&block) ⇒ Object

Set a preprocessor hook which runs before each time YAML is dumped, for example, via to_yaml() or Sycl::dump(). The hook is a block that gets the object itself as an argument. The hook can then set render_inline!() or similar style arguments, prune nil or empty leaf values from hashes, or do whatever other styling needs to be done before a Sycl object is rendered as YAML.



595
596
597
# File 'lib/sycl.rb', line 595

def yaml_preprocessor(&block)
  @yaml_preprocessor = block if block_given?
end