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.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/CHANGES.md +17 -11
- data/README.md +72 -39
- data/lib/active_triples/configurable.rb +6 -1
- data/lib/active_triples/list.rb +1 -4
- data/lib/active_triples/nested_attributes.rb +10 -7
- data/lib/active_triples/persistable.rb +13 -0
- data/lib/active_triples/persistence_strategies/parent_strategy.rb +47 -34
- data/lib/active_triples/persistence_strategies/persistence_strategy.rb +14 -1
- data/lib/active_triples/properties.rb +19 -4
- data/lib/active_triples/property_builder.rb +4 -4
- data/lib/active_triples/rdf_source.rb +142 -189
- data/lib/active_triples/relation.rb +307 -156
- data/lib/active_triples/util/buffered_transaction.rb +126 -0
- data/lib/active_triples/util/extended_bounded_description.rb +75 -0
- data/lib/active_triples/version.rb +1 -1
- data/spec/active_triples/configurable_spec.rb +35 -7
- data/spec/active_triples/identifiable_spec.rb +19 -6
- data/spec/active_triples/list_spec.rb +15 -7
- data/spec/active_triples/nested_attributes_spec.rb +12 -10
- data/spec/active_triples/persistable_spec.rb +0 -4
- data/spec/active_triples/persistence_strategies/parent_strategy_spec.rb +57 -10
- data/spec/active_triples/rdf_source_spec.rb +137 -97
- data/spec/active_triples/relation_spec.rb +436 -132
- data/spec/active_triples/resource_spec.rb +8 -23
- data/spec/active_triples/util/buffered_transaction_spec.rb +187 -0
- data/spec/active_triples/util/extended_bounded_description_spec.rb +98 -0
- data/spec/integration/reciprocal_properties_spec.rb +10 -10
- data/spec/support/matchers.rb +13 -1
- 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
|
@@ -8,9 +8,7 @@ describe ActiveTriples::Configurable do
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
after
|
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:
|
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,
|
35
|
-
|
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
|
-
|
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
|
-
|
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))
|
247
|
-
|
248
|
-
expect(doc.xpath('//mads:ComplexSubject/mads:elementList
|
249
|
-
|
250
|
-
expect(doc.xpath('//mads:ComplexSubject/mads:elementList/*[position() =
|
251
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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)
|
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)
|
190
|
+
let (:remove_object_id) { subject.parts[3].rdf_subject.to_s }
|
191
191
|
|
192
|
-
let(:new_attributes)
|
193
|
-
|
194
|
-
|
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(
|
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
|
-
|
180
|
-
|
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
|
-
|
183
|
-
|
184
|
-
.
|
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)
|
250
|
+
let(:last) { BasicPersistable.new }
|
204
251
|
|
205
252
|
before do
|
206
253
|
parent.set_persistence_strategy(ActiveTriples::ParentStrategy)
|