seomoz/interpol

View on GitHub
lib/interpol/dynamic_struct.rb

Summary

Maintainability
A
25 mins
Test Coverage
require 'hashie/mash'

module Interpol
  # Hashie::Mash is awesome: it gives us dot/method-call syntax for a hash.
  # This is perfect for dealing with structured JSON data.
  # The downside is that Hashie::Mash responds to anything--it simply
  # creates a new entry in the backing hash.
  #
  # DynamicStruct freezes a Hashie::Mash so that it no longer responds to
  # everything.  This is handy so that consumers of this gem can distinguish
  # between a fat-fingered field, and a field that is set to nil.
  module DynamicStruct
    SAFE_METHOD_MISSING = ::Hashie::Mash.superclass.instance_method(:method_missing)

    Mash = Class.new(::Hashie::Mash) do
      undef sort
    end

    def self.new(*args)
      Mash.new(*args).tap do |mash|
        mash.extend(self)
      end
    end

    def self.extended(mash)
      recursively_extend(mash)
    end

    def self.recursively_extend(object)
      case object
        when Array
          object.each { |v| recursively_extend(v) }
        when Mash
          object.extend(self) unless object.is_a?(self)
          object.each { |_, v| recursively_extend(v) }
      end
    end

    def method_missing(method_name, *args, &blk)
      if key = method_name.to_s[/\A([^?=]*)[?=]?\z/, 1]
        unless has_key?(key)
          return safe_method_missing(method_name, *args, &blk)
        end
      end

      super
    end

  private

    def safe_method_missing(*args)
      SAFE_METHOD_MISSING.bind(self).call(*args)
    end
  end
end