lib/sparql/algebra/expression.rb in sparql-1.0.7 vs lib/sparql/algebra/expression.rb in sparql-1.0.8
- old
+ new
@@ -124,20 +124,83 @@
operands << options unless options.empty?
operator.new(*operands)
end
##
+ # Register an extension function.
+ #
+ # Extension functions take zero or more arguments of type `RDF::Term`
+ # and return an argument of type `RDF::Term`, or raise `TypeError`.
+ #
+ # Functions are identified using the `uri` parameter and specified using a block.
+ #
+ # Arguments are evaluated, and the block is called with argument values (if a variable was unbound, an error will have been generated).
+ #
+ # It is possible to get unevaluated arguments but care must be taken not to violate the rules of function evaluation.
+ #
+ # Normally, block should be a pure evaluation based on it's arguments. It should not access a graph nor return different values for the same arguments (to allow expression optimization). Blocks can't bind a variables.
+ #
+ # @example registering a function definition applying the Ruby `crypt` method to its unary argument.
+ # SPARQL::Algebra::Expression.register_extension(RDF::URI("http://example/crypt") do |literal|
+ # raise TypeError, "argument must be a literal" unless literal.literal?
+ # RDF::Literal(literal.to_s.crypt("salt"))
+ # end
+ #
+ # @param [RDF::URI] uri
+ # @yield *args
+ # @yieldparam [Array<RDF::Term>] *args
+ # @yieldreturn [RDF::Term]
+ # @param [Proc] function
+ # @return [void]
+ # @raise [TypeError] if `uri` is not an RDF::URI or no block is given
+ def self.register_extension(uri, &block)
+ raise TypeError, "uri must be an IRI" unless uri.is_a?(RDF::URI)
+ raise TypeError, "must pass a block" unless block_given?
+ self.extensions[uri] = block
+ end
+
+ ##
+ # Registered extensions
+ #
+ # @return [Hash{RDF::URI => Proc}]
+ def self.extensions
+ @extensions ||= {}
+ end
+
+ ##
+ # Invoke an extension function.
+ #
+ # Applies a registered extension function, if registered.
+ # Otherwise, if it is an XSD Constructor function, apply
+ # that.
+ #
+ # @param [RDF::URI] function
+ # @param [Array<RDF::Term>] *args
+ # @return [RDF::Term]
+ # @see http://www.w3.org/TR/sparql11-query/#extensionFunctions
+ # @see http://www.w3.org/TR/sparql11-query/#FunctionMapping
+ def self.extension(function, *args)
+ if function.to_s.start_with?(RDF::XSD.to_s)
+ self.cast(function, args.first)
+ elsif extension_function = self.extensions[function]
+ extension_function.call(*args)
+ else
+ raise TypeError, "Extension function #{function} not recognized"
+ end
+ end
+
+ ##
# Casts operand as the specified datatype
#
# @param [RDF::URI] datatype
# Datatype to evaluate, one of:
# xsd:integer, xsd:decimal xsd:float, xsd:double, xsd:string, xsd:boolean, or xsd:dateTime
# @param [RDF::Term] value
# Value, which should be a typed literal, where the type must be that specified
# @raise [TypeError] if datatype is not a URI or value cannot be cast to datatype
- # @return [Boolean]
- # @see http://www.w3.org/TR/rdf-sparql-query/#FunctionMapping
+ # @return [RDF::Term]
+ # @see http://www.w3.org/TR/sparql11-query/#FunctionMapping
def self.cast(datatype, value)
case datatype
when RDF::XSD.dateTime
case value
when RDF::Literal::DateTime, RDF::Literal::Date, RDF::Literal::Time
@@ -147,40 +210,44 @@
else
RDF::Literal.new(value.value, :datatype => datatype, :validate => true)
end
when RDF::XSD.float, RDF::XSD.double
case value
- when RDF::Literal::Numeric, RDF::Literal::Boolean
- RDF::Literal.new(value, :datatype => datatype)
+ when RDF::Literal::Boolean
+ RDF::Literal.new(value.object ? 1 : 0, :datatype => datatype)
+ when RDF::Literal::Numeric
+ RDF::Literal.new(value.to_f, :datatype => datatype)
when RDF::Literal::DateTime, RDF::Literal::Date, RDF::Literal::Time, RDF::URI, RDF::Node
raise TypeError, "Value #{value.inspect} cannot be cast as #{datatype}"
else
RDF::Literal.new(value.value, :datatype => datatype, :validate => true)
end
when RDF::XSD.boolean
case value
when RDF::Literal::Boolean
value
when RDF::Literal::Numeric
- RDF::Literal::Boolean.new(value.value != 0)
+ RDF::Literal::Boolean.new(value.object != 0)
when RDF::Literal::DateTime, RDF::Literal::Date, RDF::Literal::Time, RDF::URI, RDF::Node
raise TypeError, "Value #{value.inspect} cannot be cast as #{datatype}"
else
RDF::Literal.new(!value.to_s.empty?, :datatype => datatype, :validate => true)
end
when RDF::XSD.decimal, RDF::XSD.integer
case value
- when RDF::Literal::Integer, RDF::Literal::Decimal, RDF::Literal::Boolean
+ when RDF::Literal::Boolean
+ RDF::Literal.new(value.object ? 1 : 0, :datatype => datatype)
+ when RDF::Literal::Integer, RDF::Literal::Decimal
RDF::Literal.new(value, :datatype => datatype)
when RDF::Literal::DateTime, RDF::Literal::Date, RDF::Literal::Time, RDF::URI, RDF::Node
raise TypeError, "Value #{value.inspect} cannot be cast as #{datatype}"
else
RDF::Literal.new(value.value, :datatype => datatype, :validate => true)
end
when RDF::XSD.string
RDF::Literal.new(value, :datatype => datatype)
else
- raise TypeError, "Expected datatype (#{datatype}) to be an XSD type"
+ raise TypeError, "Expected datatype (#{datatype}) to be a recognized XPath function"
end
rescue
raise TypeError, $!.message
end