active-triples 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/CHANGES.md +17 -11
  4. data/README.md +72 -39
  5. data/lib/active_triples/configurable.rb +6 -1
  6. data/lib/active_triples/list.rb +1 -4
  7. data/lib/active_triples/nested_attributes.rb +10 -7
  8. data/lib/active_triples/persistable.rb +13 -0
  9. data/lib/active_triples/persistence_strategies/parent_strategy.rb +47 -34
  10. data/lib/active_triples/persistence_strategies/persistence_strategy.rb +14 -1
  11. data/lib/active_triples/properties.rb +19 -4
  12. data/lib/active_triples/property_builder.rb +4 -4
  13. data/lib/active_triples/rdf_source.rb +142 -189
  14. data/lib/active_triples/relation.rb +307 -156
  15. data/lib/active_triples/util/buffered_transaction.rb +126 -0
  16. data/lib/active_triples/util/extended_bounded_description.rb +75 -0
  17. data/lib/active_triples/version.rb +1 -1
  18. data/spec/active_triples/configurable_spec.rb +35 -7
  19. data/spec/active_triples/identifiable_spec.rb +19 -6
  20. data/spec/active_triples/list_spec.rb +15 -7
  21. data/spec/active_triples/nested_attributes_spec.rb +12 -10
  22. data/spec/active_triples/persistable_spec.rb +0 -4
  23. data/spec/active_triples/persistence_strategies/parent_strategy_spec.rb +57 -10
  24. data/spec/active_triples/rdf_source_spec.rb +137 -97
  25. data/spec/active_triples/relation_spec.rb +436 -132
  26. data/spec/active_triples/resource_spec.rb +8 -23
  27. data/spec/active_triples/util/buffered_transaction_spec.rb +187 -0
  28. data/spec/active_triples/util/extended_bounded_description_spec.rb +98 -0
  29. data/spec/integration/reciprocal_properties_spec.rb +10 -10
  30. data/spec/support/matchers.rb +13 -1
  31. metadata +7 -3
@@ -0,0 +1,126 @@
1
+ require 'active_triples/util/extended_bounded_description'
2
+
3
+ module ActiveTriples
4
+ ##
5
+ # A buffered trasaction for use with `ActiveTriples::ParentStrategy`.
6
+ #
7
+ # If an `ActiveTriples::RDFSource` instance is passed as the underlying
8
+ # repository, this transaction will try to find an existing
9
+ # `BufferedTransaction` to use as the basis for a snapshot. When the
10
+ # transaction is executed, the inserts and deletes are replayed against the
11
+ # `RDFSource`.
12
+ #
13
+ # If a `RDF::Transaction::TransactionError` is raised on commit, this
14
+ # transaction optimistically attempts to replay the changes.
15
+ #
16
+ # Reads are projected onto a specialized "Extended Bounded Description"
17
+ # subgraph.
18
+ #
19
+ # @see ActiveTriples::Util::ExtendedBoundedDescription
20
+ class BufferedTransaction <
21
+ RDF::Repository::Implementation::SerializedTransaction
22
+ # @!attribute snapshot [r]
23
+ # @return RDF::Dataset
24
+ # @!attribute subject [r]
25
+ # @return RDF::Term
26
+ # @!attribute ancestors [r]
27
+ # @return Array<RDF::Term>
28
+ attr_reader :snapshot, :subject, :ancestors
29
+
30
+ def initialize(repository,
31
+ ancestors: [],
32
+ subject: nil,
33
+ graph_name: nil,
34
+ mutable: false,
35
+ **options,
36
+ &block)
37
+ @subject = subject
38
+ @ancestors = ancestors
39
+
40
+ if repository.is_a?(RDFSource)
41
+ if repository.persistence_strategy.graph.is_a?(BufferedTransaction)
42
+ super
43
+ @snapshot = repository.persistence_strategy.graph.snapshot
44
+ return
45
+ else
46
+ repository = repository.persistence_strategy.graph.data
47
+ end
48
+ end
49
+
50
+ return super
51
+ end
52
+
53
+ ##
54
+ # Provides :repeatable_read isolation (???)
55
+ #
56
+ # @see RDF::Transaction#isolation_level
57
+ def isolation_level
58
+ :repeatable_read
59
+ end
60
+
61
+ ##
62
+ # @return [BufferedTransaction] self
63
+ def data
64
+ self
65
+ end
66
+
67
+ ##
68
+ # @see RDF::Mutable#supports
69
+ def supports?(feature)
70
+ return true if feature.to_sym == :snapshots
71
+ end
72
+
73
+ ##
74
+ # Adds statement to the `inserts` collection of the buffered changeset and
75
+ # updates the snapshot.
76
+ #
77
+ # @see RDF::Mutable#insert_statement
78
+ def insert_statement(statement)
79
+ @changes.insert(statement)
80
+ @changes.deletes.delete(statement)
81
+ super
82
+ end
83
+
84
+ ##
85
+ # Adds statement to the `deletes` collection of the buffered changeset and
86
+ # updates the snapshot.
87
+ #
88
+ # @see RDF::Transaction#delete_statement
89
+ def delete_statement(statement)
90
+ @changes.delete(statement)
91
+ @changes.inserts.delete(statement)
92
+ super
93
+ end
94
+
95
+ ##
96
+ # Executes optimistically. If errors are encountered, we replay the buffer
97
+ # on the latest version.
98
+ #
99
+ # If the `repository` is a transaction, we immediately replay the buffer
100
+ # onto it.
101
+ #
102
+ # @see RDF::Transaction#execute
103
+ def execute
104
+ raise TransactionError, 'Cannot execute a rolled back transaction. ' \
105
+ 'Open a new one instead.' if @rolledback
106
+ return if changes.empty?
107
+ return super unless repository.is_a?(ActiveTriples::RDFSource)
108
+
109
+ repository.insert(changes.inserts)
110
+ repository.delete(changes.deletes)
111
+ rescue RDF::Transaction::TransactionError => err
112
+ raise err if @rolledback
113
+
114
+ # replay changest on the current version of the repository
115
+ repository.delete(*changes.deletes)
116
+ repository.insert(*changes.inserts)
117
+ end
118
+
119
+ private
120
+
121
+ def read_target
122
+ return super unless subject
123
+ ExtendedBoundedDescription.new(super, subject, ancestors)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,75 @@
1
+ module ActiveTriples
2
+ ##
3
+ # Bounds the scope of an `RDF::Queryable` to a subgraph defined from a source
4
+ # graph, a starting node, and a list of "ancestors" terms, by the following
5
+ # process:
6
+ #
7
+ # Include in the subgraph:
8
+ # 1. All statements in the source graph where the subject of the statement
9
+ # is the starting node.
10
+ # 2. Add the starting node to the ancestors list.
11
+ # 2. Recursively, for all statements already in the subgraph, include in
12
+ # the subgraph the Extended Bounded Description for each object node,
13
+ # unless the object is in the ancestors list.
14
+ #
15
+ # The list of "ancestors" is empty by default.
16
+ #
17
+ # This subgraph this process yields can be considered as a description of
18
+ # the starting node.
19
+ #
20
+ # Compare to Concise Bounded Description
21
+ # (https://www.w3.org/Submission/CBD/), the common subgraph scope used for
22
+ # SPARQL DESCRIBE queries.
23
+ #
24
+ # @note this implementation requires that the `source_graph` remain unchanged
25
+ # while iterating over the description. The safest way to achive this is to
26
+ # use an immutable `RDF::Dataset` (e.g. a `Repository#snapshot`).
27
+ class ExtendedBoundedDescription
28
+ include RDF::Enumerable
29
+ include RDF::Queryable
30
+
31
+ ##
32
+ # @!attribute ancestors [r]
33
+ # @return Array<RDF::Term>
34
+ # @!attribute source_graph [r]
35
+ # @return RDF::Queryable
36
+ # @!attribute starting_node [r]
37
+ # @return RDF::Term
38
+ attr_reader :ancestors, :source_graph, :starting_node
39
+
40
+ ##
41
+ # By analogy to Concise Bounded Description.
42
+ #
43
+ # @param source_graph [RDF::Queryable]
44
+ # @param starting_node [RDF::Term]
45
+ # @param ancestors [Array<RDF::Term>] default: []
46
+ def initialize(source_graph, starting_node, ancestors = [])
47
+ @source_graph = source_graph
48
+ @starting_node = starting_node
49
+ @ancestors = ancestors
50
+ end
51
+
52
+ ##
53
+ # @see RDF::Enumerable#each
54
+ def each_statement
55
+ ancestors = @ancestors.dup
56
+
57
+ if block_given?
58
+ statements = source_graph.query(subject: starting_node).each
59
+ statements.each_statement { |st| yield st }
60
+
61
+ ancestors << starting_node
62
+
63
+ statements.each_object do |object|
64
+ next if object.literal? || ancestors.include?(object)
65
+ ExtendedBoundedDescription
66
+ .new(source_graph, object, ancestors).each do |statement|
67
+ yield statement
68
+ end
69
+ end
70
+ end
71
+ enum_statement
72
+ end
73
+ alias_method :each, :each_statement
74
+ end
75
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ActiveTriples
3
- VERSION = '0.10.2'.freeze
3
+ VERSION = '0.11.0'.freeze
4
4
  end
@@ -8,9 +8,7 @@ describe ActiveTriples::Configurable do
8
8
  end
9
9
  end
10
10
 
11
- after do
12
- Object.send(:remove_const, "DummyConfigurable")
13
- end
11
+ after { Object.send(:remove_const, "DummyConfigurable") }
14
12
 
15
13
  it "should be okay if not configured" do
16
14
  expect(DummyConfigurable.type).to eq nil
@@ -21,9 +19,32 @@ describe ActiveTriples::Configurable do
21
19
  expect(DummyConfigurable.type).to eq []
22
20
  end
23
21
 
22
+ describe 'configuration inheritance' do
23
+ before do
24
+ DummyConfigurable.configure type: type,
25
+ base_uri: base_uri,
26
+ rdf_label: rdf_label,
27
+ repository: repository
28
+ class ConfigurableSubclass < DummyConfigurable; end
29
+ end
30
+
31
+ let(:type) { RDF::Vocab::FOAF.Person }
32
+ let(:base_uri) { 'http://example.org/moomin' }
33
+ let(:rdf_label) { RDF::Vocab::DC.title }
34
+ let(:repository) { RDF::Repository.new }
35
+
36
+ after { Object.send(:remove_const, "ConfigurableSubclass") }
37
+
38
+ it 'inherits type from parent' do
39
+ expect(ConfigurableSubclass.type).to eq DummyConfigurable.type
40
+ end
41
+ end
42
+
24
43
  describe '#configure' do
25
44
  before do
26
- DummyConfigurable.configure base_uri: "http://example.org/base", type: RDF::RDFS.Class, rdf_label: RDF::Vocab::DC.title
45
+ DummyConfigurable.configure base_uri: "http://example.org/base",
46
+ type: RDF::RDFS.Class,
47
+ rdf_label: RDF::Vocab::DC.title
27
48
  end
28
49
 
29
50
  it 'should set a base uri' do
@@ -31,8 +52,13 @@ describe ActiveTriples::Configurable do
31
52
  end
32
53
 
33
54
  it "should be able to set multiple types" do
34
- DummyConfigurable.configure type: [RDF::RDFS.Container, RDF::RDFS.ContainerMembershipProperty]
35
- expect(DummyConfigurable.type).to contain_exactly(RDF::RDFS.Class, RDF::RDFS.Container, RDF::RDFS.ContainerMembershipProperty)
55
+ DummyConfigurable.configure type: [RDF::RDFS.Container,
56
+ RDF::RDFS.ContainerMembershipProperty]
57
+
58
+ expect(DummyConfigurable.type)
59
+ .to contain_exactly(RDF::RDFS.Class,
60
+ RDF::RDFS.Container,
61
+ RDF::RDFS.ContainerMembershipProperty)
36
62
  end
37
63
 
38
64
  it 'should set an rdf_label' do
@@ -45,7 +71,9 @@ describe ActiveTriples::Configurable do
45
71
 
46
72
  it "should be able to set multiple types" do
47
73
  DummyConfigurable.configure type: RDF::RDFS.Container
48
- expect(DummyConfigurable.type).to eq [RDF::RDFS.Class, RDF::RDFS.Container]
74
+
75
+ expect(DummyConfigurable.type)
76
+ .to eq [RDF::RDFS.Class, RDF::RDFS.Container]
49
77
  end
50
78
  end
51
79
  end
@@ -58,12 +58,6 @@ describe ActiveTriples::Identifiable do
58
58
  attr_accessor :id
59
59
  configure base_uri: 'http://example.org/ns/'
60
60
 
61
- def self.from_uri(uri, *args)
62
- item = self.new
63
- item.parent = args.first unless args.empty? or args.first.is_a?(Hash)
64
- item
65
- end
66
-
67
61
  def self.property(*args)
68
62
  prop = args.first
69
63
 
@@ -144,6 +138,16 @@ describe ActiveTriples::Identifiable do
144
138
  end
145
139
  end
146
140
 
141
+ describe '#parent=' do
142
+ before { class MyResource; include ActiveTriples::RDFSource; end }
143
+ let(:parent) { MyResource.new }
144
+
145
+ it 'sets parent' do
146
+ expect { subject.parent = parent }
147
+ .to change { subject.parent }.from(nil).to(parent)
148
+ end
149
+ end
150
+
147
151
  describe '#rdf_subject' do
148
152
  it 'has a subject' do
149
153
  expect(subject.rdf_subject).to eq 'http://example.org/ns/123'
@@ -164,6 +168,15 @@ describe ActiveTriples::Identifiable do
164
168
 
165
169
  expect(resource.relation).to eq [subject]
166
170
  end
171
+ it "can share that object with another resource" do
172
+ resource = MyResource.new
173
+ resource_2 = MyResource.new
174
+
175
+ resource.relation = subject
176
+ resource_2.relation = resource.relation
177
+
178
+ expect(resource.relation).to eq resource_2.relation
179
+ end
167
180
  end
168
181
  end
169
182
  end
@@ -230,7 +230,9 @@ END
230
230
  it "should have to_ary" do
231
231
  ary = list.to_ary
232
232
  expect(ary.size).to eq 4
233
- expect(ary[1].elementValue).to eq ['Relations with Mexican Americans']
233
+
234
+ expect(ary[1].elementValue)
235
+ .to contain_exactly 'Relations with Mexican Americans'
234
236
  end
235
237
 
236
238
  it "should have size" do
@@ -243,12 +245,18 @@ END
243
245
  list[3].elementValue = ["1900s"]
244
246
  doc = Nokogiri::XML(subject.dump :rdfxml)
245
247
  ns = {rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", mads: "http://www.loc.gov/mads/rdf/v1#"}
246
- expect(doc.xpath('/rdf:RDF/mads:ComplexSubject/@rdf:about', ns).map(&:value)).to eq ["http://example.org/foo"]
247
- expect(doc.xpath('//mads:ComplexSubject/mads:elementList/@rdf:parseType', ns).map(&:value)).to eq ["Collection"]
248
- expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 1]/@rdf:about', ns).map(&:value)).to eq ["http://library.ucsd.edu/ark:/20775/bbXXXXXXX6"]
249
- expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 2]/mads:elementValue', ns).map(&:text)).to eq ["Relations with Mexican Americans"]
250
- expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 3]/@rdf:about', ns).map(&:value)).to eq ["http://library.ucsd.edu/ark:/20775/bbXXXXXXX4"]
251
- expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 4]/mads:elementValue', ns).map(&:text)).to eq ["1900s"]
248
+ expect(doc.xpath('/rdf:RDF/mads:ComplexSubject/@rdf:about', ns).map(&:value))
249
+ .to contain_exactly "http://example.org/foo"
250
+ expect(doc.xpath('//mads:ComplexSubject/mads:elementList/@rdf:parseType', ns).map(&:value))
251
+ .to contain_exactly "Collection"
252
+ expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 1]/@rdf:about', ns).map(&:value))
253
+ .to contain_exactly "http://library.ucsd.edu/ark:/20775/bbXXXXXXX6"
254
+ expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 2]/mads:elementValue', ns).map(&:text))
255
+ .to contain_exactly "Relations with Mexican Americans"
256
+ expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 3]/@rdf:about', ns).map(&:value))
257
+ .to contain_exactly "http://library.ucsd.edu/ark:/20775/bbXXXXXXX4"
258
+ expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() = 4]/mads:elementValue', ns).map(&:text))
259
+ .to contain_exactly "1900s"
252
260
  expect(RDF::List.new(subject: list.rdf_subject, graph: subject)).to be_valid
253
261
  end
254
262
 
@@ -177,24 +177,26 @@ describe "nesting attribute behavior" do
177
177
  context "for an existing B-nodes" do
178
178
  before do
179
179
  subject.attributes = { parts_attributes: [
180
- {label: 'Alternator'},
181
- {label: 'Distributor'},
182
- {label: 'Transmission'},
183
- {label: 'Fuel Filter'}]}
180
+ {label: 'Alternator'},
181
+ {label: 'Distributor'},
182
+ {label: 'Transmission'},
183
+ {label: 'Fuel Filter'}]}
184
184
  subject.parts_attributes = new_attributes
185
185
  end
186
186
 
187
187
  context "that allows destroy" do
188
- let(:args) { [:parts, allow_destroy: true] }
188
+ let(:args) { [:parts, allow_destroy: true] }
189
189
  let (:replace_object_id) { subject.parts[1].rdf_subject.to_s }
190
- let (:remove_object_id) { subject.parts[3].rdf_subject.to_s }
190
+ let (:remove_object_id) { subject.parts[3].rdf_subject.to_s }
191
191
 
192
- let(:new_attributes) { [{ id: replace_object_id, label: "Universal Joint" },
193
- { label:"Oil Pump" },
194
- { id: remove_object_id, _destroy: '1', label: "bar1 uno" }] }
192
+ let(:new_attributes) do
193
+ [{ id: replace_object_id, label: "Universal Joint" },
194
+ { label:"Oil Pump" },
195
+ { id: remove_object_id, _destroy: '1', label: "bar1 uno" }]
196
+ end
195
197
 
196
198
  it "should update nested objects" do
197
- expect(subject.parts.map{|p| p.label.first})
199
+ expect(subject.parts.map { |p| p.label.first })
198
200
  .to contain_exactly 'Universal Joint', 'Oil Pump',
199
201
  an_instance_of(String), an_instance_of(String)
200
202
  end
@@ -10,10 +10,6 @@ describe ActiveTriples::Persistable do
10
10
  RDF::Statement(RDF::Node.new, RDF::Vocab::DC.title, 'Moomin')
11
11
  end
12
12
 
13
- it 'raises an error with no #graph implementation' do
14
- expect { subject << statement }.to raise_error(NameError, /graph/)
15
- end
16
-
17
13
  describe 'method delegation' do
18
14
  context 'with a strategy' do
19
15
  let(:strategy_class) do
@@ -6,11 +6,10 @@ describe ActiveTriples::ParentStrategy do
6
6
  let(:rdf_source) { BasicPersistable.new }
7
7
 
8
8
  shared_context 'with a parent' do
9
+ subject { rdf_source.persistence_strategy }
9
10
  let(:parent) { BasicPersistable.new }
10
11
 
11
12
  before do
12
- subject.parent = parent
13
-
14
13
  rdf_source.set_persistence_strategy(described_class)
15
14
  rdf_source.persistence_strategy.parent = parent
16
15
  end
@@ -54,8 +53,7 @@ describe ActiveTriples::ParentStrategy do
54
53
 
55
54
  let(:statements) do
56
55
  [RDF::Statement(subject.source.rdf_subject, RDF::Vocab::DC.title, 'moomin'),
57
- RDF::Statement(:node, RDF::Vocab::DC.relation, subject.source.rdf_subject),
58
- RDF::Statement(:node, RDF::Vocab::DC.relation, :other_node)]
56
+ RDF::Statement(subject.parent, RDF::Vocab::DC.relation, subject.source.rdf_subject)]
59
57
  end
60
58
 
61
59
  it 'removes graph from the parent' do
@@ -176,12 +174,61 @@ describe ActiveTriples::ParentStrategy do
176
174
  context 'with parent' do
177
175
  include_context 'with a parent'
178
176
 
179
- it 'writes to #final_parent graph' do
180
- rdf_source << [RDF::Node.new, RDF::Vocab::DC.title, 'moomin']
177
+ let(:parent_st) { RDF::Statement(parent, RDF::URI(:p), rdf_source) }
178
+ let(:child_st) { RDF::Statement(rdf_source, RDF::URI(:p), 'chld') }
179
+
180
+ it 'writes to #parent graph' do
181
+ rdf_source << child_st
182
+
183
+ expect { subject.persist! }
184
+ .to change { subject.parent.statements }
185
+ .to contain_exactly *rdf_source.statements
186
+ end
187
+
188
+ it 'writes to #parent graph when parent changes while child is live' do
189
+ parent.insert(parent_st)
190
+ parent.persist!
191
+
192
+ rdf_source.insert(child_st)
193
+
194
+ expect { subject.persist! }
195
+ .to change { parent.statements }
196
+ .from(contain_exactly(parent_st))
197
+ .to(contain_exactly(parent_st, child_st))
198
+ end
199
+
200
+ context 'with nested parents' do
201
+ let(:last) { BasicPersistable.new }
181
202
 
182
- subject.persist!
183
- expect(subject.final_parent.statements)
184
- .to contain_exactly *rdf_source.statements
203
+ before do
204
+ parent.set_persistence_strategy(ActiveTriples::ParentStrategy)
205
+ parent.persistence_strategy.parent = last
206
+ rdf_source.reload
207
+ end
208
+
209
+ it 'writes to #parent graph when parent changes while child is live' do
210
+ parent.insert(parent_st)
211
+ parent.persist!
212
+
213
+ rdf_source.insert(child_st)
214
+
215
+ expect { subject.persist! }
216
+ .to change { parent.statements }
217
+ .from(contain_exactly(parent_st))
218
+ .to(contain_exactly(parent_st, child_st))
219
+ end
220
+
221
+ it 'writes to #last graph when persisting' do
222
+ parent.insert(parent_st)
223
+ parent.persist!
224
+
225
+ rdf_source.insert(child_st)
226
+
227
+ expect { subject.persist!; parent.persist! }
228
+ .to change { last.statements }
229
+ .from(contain_exactly(parent_st))
230
+ .to(contain_exactly(parent_st, child_st))
231
+ end
185
232
  end
186
233
  end
187
234
  end
@@ -200,7 +247,7 @@ describe ActiveTriples::ParentStrategy::Ancestors do
200
247
 
201
248
  context 'with parents' do
202
249
  let(:parent) { BasicPersistable.new }
203
- let(:last) { BasicPersistable.new }
250
+ let(:last) { BasicPersistable.new }
204
251
 
205
252
  before do
206
253
  parent.set_persistence_strategy(ActiveTriples::ParentStrategy)