lib/skemata/node.rb
# 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