mybanktracker/skemata

View on GitHub
lib/skemata/node.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Skemata
  class Node
    ALLOWED_OPTS = %i[type root_object].freeze
    #
    # Prepares internal data hash and assigns locals
    # @param opts = {} [Hash] See Skemata::DSL.draw for valid
    #                         opts
    #
    # @return [Node] A Node class.
    def initialize(opts = {})
      ALLOWED_OPTS.each { |o| instance_variable_set("@#{o}", opts[o]) }
      @data = { '@type' => type }
      @data['@context'] = 'https://schema.org' if opts.fetch(:is_root, true)
    end

    #
    # Decorate the node with a new property (as delegated by method_missing)
    #
    # @param name [Symbol] Key name
    # @param *args [Array] Varargs for attributes describing key
    # @param &block [Proc] Body for a child node, if present
    #
    def decorate(name, *args, &block)
      # Draw another node
      return route_block(name, *args, &block) if block.present?
      # Or populate the hash
      data[attify_token(name)] = extract(args.first || name.to_sym)
    end

    attr_reader :data

    private

    attr_reader :root_object, :type

    RESERVED_SCHEMA_TOKENS = %w[id type context].freeze
    #
    # Interpolate @ into string of reserved schema.org names
    # @param token [String] Key
    #
    # @return [String]
    def attify_token(token)
      RESERVED_SCHEMA_TOKENS.include?(token) ? "@#{token}" : token
    end

    #
    # Driver for #fetch_property. If passed a NodeMethodChain (Array), fold
    # the chain of methods until the final value. If passed a Symbol,
    # just extract that single method.
    #
    # @param property [Symbol|NodeMethodChain] Propert(ies) to extract
    #
    # @return [Object] Serializable value
    def extract(property)
      case property
      when DSL::NodeMethodChain
        property.inject(root_object, &method(:fetch_property))
      when Symbol
        fetch_property(root_object, property)
      else property
      end
    rescue NoMethodError, ArgumentError
      nil
    end

    #
    # Extract property from object, if Hash, look up via #[]
    #
    # @param object [Object] Object to serialize
    # @param property [Symbol] Accessor signature
    #
    # @return [Object] Serializable value
    def fetch_property(object, property)
      object.send(object.is_a?(Hash) ? :fetch : :send, property)
    end

    def find_property(*props)
      props.inject(nil) do |m, e|
        next m if m.present?
        extract(e.to_s.underscore.to_sym)
      end
    end

    #
    # Draw a new schema.org node and merge it into the current serializable
    # hash.
    #
    # @param token [String] Name of DSL / Hash entry
    # @param type [String] schema.org type
    # @param property [Object] Anything, but if symbol,
    #   will extract from #root_object
    # @param &block [Block] DSL definition
    #
    # @return [Hash] Hash#merge! return value with new node
    def internal_draw(token, type, property, &block)
      property = root_object.send(property) if property.is_a?(Symbol)
      data.merge!(
        token.to_s => DSL.draw(
          { type: type, root_object: property, is_root: false },
          &block
        )
      ) if property.present?
    end

    #
    # If a schema entry is passed a block, extract the child's root_object
    # attribute and draw a new node. Attempts to infer the attribute name.
    #
    # The token is the schema definition key (e.g. a function invocation),
    # the type is the schema.org object type, and the last key is the explicit
    # attribute on the current node's root object. If only the token is
    # provided, or if both the token and the type are provided, we try to
    # extract an attribute with either of those names before falling back to
    # null.
    #
    # @param token [String] Invoked method name in DSL.draw block body
    # @param *args [Array] Contains [type, token]
    # @param &block [Block] The DSL definition of the child object
    #
    # @return [Hash] A copy of the data hash as returned by Hash#merge!
    def route_block(token, *args, &block)
      type, prop = args.shift(2)

      # Explicitly defined property
      child_root = extract(prop) if prop.is_a?(Symbol)

      # Hash key / token is type
      child_root = extract(token.titleize.to_sym) if type.nil? && prop.nil?

      # If we still have no data, fold to the first present
      # property by using token and type as keys
      child_root = find_property(token, type) unless child_root.present?

      internal_draw(token, type, child_root, &block)
    end
  end
end