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:



435
436
437
438
439
440
# File 'lib/sycl.rb', line 435

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).



498
499
500
501
502
503
504
505
506
507
508
# File 'lib/sycl.rb', line 498

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:



442
443
444
# File 'lib/sycl.rb', line 442

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.



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

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.



449
450
451
# File 'lib/sycl.rb', line 449

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

Instance Method Details

#<=>(other) ⇒ Object

:nodoc:



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/sycl.rb', line 573

def <=>(other)  # :nodoc:
  self_keys = self.keys.sort
  other_keys = other.respond_to?(:keys) ?  other.keys.sort :
               other.respond_to?(:sort) ?  other.sort      :
               other.respond_to?(:to_s) ? [other.to_s]     :
               other                    ? [other]          : []

  while true
    if self_keys.empty? && other_keys.empty?
      return 0
    elsif self_keys.empty?
      return 1
    elsif other_keys.empty?
      return -1
    else
      self_key = self_keys.shift
      other_key = other_keys.shift
      if self_key != other_key
        return self_key <=> other_key
      elsif other.is_a?(Hash) && self[self_key] != other[other_key]
        if self[self_key].respond_to?(:<=>)
          return self[self_key] <=> other[other_key]
        else
          return self[self_key].to_s <=> other[other_key].to_s
        end
      end
    end
  end
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.



467
468
469
470
471
472
# File 'lib/sycl.rb', line 467

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.



555
556
557
558
559
560
561
562
563
564
565
# File 'lib/sycl.rb', line 555

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:



713
714
715
716
# File 'lib/sycl.rb', line 713

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.



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

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:



481
482
483
484
# File 'lib/sycl.rb', line 481

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

#method(sym) ⇒ Object

:nodoc:



682
683
684
# File 'lib/sycl.rb', line 682

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.



606
607
608
# File 'lib/sycl.rb', line 606

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]}'


625
626
627
628
629
# File 'lib/sycl.rb', line 625

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.



536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/sycl.rb', line 536

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:



474
475
476
477
478
479
# File 'lib/sycl.rb', line 474

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_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.



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
# File 'lib/sycl.rb', line 696

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:



486
487
488
489
# File 'lib/sycl.rb', line 486

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

#yaml_postprocess(yaml) ⇒ Object

:nodoc:



667
668
669
# File 'lib/sycl.rb', line 667

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+$/, '') }


659
660
661
# File 'lib/sycl.rb', line 659

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

#yaml_preprocess!Object

:nodoc:



663
664
665
# File 'lib/sycl.rb', line 663

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.



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

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