require 'sparql/algebra' require 'sparql/extensions' module ShEx::Algebra ## # The ShEx operator. # # @abstract class Operator extend SPARQL::Algebra::Expression include RDF::Util::Logger # Location of schema including this operator attr_accessor :schema # Initialization options attr_accessor :options ARITY = -1 # variable arity ## # Initializes a new operator instance. # # @overload initialize(*operands) # @param [Array<RDF::Term>] operands # # @overload initialize(*operands, options) # @param [Array<RDF::Term>] operands # @param [Hash{Symbol => Object}] options # any additional options # @option options [Boolean] :memoize (false) # whether to memoize results for particular operands # @raise [TypeError] if any operand is invalid def initialize(*operands) @options = operands.last.is_a?(Hash) ? operands.pop.dup : {} @operands = operands.map! do |operand| case operand when Array operand.each do |op| op.parent = self if op.respond_to?(:parent=) end operand when Operator, RDF::Term, RDF::Query, RDF::Query::Pattern, Array, Symbol operand.parent = self if operand.respond_to?(:parent=) operand when TrueClass, FalseClass, Numeric, String, DateTime, Date, Time RDF::Literal(operand) when NilClass raise ArgumentError, "Found nil operand for #{self.class.name}" else raise TypeError, "invalid ShEx::Algebra::Operator operand: #{operand.inspect}" end end if options[:logger] options[:depth] = 0 each_descendant(1) do |depth, operand| if operand.respond_to?(:options) operand.options[:logger] = options[:logger] operand.options[:depth] = depth end end end end ## # Is this shape closed? # @return [Boolean] def closed? operands.include?(:closed) end ## # Semantic Actions # @return [Array<SemAct>] def semantic_actions operands.select {|o| o.is_a?(SemAct)} end # Does this operator include Satisfiable? def satisfiable?; false; end # Does this operator include TripleExpression? def triple_expression?; false; end # Does this operator a SemAct? def semact?; false; end ## # Exception handling def not_matched(message, **opts) expression = opts.fetch(:expression, self) exception = opts.fetch(:exception, NotMatched) log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"} end def not_satisfied(message, **opts) expression = opts.fetch(:expression, self) exception = opts.fetch(:exception, ShEx::NotSatisfied) log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"} end def structure_error(message, **opts) expression = opts.fetch(:expression, self) exception = opts.fetch(:exception, ShEx::StructureError) log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"} end def status(message, &block) log_info(self.class.const_get(:NAME), message, depth: options.fetch(:depth, 0), &block) true end ## # The operands to this operator. # # @return [Array] attr_reader :operands ## # Returns the operand at the given `index`. # # @param [Integer] index # an operand index in the range `(0...(operands.count))` # @return [RDF::Term] def operand(index = 0) operands[index] end ## # Returns the SPARQL S-Expression (SSE) representation of this operator. # # @return [Array] # @see http://openjena.org/wiki/SSE def to_sxp_bin operator = [self.class.const_get(:NAME)].flatten.first [operator, *(operands || []).map(&:to_sxp_bin)] end ## # Returns an S-Expression (SXP) representation of this operator # # @return [String] def to_sxp begin require 'sxp' # @see http://rubygems.org/gems/sxp rescue LoadError abort "SPARQL::Algebra::Operator#to_sxp requires the SXP gem (hint: `gem install sxp')." end require 'sparql/algebra/sxp_extensions' to_sxp_bin.to_sxp end ## # Returns a developer-friendly representation of this operator. # # @return [String] def inspect sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, operands.to_sse.gsub(/\s+/m, ' ')) end ## # @param [Statement] other # @return [Boolean] def eql?(other) other.class == self.class && other.operands == self.operands end alias_method :==, :eql? ## # Enumerate via depth-first recursive descent over operands, yielding each operator # @param [Integer] depth incrementeded for each depth of operator, and provided to block if Arity is 2 # @yield operator # @yieldparam [Object] operator # @return [Enumerator] def each_descendant(depth = 0, &block) if block_given? operands.each do |operand| case operand when Array operand.each do |op| op.each_descendant(depth + 1, &block) if op.respond_to?(:each_descendant) end else operand.each_descendant(depth + 1, &block) if operand.respond_to?(:each_descendant) end case block.arity when 1 then block.call(operand) else block.call(depth, operand) end end end enum_for(:each_descendant) end alias_method :descendants, :each_descendant alias_method :each, :each_descendant ## # Parent expression, if any # # @return [Operator] def parent; @options[:parent]; end ## # Parent operator, if any # # @return [Operator] def parent=(operator) @options[:parent]= operator end ## # First ancestor operator of type `klass` # # @param [Class] klass # @return [Operator] def first_ancestor(klass) parent.is_a?(klass) ? parent : parent.first_ancestor(klass) if parent end ## # Validate all operands, operator specific classes should override for operator-specific validation # @return [SPARQL::Algebra::Expression] `self` # @raise [ShEx::StructureError] if the value is invalid def validate! operands.each {|op| op.validate! if op.respond_to?(:validate!)} self end ## # A unary operator. # # Operators of this kind take one operand. # # @abstract class Unary < Operator ARITY = 1 ## # @param [RDF::Term] arg1 # the first operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) def initialize(arg1, options = {}) raise ArgumentError, "wrong number of arguments (given 2, expected 1)" unless options.is_a?(Hash) super end end # Unary ## # A binary operator. # # Operators of this kind take two operands. # # @abstract class Binary < Operator ARITY = 2 ## # @param [RDF::Term] arg1 # the first operand # @param [RDF::Term] arg2 # the second operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) def initialize(arg1, arg2, options = {}) raise ArgumentError, "wrong number of arguments (given 3, expected 2)" unless options.is_a?(Hash) super end end # Binary end end