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:



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

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



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

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:



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

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.



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

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.



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

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

Instance Method Details

#<=>(other) ⇒ Object

:nodoc:



569
570
571
572
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
# File 'lib/sycl.rb', line 569

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.



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

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.



551
552
553
554
555
556
557
558
559
560
561
# File 'lib/sycl.rb', line 551

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:



709
710
711
712
# File 'lib/sycl.rb', line 709

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.



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

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:



477
478
479
480
# File 'lib/sycl.rb', line 477

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

#method(sym) ⇒ Object

:nodoc:



678
679
680
# File 'lib/sycl.rb', line 678

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.



602
603
604
# File 'lib/sycl.rb', line 602

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


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

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.



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

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:



470
471
472
473
474
475
# File 'lib/sycl.rb', line 470

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.



692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/sycl.rb', line 692

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:



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

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

#yaml_postprocess(yaml) ⇒ Object

:nodoc:



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

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


655
656
657
# File 'lib/sycl.rb', line 655

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

#yaml_preprocess!Object

:nodoc:



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

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.



635
636
637
# File 'lib/sycl.rb', line 635

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