Class: ActiveTriples::Relation

Inherits:
Object
  • Object
show all
Includes:
Comparable, Enumerable
Defined in:
lib/active_triples/relation.rb

Overview

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 Also:

  • RDF::Term

Defined Under Namespace

Classes: ValueError

Constant Summary collapse

TYPE_PROPERTY =
{ predicate: RDF.type, cast: false }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent_source, value_arguments) ⇒ Relation

Returns a new instance of Relation.

Parameters:

  • parent_source (ActiveTriples::RDFSource)
  • value_arguments (Array<Symbol, Hash>)

    if a Hash is passed as the final element, it is removed and set to ‘@rel_args`.



43
44
45
46
47
48
49
50
51
# 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

Instance Attribute Details

#parentRDFSource

Returns the resource that is the domain of this relation.

Returns:

  • (RDFSource)

    the resource that is the domain of this relation



34
35
36
# File 'lib/active_triples/relation.rb', line 34

def parent
  @parent
end

#reflectionsClass (readonly)

Returns:

  • (Class)


34
# File 'lib/active_triples/relation.rb', line 34

attr_accessor :parent, :value_arguments

#rel_argsHash (readonly)

Returns:

  • (Hash)


34
# File 'lib/active_triples/relation.rb', line 34

attr_accessor :parent, :value_arguments

#value_argumentsArray<Object>

Returns:

  • (Array<Object>)


34
# File 'lib/active_triples/relation.rb', line 34

attr_accessor :parent, :value_arguments

Instance Method Details

#&(array) ⇒ Array

Note:

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

Parameters:

Returns:

  • (Array)

See Also:

  • Array#&


60
61
62
63
64
65
# 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) ⇒ Array

Note:

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

Parameters:

Returns:

  • (Array)

See Also:

  • Array#+


88
89
90
91
92
93
# 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) ⇒ Relation Also known as: push

Adds values to the result set

Parameters:

  • values (Object, Array<Object>)

    values to add

Returns:

  • (Relation)

    a relation containing the set values; i.e. ‘self`



142
143
144
145
# 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

#<=>(other) ⇒ Object

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

Parameters:

  • other (Object)

See Also:

  • Set#<=>


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# 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 = {}) ⇒ Object

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

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.

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

Examples:

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"

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"

Parameters:

  • attributes (Hash) (defaults to: {})

    a hash of attribute names and values for the built node.

See Also:



193
194
195
196
197
198
199
200
201
202
203
204
205
206
# 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

#clearRelation

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

Returns:

  • (Relation)

    self; a now empty relation



212
213
214
215
216
217
218
# 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) ⇒ ActiveTriples::Relation

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

Returns self.

Examples:

deleting a value

resource = MySource.new
resource.title = ['moomin', 'valley']
resource.title.delete('moomin') # => ["valley"]
resource.title # => ['valley']

note the behavior of unmatched values

resource = MySource.new
resource.title = 'moomin'
resource.title.delete('valley') # => ["moomin"]
resource.title # => ['moomin']

Parameters:

  • value (Object)

    the value to delete from the relation

Returns:



248
249
250
251
252
253
254
255
256
# 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) ⇒ Object?

A variation on ‘#delete`. This queries the relation for matching values before running the deletion, returning `nil` if it does not exist.

Parameters:

  • value (Object)

    the value to delete from the relation

Returns:

  • (Object, nil)

    ‘nil` if the value doesn’t exist; the value otherwise

See Also:



267
268
269
270
271
272
273
274
# 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

#eachEnumerator<Object>

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.

Examples:

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...>]

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)>]

Returns:

  • (Enumerator<Object>)

    the result set



317
318
319
320
321
322
323
324
325
326
327
328
# 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?Boolean

Returns true if the results are empty.

Returns:

  • (Boolean)

    true if the results are empty.



332
333
334
# File 'lib/active_triples/relation.rb', line 332

def empty?
  objects.empty?
end

#first_or_create(attributes = {}) ⇒ Object

Deprecated.

for removal in 1.0.0. Use ‘first || build({})`, `build({}) if empty?` or similar logic.

Returns the first result, if present; else a newly built node.

Returns:

  • (Object)

    the first result, if present; else a newly built node

See Also:



343
344
345
346
# 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

#lengthInteger

Returns:

  • (Integer)


350
351
352
# File 'lib/active_triples/relation.rb', line 350

def length
  objects.to_a.length
end

#predicateRDF::Term?

Gives the predicate used by the Relation. Values of this object are those that match the pattern ‘<rdf_subject> <predicate> [value] .`

Returns:

  • (RDF::Term, nil)

    the predicate for this relation; nil if no predicate can be found

See Also:



362
363
364
365
# 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

#propertySymbol, RDF::URI

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

Returns:

  • (Symbol, RDF::URI)

    the property for this Relation.

See Also:



373
374
375
# File 'lib/active_triples/relation.rb', line 373

def property
  value_arguments.last
end

#set(values) ⇒ Relation

Set the values of the Relation

Parameters:

  • values (Array<RDF::Resource>, RDF::Resource)

    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`

Returns:

  • (Relation)

    a relation containing the set values; i.e. ‘self`

Raises:

See Also:



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# 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(enum) ⇒ Relation #subtract(*values) ⇒ Relation

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

Returns self.

Overloads:

  • #subtract(enum) ⇒ Relation

    Deletes objects in the enumerable from the relation

    Parameters:

    • values (Enumerable)

      an enumerable of objects to delete

  • #subtract(*values) ⇒ Relation

    Deletes each argument value from the relation

    Parameters:

    • *values (Array<Object>)

      the objects to delete

Returns:

See Also:



424
425
426
427
428
429
430
431
432
433
434
# 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) ⇒ Relation

Replaces the first argument with the second as a value within the relation.

Parameters:

  • swap_out (Object)

    the value to delete

  • swap_in (Object)

    the replacement value

Returns:



444
445
446
# File 'lib/active_triples/relation.rb', line 444

def swap(swap_out, swap_in)
  self.<<(swap_in) if delete?(swap_out)
end

#|(array) ⇒ Array

Note:

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

Parameters:

Returns:

  • (Array)

See Also:

  • Array#|


74
75
76
77
78
79
# 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