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.
- 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)
|