module Krikri ## # Provides a generic interface for accessing properties from OriginalRecords. # Implement the interface, and that of `Value` on an record-type basis. # # parser = Krikri::Parser::MyParser.new(record) # parser.root # => # # class Parser attr_reader :root, :record ## # @param record [Krikri::OriginalRecord] a record whose properties can # be parsed by the parser instance. def initialize(record) @record = record end ## # Instantiates a parser object to wrap the record. Returns the record # as is if it is already parsed. # # @param record [Krikri::OriginalRecord, Krikri::Parser] the record to parse # @param args [Array, nil] the arguments to pass to the parser instance, # if any # @return [Krikri::Parser] a parsed record object def self.parse(record, *args) record.is_a?(Krikri::Parser) ? record : new(record, *args) end ## # @return [String] the local_name of the OriginalRecord wrapped by this # parser def local_name record.local_name end ## # A generic parser value. # # Interface to a single value node which can access typed data values (e.g. # String, DateTime, etc...) parsed from a string, and provides access to # child nodes and attributes. class Value ## # Property accessor interface. Passes a name expression (`name_exp`) to # the local implementation of #get_child_nodes. # # The given name expression must follow the pattern: # name [| name ...] # # The optional "|" is a short-circuit operator that will return the # property or element in the document for the first matching part of the # phrase. # # @example # va = value['title'] # va = value['this|that'] # gives value for "this" if both defined # # @param name [String] An expression of named properties to access # @return [Krikri::Parser::ValueArray] def [](name_exp) name_exp.strip.split(/\s*\|\s*/).each do |n| result = get_child_nodes(n) return result unless result.empty? end Krikri::Parser::ValueArray.new([]) end ## # Queries whether `name` is a subproperty of this node # @param name [#to_sym] a named property to query # @return [Boolean] true if `name` is a subproperty of the current node def child?(name) children.include?(name) end ## # @abstract # @return [Array] a list of subproperties that can be passed back # to #[] to access child nodes def children raise NotImplementedError end ## # @abstract # @return [<#to_s>] typed value for the property def value raise NotImplementedError end ## # @abstract # @return [Boolean] true if this node has typed values accessible with # #values def values? raise NotImplementedError end ## # @abstract # @return [Array] a list of attributes accessible on the node def attributes raise NotImplementedError end ## # Queries whether `name` is an attribute of this node # @param name [#to_sym] an attribute name to query # @return [Boolean] true if `name` is an attribute of the current node def attribute?(name) begin attributes.include?(name) rescue NotImplementedError false end end def method_missing(name, *args, &block) return attribute(name) if attribute?(name) super end def respond_to_missing(method, *) attribute?(method) || super end private ## # @abstract def attribute(name) raise NotImplementedError, "Can't access attribute #{name}" end ## # @abstract Return a Krikri::Parser::ValueArray of child nodes # # @param name [String] Element or property name # @return [Krikri::Parser::ValueArray] The child nodes def get_child_nodes(name) raise NotImplementedError, "Can't access property #{name}" end end ## # A specialized Array object for containing Parser::Values. Provides methods # for accessing and filtering values that can be chained. # # @example chaining methods to select values # # my_value_array.field('dc:creator', 'foaf:name') # .match_attribute('first_name').values # # Methods defined on this class should return another ValueArray, an Array # of literal values (retrieved from Parser::Value#value), or a single # literal value. # # Uses `#bindings` to track variables for recovery via `#else`, `#from`, and #`#back`. Methods that return a `ValueArray` pass `#bindings` down to the # new instance. # # @example # # my_value_array.field('dc:creator').bind(:creator) # # # @example `#if` sets `bindings[:top]`, `#else` recoveres if empty # # my_value_array.field('dc:creator', 'foaf:name').if.field('empty:field') # .else { |vs| vs.field('some:otherField') } # class ValueArray include Enumerable # @!attribute [r] bindings # A hash containing bindings of variables to (Symbols) to ValueArrays attr_reader :bindings delegate :[], :each, :empty?, :to_a, :to_ary, :to => :@array ## # @param array [Array] an array of values to delegate array operations to # @param bindings [Hash] # # @return [ValueArray] an array containing nodes for which the specified # attribute does not have a child node matching the given child name def match_child(*children) select { |v| !(v.children & children).empty? } end ## # @param name [#to_sym] an attribute name # @param other [Object] an object to check for equality with the # values from the given attribute. # # @yield [value] yields each value with the attribute in name to the block # # @return [ValueArray] an array containing nodes for which the specified # attribute does not have a value matching the given attribute name, # object, and block. # # @see #match_attribute for examples; this calls #reject, where it calls # #select. def reject_attribute(name, other = nil, &block) reject(&compare_to_attribute(name, other, &block)) end ## # @param *children [Array] # # @return [ValueArray] an array containing nodes for which the specified # attribute does not have a childnode matching the given child name def reject_child(*children) reject { |v| !(v.children & children).empty? } end ## # Wraps the root node of the given record in this class. # # @param record [Krikri::Parser] a parsed record to wrap in a ValueArray # @return [ValueArray] def self.build(record) new([record.root]) end protected def get_field(name) self.class.new(flat_map { |val| val[name] }, bindings) end private ## # @see #match_attribute, #reject_attribute def compare_to_attribute(name, other, &block) lambda do |v| next unless v.attribute?(name.to_sym) result = v.send(name) result = yield(result) if block_given? return result == other if other result end end public class InvalidParserValueError < TypeError; end end end end