active-triples 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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)