lib/active_triples/relation.rb in active-triples-0.10.2 vs lib/active_triples/relation.rb in active-triples-0.11.0
- old
+ new
@@ -19,11 +19,14 @@
#
#
# @see RDF::Term
class Relation
include Enumerable
+ include Comparable
+ TYPE_PROPERTY = { predicate: RDF.type, cast: false }.freeze
+
# @!attribute [rw] parent
# @return [RDFSource] the resource that is the domain of this relation
# @!attribute [rw] value_arguments
# @return [Array<Object>]
# @!attribute [rw] rel_args
@@ -31,110 +34,115 @@
# @!attribute [r] reflections
# @return [Class]
attr_accessor :parent, :value_arguments, :rel_args
attr_reader :reflections
- delegate :<=>, :==, :===, :[], :each, :empty?, :equal, :inspect, :last,
- :to_a, :to_ary, :size, :join, :length, :+, :to => :result
+ delegate :[], :inspect, :last, :size, :join, to: :to_a
##
# @param [ActiveTriples::RDFSource] parent_source
# @param [Array<Symbol, Hash>] value_arguments if a Hash is passed as the
# final element, it is removed and set to `@rel_args`.
def initialize(parent_source, value_arguments)
self.parent = parent_source
@reflections = parent_source.reflections
self.rel_args ||= {}
- self.rel_args = value_arguments.pop if value_arguments.is_a?(Array) &&
- value_arguments.last.is_a?(Hash)
+ self.rel_args = value_arguments.pop if
+ value_arguments.is_a?(Array) && value_arguments.last.is_a?(Hash)
+
self.value_arguments = value_arguments
end
##
- # Empties the `Relation`, deleting any associated triples from `parent`.
+ # @param array [#to_ary, ActiveTriples::Relation]
+ # @return [Array]
#
- # @return [Relation] self; a now empty relation
- def clear
- parent.delete([rdf_subject, predicate, nil])
+ # @note simply passes to `Array#&` unless argument is a `Relation`
+ #
+ # @see Array#&
+ def &(array)
+ return to_a & array unless array.is_a? Relation
- self
+ (objects.to_a & array.objects.to_a)
+ .map { |object| convert_object(object) }
end
-
+
##
- # Gives an {Array} containing the result set for the {Relation}.
+ # @param array [#to_ary, ActiveTriples::Relation]
+ # @return [Array]
#
- # By default, {RDF::URI} and {RDF::Node} results are cast to `RDFSource`.
- # {Literal} results are given as their `#object` representations (e.g.
- # {String}, {Date}.
+ # @note simply passes to `Array#|` unless argument is a `Relation`
#
- # @example results with default casting
- # parent << [parent.rdf_subject, predicate, 'my value']
- # parent << [parent.rdf_subject, predicate, Date.today]
- # parent << [parent.rdf_subject, predicate, RDF::URI('http://ex.org/#me')]
- # parent << [parent.rdf_subject, predicate, RDF::Node.new]
- # relation.result
- # # => ["my_value",
- # # Fri, 25 Sep 2015,
- # # #<ActiveTriples::Resource:0x3f8...>,
- # # #<ActiveTriples::Resource:0x3f8...>]
+ # @see Array#|
+ def |(array)
+ return to_a | array unless array.is_a? Relation
+
+ (objects.to_a | array.objects.to_a)
+ .map { |object| convert_object(object) }
+ end
+
+ ##
+ # @param array [#to_ary, ActiveTriples::Relation]
+ # @return [Array]
#
- # When `cast?` is `false`, {RDF::Resource} values are left in their raw
- # form. Similarly, when `#return_literals?` is `true`, literals are
- # returned in their {RDF::Literal} form, preserving language tags,
- # datatype, and value.
+ # @note simply passes to `Array#+` unless argument is a `Relation`
#
- # @example results with `cast?` set to `false`
- # relation.result
- # # => ["my_value",
- # # Fri, 25 Sep 2015,
- # # #<RDF::URI:0x3f8... URI:http://ex.org/#me>,
- # # #<RDF::Node:0x3f8...(_:g69843536054680)>]
+ # @see Array#+
+ def +(array)
+ return to_a + array unless array.is_a? Relation
+
+ (objects.to_a + array.objects.to_a)
+ .map { |object| convert_object(object) }
+ end
+
+ ##
+ # Mimics `Set#<=>`, returning `0` when set membership is equivalent, and
+ # `nil` (as non-comparable) otherwise. Unlike `Set#<=>`, uses `#==` for
+ # member comparisons.
#
- # @example results with `return_literals?` set to `true`
- # relation.result
- # # => [#<RDF::Literal:0x3f8...("my_value")>,
- # # #<RDF::Literal::Date:0x3f8...("2015-09-25"^^<http://www.w3.org/2001/XMLSchema#date>)>,
- # # #<ActiveTriples::Resource:0x3f8...>,
- # # #<ActiveTriples::Resource:0x3f8...>]
+ # @param [Object] other
#
- # @return [Array<Object>] the result set
- def result
- return [] if predicate.nil?
- statements = parent.query(:subject => rdf_subject,
- :predicate => predicate)
- statements.each_with_object([]) do |x, collector|
- converted_object = convert_object(x.object)
- collector << converted_object unless converted_object.nil?
+ # @see Set#<=>
+ def <=>(other)
+ return nil unless other.respond_to?(:each)
+
+ if empty?
+ return 0 if other.each.first.nil?
+ return nil
end
+
+ # We'll need to traverse `other` repeatedly, so we get a stable `Array`
+ # representation. This avoids any repeated query cost if `other` is a
+ # `Relation`.
+ length = 0
+ other = other.to_a
+ this = each
+
+ loop do
+ begin
+ cur = this.next
+ rescue StopIteration
+ return other.length == length ? 0 : nil
+ end
+
+ length += 1
+
+ return nil if other.length < length || !other.include?(cur)
+ end
end
##
- # Adds values to the relation
+ # Adds values to the result set
#
- # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
- # or a single value. If not an {RDF::Resource}, the values will be
- # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
+ # @param values [Object, Array<Object>] values to add
#
# @return [Relation] a relation containing the set values; i.e. `self`
- #
- # @raise [ActiveTriples::UndefinedPropertyError] if the property is not
- # already an {RDF::Term} and is not defined in `#property_config`
- #
- # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For
- # documentation on {RDF::Statement} and the handling of
- # non-{RDF::Resource} values.
- def set(values)
- raise UndefinedPropertyError.new(property, reflections) if predicate.nil?
- values = values.to_a if values.is_a? Relation
- values = [values].compact unless values.kind_of?(Array)
-
- clear
- values.each { |val| set_value(val) }
-
- parent.persist! if parent.persistence_strategy.is_a? ParentStrategy
- self
+ def <<(values)
+ values = prepare_relation(values) if values.is_a?(Relation)
+ self.set(objects.to_a | Array.wrap(values))
end
+ alias_method :push, :<<
##
# Builds a node with the given attributes, adding it to the relation.
#
# @param attributes [Hash] a hash of attribute names and values for the
@@ -193,21 +201,31 @@
end
end
end
##
- # @note this method behaves somewhat differently from `Array#delete`.
+ # Empties the `Relation`, deleting any associated triples from `parent`.
+ #
+ # @return [Relation] self; a now empty relation
+ def clear
+ parent.delete([rdf_subject, predicate, nil])
+
+ self
+ end
+
+ ##
+ # @note this method behaves somewhat differently from `Array#delete`.
# It succeeds on deletion of non-existing values, always returning
- # `self` unless an error is raised. There is no option to pass a block
- # to evaluate if the value is not present. This is because access for
+ # `self` unless an error is raised. There is no option to pass a block
+ # to evaluate if the value is not present. This is because access for
# `value` depends on query time. i.e. the `Relation` set does not have an
# underlying efficient data structure allowing a reliably cheap existence
# check.
#
- # @note symbols are treated as RDF::Nodes by default in
+ # @note symbols are treated as RDF::Nodes by default in
# `RDF::Mutable#delete`, but may also represent tokens in statements.
- # This casts symbols to a literals, which gets us symmetric behavior
+ # This casts symbols to a literals, which gets us symmetric behavior
# between `#set(:sym)` and `#delete(:sym)`.
#
# @example deleting a value
# resource = MySource.new
# resource.title = ['moomin', 'valley']
@@ -217,27 +235,27 @@
# @example note the behavior of unmatched values
# resource = MySource.new
# resource.title = 'moomin'
# resource.title.delete('valley') # => ["moomin"]
# resource.title # => ['moomin']
- #
+ #
# @param value [Object] the value to delete from the relation
# @return [ActiveTriples::Relation] self
def delete(value)
value = RDF::Literal(value) if value.is_a? Symbol
parent.delete([rdf_subject, predicate, value])
self
end
##
- # A variation on `#delete`. This queries the relation for matching
+ # A variation on `#delete`. This queries the relation for matching
# values before running the deletion, returning `nil` if it does not exist.
#
# @param value [Object] the value to delete from the relation
#
- # @return [Object, nil] `nil` if the value doesn't exist; the value
+ # @return [Object, nil] `nil` if the value doesn't exist; the value
# otherwise
# @see #delete
def delete?(value)
value = RDF::Literal(value) if value.is_a? Symbol
@@ -246,75 +264,98 @@
delete(value)
value
end
##
- # @overload subtract(enum)
- # Deletes objects in the enumerable from the relation
- # @param values [Enumerable] an enumerable of objects to delete
- # @overload subtract(*values)
- # Deletes each argument value from the relation
- # @param *values [Array<Object>] the objects to delete
+ # Gives a result set for the `Relation`.
#
- # @return [Relation] self
+ # By default, `RDF::URI` and `RDF::Node` results are cast to `RDFSource`.
+ # When `cast?` is `false`, `RDF::Resource` values are left in their raw
+ # form.
#
- # @note This casts symbols to a literals, which gets us symmetric behavior
- # with `#set(:sym)`.
- # @see #delete
- def subtract(*values)
- values = values.first if values.first.is_a? Enumerable
- statements = values.map do |value|
- value = RDF::Literal(value) if value.is_a? Symbol
- [rdf_subject, predicate, value]
+ # `Literal` results are cast as follows:
+ #
+ # - Simple string literals are returned as `String`
+ # - `rdf:langString` literals are always returned as raw `Literal` objects,
+ # retaining their language tags.
+ # - Typed literals are cast to their Ruby `#object` when their datatype
+ # is associated with a `Literal` subclass.
+ #
+ # @example results with default casting
+ # datatype = RDF::URI("http://example.com/custom_type")
+ #
+ # parent << [parent.rdf_subject, predicate, 'my value']
+ # parent << [parent.rdf_subject, predicate, RDF::Literal('my_value',
+ # datatype: datatype)]
+ # parent << [parent.rdf_subject, predicate, Date.today]
+ # parent << [parent.rdf_subject, predicate, RDF::URI('http://ex.org/#me')]
+ # parent << [parent.rdf_subject, predicate, RDF::Node.new]
+ #
+ # relation.to_a
+ # # => ["my_value",
+ # # "my_value" R:L:(Literal),
+ # # Fri, 25 Sep 2015,
+ # # #<ActiveTriples::Resource:0x3f8...>,
+ # # #<ActiveTriples::Resource:0x3f8...>]
+ #
+ # @example results with `cast?` set to `false`
+ # relation.to_a
+ # # => ["my_value",
+ # # "my_value" R:L:(Literal),
+ # # Fri, 25 Sep 2015,
+ # # #<RDF::URI:0x3f8... URI:http://ex.org/#me>,
+ # # #<RDF::Node:0x3f8...(_:g69843536054680)>]
+ #
+ # @return [Enumerator<Object>] the result set
+ def each
+ return [].to_enum if predicate.nil?
+
+ if block_given?
+ objects do |object|
+ converted_object = convert_object(object)
+ yield converted_object unless converted_object.nil?
+ end
end
-
- parent.delete(*statements)
- self
+
+ to_enum
end
##
- # Replaces the first argument with the second as a value within the
- # relation.
- #
- # @example
- #
- #
- # @param swap_out [Object] the value to delete
- # @param swap_in [Object] the replacement value
- #
- # @return [Relation] self
- def swap(swap_out, swap_in)
- self.<<(swap_in) if delete?(swap_out)
+ # @return [Boolean] true if the results are empty.
+ def empty?
+ objects.empty?
end
##
+ # @deprecated for removal in 1.0.0. Use `first || build({})`,
+ # `build({}) if empty?` or similar logic.
+ #
# @return [Object] the first result, if present; else a newly built node
#
# @see #build
def first_or_create(attributes={})
- result.first || build(attributes)
+ warn 'DEPRECATION: #first_or_create is deprecated for removal in 1.0.0.'
+ first || build(attributes)
end
##
- # Adds values to the result set
- #
- # @param values [Object, Array<Object>] values to add
- #
- # @return [Relation] a relation containing the set values; i.e. `self`
- def <<(values)
- values = Array.wrap(result) | Array.wrap(values)
- self.set(values)
+ # @return [Integer]
+ def length
+ objects.to_a.length
end
- alias_method :push, :<<
##
- # @return [Hash<Symbol, ]
- # @todo find a way to simplify this?
- def property_config
- return type_property if is_type?
-
- reflections.reflect_on_property(property)
+ # Gives the predicate used by the Relation. Values of this object are
+ # those that match the pattern `<rdf_subject> <predicate> [value] .`
+ #
+ # @return [RDF::Term, nil] the predicate for this relation; nil if
+ # no predicate can be found
+ #
+ # @see #property
+ def predicate
+ return property if property.is_a?(RDF::Term)
+ property_config[:predicate] if is_property?
end
##
# Returns the property for the Relation. This may be a registered
# property key or an {RDF::URI}.
@@ -324,37 +365,140 @@
def property
value_arguments.last
end
##
- # Gives the predicate used by the Relation. Values of this object are
- # those that match the pattern `<rdf_subject> <predicate> [value] .`
+ # Adds values to the relation
#
- # @return [RDF::Term, nil] the predicate for this relation; nil if
- # no predicate can be found
+ # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
+ # or a single value. If not an {RDF::Resource}, the values will be
+ # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
#
- # @see #property
- def predicate
- return property if property.is_a?(RDF::Term)
- property_config[:predicate] if is_property?
+ # @return [Relation] a relation containing the set values; i.e. `self`
+ #
+ # @raise [ActiveTriples::UndefinedPropertyError] if the property is not
+ # already an {RDF::Term} and is not defined in `#property_config`
+ #
+ # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For
+ # documentation on {RDF::Statement} and the handling of
+ # non-{RDF::Resource} values.
+ def set(values)
+ raise UndefinedPropertyError.new(property, reflections) if predicate.nil?
+
+ values = prepare_relation(values) if values.is_a?(Relation)
+ values = [values].compact unless values.kind_of?(Array)
+
+ clear
+ values.each { |val| set_value(val) }
+
+ parent.persist! if parent.persistence_strategy.is_a? ParentStrategy
+ self
end
+ ##
+ # @overload subtract(enum)
+ # Deletes objects in the enumerable from the relation
+ # @param values [Enumerable] an enumerable of objects to delete
+ # @overload subtract(*values)
+ # Deletes each argument value from the relation
+ # @param *values [Array<Object>] the objects to delete
+ #
+ # @return [Relation] self
+ #
+ # @note This casts symbols to a literals, which gets us symmetric behavior
+ # with `#set(:sym)`.
+ # @see #delete
+ def subtract(*values)
+ values = values.first if values.first.is_a? Enumerable
+ statements = values.map do |value|
+ value = RDF::Literal(value) if value.is_a? Symbol
+ [rdf_subject, predicate, value]
+ end
+
+ parent.delete(*statements)
+ self
+ end
+
+ ##
+ # Replaces the first argument with the second as a value within the
+ # relation.
+ #
+ # @param swap_out [Object] the value to delete
+ # @param swap_in [Object] the replacement value
+ #
+ # @return [Relation] self
+ def swap(swap_out, swap_in)
+ self.<<(swap_in) if delete?(swap_out)
+ end
+
protected
+ ##
+ # Converts an object to the appropriate class.
+ #
+ # Literals are cast only when the datatype is known.
+ #
+ # @private
+ def convert_object(value)
+ case value
+ when RDFSource
+ value
+ when RDF::Literal
+ if value.simple?
+ value.object
+ elsif value.has_datatype?
+ RDF::Literal.datatyped_class(value.datatype.to_s) ? value.object : value
+ else
+ value
+ end
+ when RDF::Resource
+ make_node(value)
+ else
+ value
+ end
+ end
+
+ ##
+ # @private
def node_cache
@node_cache ||= {}
end
+ ##
+ # @private
+ def objects(&block)
+ solutions = parent.query(subject: rdf_subject, predicate: predicate)
+ solutions.extend(RDF::Enumerable) unless solutions.respond_to?(:each_object)
+
+ solutions.each_object(&block)
+ end
+
+ private
+ ##
+ # @private
def is_property?
reflections.has_property?(property) || is_type?
end
+ ##
+ # @private
def is_type?
(property == RDF.type || property.to_s == "type") &&
(!reflections.kind_of?(RDFSource) || !is_property?)
end
+ ##
+ # @private
+ # @return [Hash<Symbol, ]
+ def property_config
+ return TYPE_PROPERTY if is_type?
+
+ reflections.reflect_on_property(property)
+ end
+
+ ##
+ # @private
def set_value(val)
resource = value_to_node(val.respond_to?(:resource) ? val.resource : val)
if resource.kind_of? RDFSource
node_cache[resource.rdf_subject] = nil
add_child_node(val, resource)
@@ -363,18 +507,28 @@
resource = resource.to_uri if resource.respond_to? :to_uri
raise ValueError, resource unless resource.kind_of? RDF::Term
parent.insert [rdf_subject, predicate, resource]
end
- def type_property
- { :predicate => RDF.type, :cast => false }
- end
-
+ ##
+ # @private
def value_to_node(val)
valid_datatype?(val) ? RDF::Literal(val) : val
end
+ def prepare_relation(values)
+ values.objects.map do |value|
+ if value.respond_to?(:resource?) && value.resource?
+ values.convert_object(value)
+ else
+ value
+ end
+ end
+ end
+
+ ##
+ # @private
def add_child_node(object, resource)
parent.insert [rdf_subject, predicate, resource.rdf_subject]
resource = resource.respond_to?(:resource) ? resource.resource : resource
new_resource = resource.dup unless object.respond_to?(:resource) && object.resource == resource
@@ -382,65 +536,55 @@
unless new_resource == parent ||
(parent.persistence_strategy.is_a?(ParentStrategy) &&
parent.persistence_strategy.ancestors.find { |a| a == new_resource })
new_resource.set_persistence_strategy(ParentStrategy)
new_resource.parent = parent
+ new_resource.persist!
end
self.node_cache[resource.rdf_subject] = (resource == object ? new_resource : object)
- new_resource.persist! if new_resource.persistence_strategy.is_a? ParentStrategy
end
+ ##
+ # @private
def valid_datatype?(val)
case val
when String, Date, Time, Numeric, Symbol, TrueClass, FalseClass then true
else false
end
end
- # Converts an object to the appropriate class.
- def convert_object(value)
- case value
- when RDFSource
- value
- when RDF::Literal
- return_literals? ? value : value.object
- when RDF::Resource
- make_node(value)
- else
- value
- end
- end
-
##
# Build a child resource or return it from this object's cache
#
# Builds the resource from the class_name specified for the
# property.
+ #
+ # @private
def make_node(value)
return value unless cast?
klass = class_for_value(value)
value = RDF::Node.new if value.nil?
node = node_cache[value] if node_cache[value]
node ||= klass.from_uri(value,parent)
- node.set_persistence_strategy(property_config[:persist_to]) if
+ node.set_persistence_strategy(property_config[:persist_to]) if
is_property? && property_config[:persist_to]
return nil if (is_property? && property_config[:class_name]) && (class_for_value(value) != class_for_property)
self.node_cache[value] ||= node
node
end
+ ##
+ # @private
def cast?
return true unless is_property? || (rel_args && rel_args[:cast])
return rel_args[:cast] if rel_args.has_key?(:cast)
!!property_config[:cast]
end
- def return_literals?
- rel_args && rel_args[:literal]
- end
-
+ ##
+ # @private
def final_parent
@final_parent ||= begin
parent = self.parent
while parent != parent.parent && parent.parent
parent = parent.parent
@@ -448,28 +592,35 @@
return parent.datastream if parent.respond_to?(:datastream) && parent.datastream
parent
end
end
+ ##
+ # @private
def class_for_value(v)
uri_class(v) || class_for_property
end
+ ##
+ # @private
def uri_class(v)
- v = RDF::URI.new(v) if v.kind_of? String
+ v = RDF::URI.intern(v) if v.kind_of? String
type_uri = parent.query([v, RDF.type, nil]).to_a.first.try(:object)
Resource.type_registry[type_uri]
end
+ ##
+ # @private
def class_for_property
klass = property_config[:class_name] if is_property?
klass ||= Resource
klass = ActiveTriples.class_from_string(klass, final_parent.class) if
klass.kind_of? String
klass
end
##
+ # @private
# @return [RDF::Term] the subject of the relation
def rdf_subject
if value_arguments.length < 1 || value_arguments.length > 2
raise(ArgumentError,
"wrong number of arguments (#{value_arguments.length} for 1-2)")