class ActiveTriples::Relation

A ‘Relation` represents the values of a specific property/predicate on an `RDFSource`. Each relation is a set (`Enumerable` of the `RDF::Term`s that are objects in the of source’s triples of the form:

<{#parent}> <{#predicate}> [term] .

Relations express a binary relationships (over a predicate) between the parent node and a set of terms.

When the term is a URI or Blank Node, it is represented in the results as an ‘RDFSource`. Literal values are cast to strings, Ruby native types, or remain as an `RDF::Literal` as documented in `#each`.

@see RDF::Term

Constants

TYPE_PROPERTY

Attributes

parent[RW]

@!attribute [rw] parent

@return [RDFSource] the resource that is the domain of this relation

@!attribute [rw] value_arguments

@return [Array<Object>]

@!attribute [r] rel_args

@return [Hash]

@!attribute [r] reflections

@return [Class]
reflections[R]
rel_args[R]
value_arguments[RW]

@!attribute [rw] parent

@return [RDFSource] the resource that is the domain of this relation

@!attribute [rw] value_arguments

@return [Array<Object>]

@!attribute [r] rel_args

@return [Hash]

@!attribute [r] reflections

@return [Class]

Public Class Methods

new(parent_source, value_arguments) click to toggle source

@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`.
# File lib/active_triples/relation.rb, line 43
def initialize(parent_source, value_arguments)
  self.parent = parent_source
  @reflections = parent_source.reflections
  @rel_args ||= {}
  @rel_args = value_arguments.pop if
    value_arguments.is_a?(Array) && value_arguments.last.is_a?(Hash)

  @value_arguments = value_arguments
end

Public Instance Methods

&(array) click to toggle source

@param array [#to_ary, ActiveTriples::Relation] @return [Array]

@note simply passes to ‘Array#&` unless argument is a `Relation`

@see Array#&

# File lib/active_triples/relation.rb, line 60
def &(array)
  return to_a & array unless array.is_a? Relation

  (objects.to_a & array.objects.to_a)
    .map { |object| convert_object(object) }
end
+(array) click to toggle source

@param array [#to_ary, ActiveTriples::Relation] @return [Array]

@note simply passes to ‘Array#+` unless argument is a `Relation`

@see Array#+

# File lib/active_triples/relation.rb, line 88
def +(array)
  return to_a + array unless array.is_a? Relation

  (objects.to_a + array.objects.to_a)
    .map { |object| convert_object(object) }
end
<<(values) click to toggle source

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`

# File lib/active_triples/relation.rb, line 142
def <<(values)
  values = prepare_relation(values) if values.is_a?(Relation)
  self.set(objects.to_a | Array.wrap(values))
end
Also aliased as: push
<=>(other) click to toggle source

Mimics ‘Set#<=>`, returning `0` when set membership is equivalent, and `nil` (as non-comparable) otherwise. Unlike `Set#<=>`, uses `#==` for member comparisons.

@param [Object] other

@see Set#<=>

# File lib/active_triples/relation.rb, line 103
def <=>(other)
  return nil unless other.respond_to?(:each)

  # If we're empty, avoid calling `#to_a` on other.
  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
  other_length = other.length
  this         = each

  loop do
    begin
      current = this.next
    rescue StopIteration
      # If we die, we are equal to other so far, check length and walk away.
      return other_length == length ? 0 : nil
    end

    length += 1
    
    # Return as not comparable if we have seen more terms than are in other,
    # or if other does not include the current term.
    return nil if other_length < length || !other.include?(current)
  end
end
build(attributes={}) click to toggle source

Builds a node with the given attributes, adding it to the relation.

@param attributes [Hash] a hash of attribute names and values for the

built node.

@example building an empty generic node

resource = ActiveTriples::Resource.new
resource.resource.get_values(RDF::Vocab::DC.relation).build
# => #<ActiveTriples::Resource:0x2b0(#<ActiveTriples::Resource:0x005>)>)

resource.dump :ttl
# => "\n [ <http://purl.org/dc/terms/relation> []] .\n"

Nodes are built using the configured ‘class_name` for the relation. Attributes passed in the Hash argument are set on the new node through `RDFSource#attributes=`. If the attribute keys are not valid properties on the built node, we raise an error.

@example building a node with attributes

class WithRelation
  include ActiveTriples::RDFSource
  property :relation, predicate:  RDF::Vocab::DC.relation,
    class_name: 'WithTitle'
end

class WithTitle
  include ActiveTriples::RDFSource
  property :title, predicate: RDF::Vocab::DC.title
end

resource = WithRelation.new
attributes = { id: 'http://ex.org/moomin', title: 'moomin' }

resource.get_values(:relation).build(attributes)
# => #<ActiveTriples::Resource:0x2b0(#<ActiveTriples::Resource:0x005>)>)

resource.dump :ttl
# => "\n<http://ex.org/moomin> <http://purl.org/dc/terms/title> \"moomin\" .\n\n [ <http://purl.org/dc/terms/relation> <http://ex.org/moomin>] .\n"

@todo: clarify class behavior; it is actually tied to type, in some cases.

@see RDFSource#attributes= @see guides.rubyonrails.org/active_model_basics.html for some

context on ActiveModel attributes.
# File lib/active_triples/relation.rb, line 193
def build(attributes={})
  new_subject = attributes.fetch('id') { RDF::Node.new }

  make_node(new_subject).tap do |node|
    node.attributes = attributes.except('id')
    if parent.kind_of? List::ListResource
      parent.list << node
    elsif node.kind_of? RDF::List
      self.push node.rdf_subject
    else
      self.push node
    end
  end
end
clear() click to toggle source

Empties the ‘Relation`, deleting any associated triples from `parent`.

@return [Relation] self; a now empty relation

# File lib/active_triples/relation.rb, line 212
def clear
  return self if empty?
  parent.delete([rdf_subject, predicate, nil])
  parent.notify_observers(property)

  self
end
delete(value) click to toggle source

@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
`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

`RDF::Mutable#delete`, but may also represent tokens in statements.
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']
resource.title.delete('moomin') # => ["valley"]
resource.title # => ['valley']

@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

# File lib/active_triples/relation.rb, line 248
def delete(value)
  value = RDF::Literal(value) if value.is_a? Symbol

  return self if parent.query([rdf_subject, predicate, value]).nil?

  parent.delete([rdf_subject, predicate, value])
  parent.notify_observers(property)
  self
end
delete?(value) click to toggle source

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

otherwise

@see delete

# File lib/active_triples/relation.rb, line 267
def delete?(value)
  value = RDF::Literal(value) if value.is_a? Symbol

  return nil if parent.query([rdf_subject, predicate, value]).nil?

  delete(value)
  value
end
each() { |converted_object| ... } click to toggle source

Gives a result set for the ‘Relation`.

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.

‘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

# File lib/active_triples/relation.rb, line 317
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

  to_enum
end
empty?() click to toggle source

@return [Boolean] true if the results are empty.

# File lib/active_triples/relation.rb, line 332
def empty?
  objects.empty?
end
first_or_create(attributes={}) click to toggle source

@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

# File lib/active_triples/relation.rb, line 343
def first_or_create(attributes={})
  warn 'DEPRECATION: #first_or_create is deprecated for removal in 1.0.0.'
  first || build(attributes)
end
length() click to toggle source

@return [Integer]

# File lib/active_triples/relation.rb, line 350
def length
  objects.to_a.length
end
predicate() click to toggle source

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

# File lib/active_triples/relation.rb, line 362
def predicate
  return property if property.is_a?(RDF::Term)
  property_config[:predicate] if is_property?
end
property() click to toggle source

Returns the property for the Relation. This may be a registered property key or an {RDF::URI}.

@return [Symbol, RDF::URI] the property for this Relation. @see predicate

# File lib/active_triples/relation.rb, line 373
def property
  value_arguments.last
end
push(values)
Alias for: <<
set(values) click to toggle source

Set the values of the Relation

@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`

@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 www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For

documentation on `RDF::Statement` and the handling of
non-`RDF::Resource` values.
# File lib/active_triples/relation.rb, line 392
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.notify_observers(property)
  
  parent.persist! if parent.persistence_strategy.respond_to?(:ancestors) &&
                     parent.persistence_strategy.ancestors.any? { |r| r.is_a?(ActiveTriples::List::ListResource) }

  self
end
subtract(*values) click to toggle source

@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)`.

@note This method treats all calls as changes for the purpose of observer

notifications

@see delete

# File lib/active_triples/relation.rb, line 424
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)
  parent.notify_observers(property)
  self
end
swap(swap_out, swap_in) click to toggle source

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

# File lib/active_triples/relation.rb, line 444
def swap(swap_out, swap_in)
  self.<<(swap_in) if delete?(swap_out)
end
|(array) click to toggle source

@param array [#to_ary, ActiveTriples::Relation] @return [Array]

@note simply passes to ‘Array#|` unless argument is a `Relation`

@see Array#|

# File lib/active_triples/relation.rb, line 74
def |(array)
  return to_a | array unless array.is_a? Relation
  
  (objects.to_a | array.objects.to_a)
    .map { |object| convert_object(object) }
end

Protected Instance Methods

convert_object(value) click to toggle source

Converts an object to the appropriate class.

Literals are cast only when the datatype is known.

@private

# File lib/active_triples/relation.rb, line 456
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
    cast? ? make_node(value) : value
  else
    value
  end
end
node_cache() click to toggle source

@private

# File lib/active_triples/relation.rb, line 477
def node_cache
  @node_cache ||= {}
end
objects(&block) click to toggle source

@private

# File lib/active_triples/relation.rb, line 483
def objects(&block)
  solutions = parent.query([rdf_subject, predicate, nil])
  solutions.extend(RDF::Enumerable) unless solutions.respond_to?(:each_object)
  
  solutions.each_object(&block)
end

Private Instance Methods

add_child_node(object, resource) click to toggle source

@private

# File lib/active_triples/relation.rb, line 545
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
  new_resource ||= resource
  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
  end

  self.node_cache[resource.rdf_subject] = (resource == object ? new_resource : object)
end
cast?() click to toggle source

@private

# File lib/active_triples/relation.rb, line 597
def cast?
  return true unless is_property? || rel_args[:cast]
  return rel_args[:cast] if rel_args.has_key?(:cast)
  !!property_config[:cast]
end
class_for_property() click to toggle source

@private

# File lib/active_triples/relation.rb, line 632
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
class_for_value(v) click to toggle source

@private

# File lib/active_triples/relation.rb, line 618
def class_for_value(v)
  uri_class(v) || class_for_property
end
final_parent() click to toggle source

@private

# File lib/active_triples/relation.rb, line 605
def final_parent
  @final_parent ||= begin
    parent = self.parent
    while parent != parent.parent && parent.parent
      parent = parent.parent
    end
    return parent.datastream if parent.respond_to?(:datastream) && parent.datastream
    parent
  end
end
is_property?() click to toggle source

@private

# File lib/active_triples/relation.rb, line 493
def is_property?
  reflections.has_property?(property) || is_type?
end
is_type?() click to toggle source

@private

# File lib/active_triples/relation.rb, line 499
def is_type?
  (property == RDF.type || property.to_s == "type") &&
  (!reflections.kind_of?(RDFSource) || !is_property?)
end
make_node(value) click to toggle source

Build a child resource or return it from this object’s cache

Builds the resource from the class_name specified for the property.

@private

# File lib/active_triples/relation.rb, line 577
def make_node(value)
  klass = class_for_value(value)
  value = RDF::Node.new if value.nil?

  return node_cache[value] if node_cache[value]

  node = klass.from_uri(value, parent)

  if is_property? && property_config[:persist_to]
    node.set_persistence_strategy(property_config[:persist_to])

    node.persistence_strategy.parent = parent if
      node.persistence_strategy.is_a?(ParentStrategy)
  end

  self.node_cache[value] ||= node
end
prepare_relation(values) click to toggle source
# File lib/active_triples/relation.rb, line 533
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
property_config() click to toggle source

@private @return [Hash<Symbol, ]

# File lib/active_triples/relation.rb, line 507
def property_config
  return TYPE_PROPERTY if is_type?

  reflections.reflect_on_property(property)
end
rdf_subject() click to toggle source

@private @return [RDF::Term] the subject of the relation

# File lib/active_triples/relation.rb, line 643
def rdf_subject
  if value_arguments.length < 1 || value_arguments.length > 2
    raise(ArgumentError,
          "wrong number of arguments (#{value_arguments.length} for 1-2)")
  end

  value_arguments.length > 1 ? value_arguments.first : parent.rdf_subject
end
set_value(val) click to toggle source

@private

# File lib/active_triples/relation.rb, line 515
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)
    return
  end
  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
uri_class(v) click to toggle source

@private

# File lib/active_triples/relation.rb, line 624
def uri_class(v)
  v = RDF::URI.intern(v) if v.kind_of? String
  type_uri = parent.query([v, RDF.type, nil]).to_a.first.try(:object)
  RDFSource.type_registry[type_uri]
end
valid_datatype?(val) click to toggle source

@private

# File lib/active_triples/relation.rb, line 563
def valid_datatype?(val)
  case val
  when String, Date, Time, Numeric, Symbol, TrueClass, FalseClass then true
  else false
  end
end
value_to_node(val) click to toggle source

@private

# File lib/active_triples/relation.rb, line 529
def value_to_node(val)
  valid_datatype?(val) ? RDF::Literal(val) : val
end