Skip to content

Commit

Permalink
Rework literal handling in Relation
Browse files Browse the repository at this point in the history
Reworks the default handling of `Relation` literal values:

  - Plain `xsd:String` literals are Ruby Strings;
  - `rdf:langString` literals are returned as `RDF::Literal` objects;
  - Known datatypes are cast to

When setting values from a Relation, always uses the `Literal`
value (through `Relation#objects`) to avoid losing data. URIs and blank
nods are still cast to their cached `RDFSource` versions.

Removes `literal: true` as a relation option.

Fixes the bug in #245 causing literal languges to be lost when setting
values to an existing `Relation` (including self).
  • Loading branch information
Tom Johnson committed Aug 19, 2016
1 parent 5fad51d commit e6d6f69
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 89 deletions.
98 changes: 57 additions & 41 deletions lib/active_triples/relation.rb
Expand Up @@ -137,8 +137,8 @@ def <=>(other)
#
# @return [Relation] a relation containing the set values; i.e. `self`
def <<(values)
values = to_a | Array.wrap(values)
self.set(values)
values = prepare_relation(values) if values.is_a?(Relation)
self.set(objects.to_a | Array.wrap(values))
end
alias_method :push, :<<

Expand Down Expand Up @@ -268,40 +268,43 @@ def delete?(value)
##
# Gives a result set for the `Relation`.
#
# By default, `RDF::URI` and `RDF::Node` results are cast to `RDFSource`.
# `Literal` results are given as their `#object` representations (e.g.
# `String`, `Date`.
# 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...>]
#
# 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.
#
# @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)>]
#
# @example results with `return_literals?` set to `true`
# relation.to_a
# # => [#<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...>]
#
# @return [Enumerator<Object>] the result set
def each
return [].to_enum if predicate.nil?
Expand Down Expand Up @@ -380,7 +383,8 @@ def property
# 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 = prepare_relation(values) if values.is_a?(Relation)
values = [values].compact unless values.kind_of?(Array)

clear
Expand Down Expand Up @@ -428,6 +432,31 @@ def swap(swap_out, swap_in)

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
Expand Down Expand Up @@ -486,6 +515,16 @@ 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)
Expand Down Expand Up @@ -514,23 +553,6 @@ def valid_datatype?(val)
end
end

##
# Converts an object to the appropriate class.
#
# @private
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
#
Expand Down Expand Up @@ -559,12 +581,6 @@ def cast?
!!property_config[:cast]
end

##
# @private
def return_literals?
rel_args && rel_args[:literal]
end

##
# @private
def final_parent
Expand Down
9 changes: 9 additions & 0 deletions spec/active_triples/identifiable_spec.rb
Expand Up @@ -168,6 +168,15 @@ class ActiveExampleTwo

expect(resource.relation).to eq [subject]
end
it "can share that object with another resource" do
resource = MyResource.new
resource_2 = MyResource.new

resource.relation = subject
resource_2.relation = resource.relation

expect(resource.relation).to eq resource_2.relation
end
end
end
end
36 changes: 21 additions & 15 deletions spec/active_triples/rdf_source_spec.rb
Expand Up @@ -341,15 +341,6 @@ class SourceWithCreator
expect(subject.get_values(other_uri, predicate))
.to be_a_relation_containing(val)
end

context 'when calling getter' do
it 'passes arguments through' do
subject.creator = 'moomin'

expect(subject.creator(literal: true).first)
.to eq RDF::Literal('moomin')
end
end
end

describe '#set_value' do
Expand Down Expand Up @@ -529,29 +520,44 @@ class SourceWithCreator
end

describe 'capturing child nodes' do
let(:other) { source_class.new }
let(:other) { source_class.new }
let(:predicate) { RDF::OWL.sameAs }

it 'adds child node data to own graph' do
other << RDF::Statement(:s, RDF::URI('p'), 'o')

expect { subject.set_value(RDF::OWL.sameAs, other) }
expect { subject.set_value(predicate, other) }
.to change { subject.statements.to_a }
.to include(*other.statements.to_a)
end

it 'does not change persistence strategy of added node' do
expect { subject.set_value(RDF::OWL.sameAs, other) }
expect { subject.set_value(predicate, other) }
.not_to change { other.persistence_strategy }
end

it 'does not capture a child node when it already persists to a parent' do
third = source_class.new
third.set_value(RDF::OWL.sameAs, other)
third.set_value(predicate, other)

child_other = third.get_values(RDF::OWL.sameAs).first
expect { subject.set_value(RDF::OWL.sameAs, child_other) }
child_other = third.get_values(predicate).first
expect { subject.set_value(predicate, child_other) }
.not_to change { child_other.persistence_strategy.parent }
end

context 'when setting to a relation' do
it 'adds child node data to graph' do
other << RDF::Statement(other, RDF::URI('p'), 'o')

relation_source = source_class.new
relation_source.set_value(predicate, other)
relation = relation_source.get_values(predicate)

expect { subject.set_value(predicate, relation) }
.to change { subject.statements.to_a }
.to include(*other.statements.to_a)
end
end
end
end

Expand Down

0 comments on commit e6d6f69

Please sign in to comment.