From e6d6f694746a1f2a450fd32e9bfebe3028c73e3f Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Fri, 19 Aug 2016 09:00:42 -0700 Subject: [PATCH] Rework literal handling in `Relation` 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). --- lib/active_triples/relation.rb | 98 ++++++++++-------- spec/active_triples/identifiable_spec.rb | 9 ++ spec/active_triples/rdf_source_spec.rb | 36 ++++--- spec/active_triples/relation_spec.rb | 123 +++++++++++++++++++---- spec/active_triples/resource_spec.rb | 17 +--- 5 files changed, 194 insertions(+), 89 deletions(-) diff --git a/lib/active_triples/relation.rb b/lib/active_triples/relation.rb index 6e1de7f..0e9fe71 100644 --- a/lib/active_triples/relation.rb +++ b/lib/active_triples/relation.rb @@ -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, :<< @@ -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, # # #, # # #] # - # 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, # # #, # # #] # - # @example results with `return_literals?` set to `true` - # relation.to_a - # # => [#, - # # #)>, - # # #, - # # #] - # # @return [Enumerator] the result set def each return [].to_enum if predicate.nil? @@ -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 @@ -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 @@ -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) @@ -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 # @@ -559,12 +581,6 @@ def cast? !!property_config[:cast] end - ## - # @private - def return_literals? - rel_args && rel_args[:literal] - end - ## # @private def final_parent diff --git a/spec/active_triples/identifiable_spec.rb b/spec/active_triples/identifiable_spec.rb index 9138734..b2f2943 100644 --- a/spec/active_triples/identifiable_spec.rb +++ b/spec/active_triples/identifiable_spec.rb @@ -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 diff --git a/spec/active_triples/rdf_source_spec.rb b/spec/active_triples/rdf_source_spec.rb index 4269b79..a96af04 100644 --- a/spec/active_triples/rdf_source_spec.rb +++ b/spec/active_triples/rdf_source_spec.rb @@ -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 @@ -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 diff --git a/spec/active_triples/relation_spec.rb b/spec/active_triples/relation_spec.rb index 3723252..c4e904e 100644 --- a/spec/active_triples/relation_spec.rb +++ b/spec/active_triples/relation_spec.rb @@ -128,14 +128,14 @@ it 'handles literal equality' do literal = RDF::Literal('mummi') lang_literal = RDF::Literal('mummi', language: :fi) - + subject << [1, literal] other << [2, lang_literal] - + expect(subject & other).to be_empty - + subject << [1, lang_literal] - expect(subject & other).to contain_exactly 'mummi' + expect(subject & other).to contain_exactly lang_literal end end end @@ -146,7 +146,7 @@ let(:parent_resource) { ActiveTriples::Resource.new } include_context 'with other relation' - + it 'handles node equality' do node = RDF::Node.new @@ -159,11 +159,11 @@ it 'handles literal equality' do literal = RDF::Literal('mummi') lang_literal = RDF::Literal('mummi', language: :fi) - + subject << [1, literal] other << [2, lang_literal] - expect(subject | other).to contain_exactly('mummi', 'mummi', 1, 2) + expect(subject | other).to contain_exactly('mummi', lang_literal, 1, 2) end end end @@ -174,9 +174,9 @@ let(:parent_resource) { ActiveTriples::Resource.new } include_context 'with other relation' - + it 'still implements as ' do - subject << [RDF::Node.new, RDF::Node.new, + subject << [RDF::Node.new, RDF::Node.new, RDF::Literal('mummi'), RDF::Literal('mummi', language: :fi)] other << [RDF::Node.new, RDF::Node.new, RDF::Literal('mummi'), RDF::Literal('mummi', language: :fi)] @@ -473,6 +473,71 @@ class WithTitle expect { subject << values } .to change { subject.to_a }.to contain_exactly(*values) end + + it 'keeps datatypes' do + values = [RDF::Literal(Date.today), RDF::Literal(:moomin)] + + expect { values.each { |v| subject << v } } + .to change { subject.send(:objects).to_a } + .to contain_exactly(*values) + end + + it 'keeps languages' do + values = [RDF::Literal("Moomin", language: :en), + RDF::Literal("Mummi", language: :fi)] + + expect { values.each { |v| subject << v } } + .to change { subject.send(:objects).to_a } + .to contain_exactly(*values) + end + + context 'when given a Relation' do + it 'keeps datatypes and languages of values' do + values = [RDF::Literal(Date.today), + RDF::Literal(:moomin), + RDF::Literal("Moomin", language: :en), + RDF::Literal("Mummi", language: :fi)] + + subject.set(values) + expect(subject.send(:objects)).to contain_exactly(*values) + + expect { subject << subject } + .not_to change { subject.send(:objects).to_a } + end + + it 'retains unknown datatypes' do + literal = + RDF::Literal('snowflake', + datatype: RDF::URI('http://emaple.com/snowflake')) + + subject << literal + + expect { subject << 'snowflake' } + .to change { subject.to_a } + .to contain_exactly(literal, 'snowflake') + + end + + context 'with a datatyped literal' do + before do + class DummySnowflake < RDF::Literal + DATATYPE = RDF::URI('http://example.com/snowflake').freeze + end + end + + after { Object.send(:remove_const, :DummySnowflake) } + + it 'retains datatypes' do + literal = DummySnowflake.new('special') + + subject << literal + + expect { subject << 'special' } + .to change { subject.send(:objects).to_a } + .to contain_exactly(literal, RDF::Literal('special')) + end + end + end end describe '#predicate' do @@ -603,18 +668,6 @@ class WithTitle expect(subject.each).to contain_exactly(*values) end end - - context 'when #return_literals? is true' do - let(:values) do - [RDF::Literal('moomin'), RDF::Literal(Date.today)] - end - - it 'does not cast results' do - allow(subject).to receive(:return_literals?).and_return(true) - - expect(subject.each).to contain_exactly(*values) - end - end end end end @@ -714,6 +767,34 @@ class WithTitle .to change { subject.to_a }.to contain_exactly(*values) end + context 'when given a Relation' do + before do + class DummySnowflake < RDF::Literal + DATATYPE = RDF::URI('http://example.com/snowflake').freeze + end + end + + after { Object.send(:remove_const, :DummySnowflake) } + + it 'keeps datatypes and languages of values' do + values = [Date.today, + 'Moomin', + :moomin, + RDF::Literal("Moomin", language: :en), + RDF::Literal("Mummi", language: :fi), + RDF::Literal("Moomin", datatype: RDF::URI('custom')), + DummySnowflake.new('Moomin')] + + subject.set(values) + + values[6] = 'Moomin' # cast known datatypes + expect(subject.to_a).to contain_exactly(*values) + + expect { subject.set(subject) } + .not_to change { subject.send(:objects).to_a } + end + end + context 'and persistence config' do before do reflections diff --git a/spec/active_triples/resource_spec.rb b/spec/active_triples/resource_spec.rb index 744e42e..beb4e54 100644 --- a/spec/active_triples/resource_spec.rb +++ b/spec/active_triples/resource_spec.rb @@ -571,21 +571,14 @@ class DummyResourceWithBaseURI < ActiveTriples::Resource context "literals are set" do let(:literal1) { RDF::Literal.new("test", :language => :en) } let(:literal2) { RDF::Literal.new("test", :language => :fr) } + before do subject.set_value(RDF::Vocab::DC.title, [literal1, literal2]) end - context "and literals are not requested" do - it "should return a string" do - # Should this de-duplicate? - expect(subject.get_values(RDF::Vocab::DC.title)) - .to contain_exactly "test", "test" - end - end - context "and literals are requested" do - it "should return literals" do - expect(subject.get_values(RDF::Vocab::DC.title, :literal => true)) - .to contain_exactly literal1, literal2 - end + + it "should return literals" do + expect(subject.get_values(RDF::Vocab::DC.title, :literal => true)) + .to contain_exactly literal1, literal2 end end end