active-triples 0.11.0 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/active-triples.gemspec +2 -1
- data/lib/active_triples/configuration.rb +22 -11
- data/lib/active_triples/extension_strategy.rb +1 -1
- data/lib/active_triples/list.rb +0 -2
- data/lib/active_triples/node_config.rb +64 -8
- data/lib/active_triples/persistable.rb +2 -12
- data/lib/active_triples/persistence_strategies/parent_strategy.rb +16 -15
- data/lib/active_triples/property.rb +36 -11
- data/lib/active_triples/rdf_source.rb +86 -9
- data/lib/active_triples/relation.rb +61 -42
- data/lib/active_triples/schema.rb +20 -1
- data/lib/active_triples/version.rb +1 -1
- data/spec/active_triples/extension_strategy_spec.rb +12 -1
- data/spec/active_triples/node_config_spec.rb +54 -0
- data/spec/active_triples/persistence_strategies/parent_strategy_spec.rb +24 -17
- data/spec/active_triples/property_spec.rb +10 -0
- data/spec/active_triples/rdf_source_spec.rb +103 -122
- data/spec/active_triples/relation_spec.rb +199 -12
- data/spec/active_triples/resource_spec.rb +115 -0
- data/spec/active_triples/schema_spec.rb +6 -0
- metadata +21 -7
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'active_model'
|
3
3
|
require 'active_support/core_ext/hash'
|
4
4
|
require 'active_support/core_ext/array/wrap'
|
5
|
+
require 'set'
|
5
6
|
|
6
7
|
module ActiveTriples
|
7
8
|
##
|
@@ -75,7 +76,26 @@ module ActiveTriples
|
|
75
76
|
define_model_callbacks :persist
|
76
77
|
end
|
77
78
|
|
79
|
+
##
|
80
|
+
# @!method count
|
81
|
+
# @return (see RDF::Graph#count)
|
82
|
+
# @!method each
|
83
|
+
# @return (see RDF::Graph#each)
|
84
|
+
# @!method load!
|
85
|
+
# @return (see RDF::Graph#load!)
|
86
|
+
# @!method has_statement?
|
87
|
+
# @return (see RDF::Graph#has_statement?)
|
88
|
+
# @!method query
|
89
|
+
# @return (see RDF::Graph#query)
|
78
90
|
delegate :query, :each, :load!, :count, :has_statement?, to: :graph
|
91
|
+
|
92
|
+
##
|
93
|
+
# @!method to_base
|
94
|
+
# @return (see RDF::Term#to_base)
|
95
|
+
# @!method term?
|
96
|
+
# @return (see RDF::Term#term?)
|
97
|
+
# @!method escape
|
98
|
+
# @return (see RDF::Term#escape)
|
79
99
|
delegate :to_base, :term?, :escape, to: :to_term
|
80
100
|
|
81
101
|
##
|
@@ -90,6 +110,8 @@ module ActiveTriples
|
|
90
110
|
# @see RDF::Graph
|
91
111
|
# @todo move this logic out to a Builder?
|
92
112
|
def initialize(*args, &block)
|
113
|
+
@observers = Set.new
|
114
|
+
|
93
115
|
resource_uri = args.shift unless args.first.is_a?(Hash)
|
94
116
|
@rdf_subject = get_uri(resource_uri) if resource_uri
|
95
117
|
|
@@ -189,6 +211,16 @@ module ActiveTriples
|
|
189
211
|
end
|
190
212
|
end
|
191
213
|
|
214
|
+
##
|
215
|
+
# @return [Array<RDF::URI>] a group of properties to use for default labels.
|
216
|
+
def default_labels
|
217
|
+
[RDF::Vocab::SKOS.prefLabel,
|
218
|
+
RDF::Vocab::DC.title,
|
219
|
+
RDF::RDFS.label,
|
220
|
+
RDF::Vocab::SKOS.altLabel,
|
221
|
+
RDF::Vocab::SKOS.hiddenLabel]
|
222
|
+
end
|
223
|
+
|
192
224
|
##
|
193
225
|
# @return [Hash]
|
194
226
|
def serializable_hash(*)
|
@@ -287,6 +319,14 @@ module ActiveTriples
|
|
287
319
|
node? ? rdf_subject.id : rdf_subject.to_s
|
288
320
|
end
|
289
321
|
|
322
|
+
##
|
323
|
+
# @return [String]
|
324
|
+
#
|
325
|
+
# @note Without a custom #inspect, we inherit from RDF::Value.
|
326
|
+
def inspect
|
327
|
+
sprintf("#<%s:%#0x ID:%s>", self.class.to_s, self.object_id, self.to_base)
|
328
|
+
end
|
329
|
+
|
290
330
|
##
|
291
331
|
# @return [Boolean] true if the Term is a node
|
292
332
|
#
|
@@ -493,7 +533,7 @@ module ActiveTriples
|
|
493
533
|
end
|
494
534
|
|
495
535
|
##
|
496
|
-
# @deprecated for removal in 1.0; use `#get_values`
|
536
|
+
# @deprecated for removal in 1.0; use `#get_values` insctead.
|
497
537
|
# @see #get_values
|
498
538
|
def get_relation(args)
|
499
539
|
warn 'DEPRECATION: `ActiveTriples::RDFSource#get_relation` will be' \
|
@@ -543,18 +583,55 @@ module ActiveTriples
|
|
543
583
|
@marked_for_destruction
|
544
584
|
end
|
545
585
|
|
546
|
-
|
586
|
+
##
|
587
|
+
# @param observer [#notify]
|
588
|
+
#
|
589
|
+
# @retern [#notify] the added observer
|
590
|
+
def add_observer(observer)
|
591
|
+
@observers.add(observer)
|
592
|
+
end
|
547
593
|
|
548
594
|
##
|
549
|
-
# @
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
RDF::Vocab::SKOS.hiddenLabel]
|
595
|
+
# @param observer [#notify] an observer to delete
|
596
|
+
#
|
597
|
+
# @return [#notify, nil] the deleted observer; nil if the observer was not
|
598
|
+
# registered
|
599
|
+
def delete_observer(observer)
|
600
|
+
@observers.delete?(observer)
|
556
601
|
end
|
557
602
|
|
603
|
+
##
|
604
|
+
# Sends `#notify` messages with the property symbol and the current values
|
605
|
+
# for the property to each observer.
|
606
|
+
#
|
607
|
+
# @note We short circuit to avoid query costs if no observers are present.
|
608
|
+
# If there are regisetred observers, values are returned as an array.
|
609
|
+
# This means that we incur query costs immediately and only once.
|
610
|
+
#
|
611
|
+
# @example Setting up observers
|
612
|
+
# class MyObserver
|
613
|
+
# def notify(property, values)
|
614
|
+
# # do something
|
615
|
+
# end
|
616
|
+
# end
|
617
|
+
#
|
618
|
+
# observer = MyObserver.new
|
619
|
+
# my_source.add_observer(observer)
|
620
|
+
#
|
621
|
+
# my_source.creator = 'Moomin'
|
622
|
+
# # the observer recieves a #notify(:creator, ['Moomin']) message here.
|
623
|
+
#
|
624
|
+
# @param property [Symbol]
|
625
|
+
#
|
626
|
+
# @return [void]
|
627
|
+
def notify_observers(property)
|
628
|
+
return if @observers.empty?
|
629
|
+
values = get_values(property).to_a
|
630
|
+
@observers.each { |o| o.notify(property, values) }
|
631
|
+
end
|
632
|
+
|
633
|
+
private
|
634
|
+
|
558
635
|
##
|
559
636
|
# Rewrites the subject and object of each statement containing
|
560
637
|
# `old_subject` in either position. Used when setting the subject to
|
@@ -4,19 +4,17 @@ require 'active_support/core_ext/module/delegation'
|
|
4
4
|
module ActiveTriples
|
5
5
|
##
|
6
6
|
# A `Relation` represents the values of a specific property/predicate on an
|
7
|
-
#
|
8
|
-
# objects in the of source's triples of the form:
|
7
|
+
# `RDFSource`. Each relation is a set (`Enumerable` of the `RDF::Term`s that
|
8
|
+
# are objects in the of source's triples of the form:
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# <{#parent}> <{#predicate}> [term] .
|
11
11
|
#
|
12
|
-
# Relations express a
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# When the term is a URI or Blank Node, it is represented in the results
|
16
|
-
# {Array} as an {RDFSource} with a graph selected as a subgraph of the
|
17
|
-
# parent's. The triples in this subgraph are: (a) those whose subject is the
|
18
|
-
# term; (b) ...
|
12
|
+
# Relations express a binary relationships (over a predicate) between the
|
13
|
+
# parent node and a set of terms.
|
19
14
|
#
|
15
|
+
# When the term is a URI or Blank Node, it is represented in the results as an
|
16
|
+
# `RDFSource`. Literal values are cast to strings, Ruby native types, or
|
17
|
+
# remain as an `RDF::Literal` as documented in `#each`.
|
20
18
|
#
|
21
19
|
# @see RDF::Term
|
22
20
|
class Relation
|
@@ -29,12 +27,12 @@ module ActiveTriples
|
|
29
27
|
# @return [RDFSource] the resource that is the domain of this relation
|
30
28
|
# @!attribute [rw] value_arguments
|
31
29
|
# @return [Array<Object>]
|
32
|
-
# @!attribute [
|
30
|
+
# @!attribute [r] rel_args
|
33
31
|
# @return [Hash]
|
34
32
|
# @!attribute [r] reflections
|
35
33
|
# @return [Class]
|
36
|
-
attr_accessor :parent, :value_arguments
|
37
|
-
attr_reader :reflections
|
34
|
+
attr_accessor :parent, :value_arguments
|
35
|
+
attr_reader :reflections, :rel_args
|
38
36
|
|
39
37
|
delegate :[], :inspect, :last, :size, :join, to: :to_a
|
40
38
|
|
@@ -45,11 +43,11 @@ module ActiveTriples
|
|
45
43
|
def initialize(parent_source, value_arguments)
|
46
44
|
self.parent = parent_source
|
47
45
|
@reflections = parent_source.reflections
|
48
|
-
|
49
|
-
|
46
|
+
@rel_args ||= {}
|
47
|
+
@rel_args = value_arguments.pop if
|
50
48
|
value_arguments.is_a?(Array) && value_arguments.last.is_a?(Hash)
|
51
49
|
|
52
|
-
|
50
|
+
@value_arguments = value_arguments
|
53
51
|
end
|
54
52
|
|
55
53
|
##
|
@@ -105,6 +103,7 @@ module ActiveTriples
|
|
105
103
|
def <=>(other)
|
106
104
|
return nil unless other.respond_to?(:each)
|
107
105
|
|
106
|
+
# If we're empty, avoid calling `#to_a` on other.
|
108
107
|
if empty?
|
109
108
|
return 0 if other.each.first.nil?
|
110
109
|
return nil
|
@@ -113,20 +112,24 @@ module ActiveTriples
|
|
113
112
|
# We'll need to traverse `other` repeatedly, so we get a stable `Array`
|
114
113
|
# representation. This avoids any repeated query cost if `other` is a
|
115
114
|
# `Relation`.
|
116
|
-
length
|
117
|
-
other
|
118
|
-
|
115
|
+
length = 0
|
116
|
+
other = other.to_a
|
117
|
+
other_length = other.length
|
118
|
+
this = each
|
119
119
|
|
120
120
|
loop do
|
121
121
|
begin
|
122
|
-
|
122
|
+
current = this.next
|
123
123
|
rescue StopIteration
|
124
|
-
|
124
|
+
# If we die, we are equal to other so far, check length and walk away.
|
125
|
+
return other_length == length ? 0 : nil
|
125
126
|
end
|
126
127
|
|
127
128
|
length += 1
|
128
|
-
|
129
|
-
|
129
|
+
|
130
|
+
# Return as not comparable if we have seen more terms than are in other,
|
131
|
+
# or if other does not include the current term.
|
132
|
+
return nil if other_length < length || !other.include?(current)
|
130
133
|
end
|
131
134
|
end
|
132
135
|
|
@@ -207,7 +210,9 @@ module ActiveTriples
|
|
207
210
|
#
|
208
211
|
# @return [Relation] self; a now empty relation
|
209
212
|
def clear
|
213
|
+
return self if empty?
|
210
214
|
parent.delete([rdf_subject, predicate, nil])
|
215
|
+
parent.notify_observers(property)
|
211
216
|
|
212
217
|
self
|
213
218
|
end
|
@@ -242,8 +247,11 @@ module ActiveTriples
|
|
242
247
|
# @return [ActiveTriples::Relation] self
|
243
248
|
def delete(value)
|
244
249
|
value = RDF::Literal(value) if value.is_a? Symbol
|
245
|
-
parent.delete([rdf_subject, predicate, value])
|
246
250
|
|
251
|
+
return self if parent.query([rdf_subject, predicate, value]).nil?
|
252
|
+
|
253
|
+
parent.delete([rdf_subject, predicate, value])
|
254
|
+
parent.notify_observers(property)
|
247
255
|
self
|
248
256
|
end
|
249
257
|
|
@@ -367,20 +375,20 @@ module ActiveTriples
|
|
367
375
|
end
|
368
376
|
|
369
377
|
##
|
370
|
-
#
|
378
|
+
# Set the values of the Relation
|
371
379
|
#
|
372
380
|
# @param [Array<RDF::Resource>, RDF::Resource] values an array of values
|
373
|
-
# or a single value. If not an
|
374
|
-
# coerced to an
|
381
|
+
# or a single value. If not an `RDF::Resource`, the values will be
|
382
|
+
# coerced to an `RDF::Literal` or `RDF::Node` by `RDF::Statement`
|
375
383
|
#
|
376
384
|
# @return [Relation] a relation containing the set values; i.e. `self`
|
377
385
|
#
|
378
386
|
# @raise [ActiveTriples::UndefinedPropertyError] if the property is not
|
379
|
-
# already an
|
387
|
+
# already an `RDF::Term` and is not defined in `#property_config`
|
380
388
|
#
|
381
389
|
# @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For
|
382
|
-
# documentation on
|
383
|
-
# non
|
390
|
+
# documentation on `RDF::Statement` and the handling of
|
391
|
+
# non-`RDF::Resource` values.
|
384
392
|
def set(values)
|
385
393
|
raise UndefinedPropertyError.new(property, reflections) if predicate.nil?
|
386
394
|
|
@@ -389,8 +397,11 @@ module ActiveTriples
|
|
389
397
|
|
390
398
|
clear
|
391
399
|
values.each { |val| set_value(val) }
|
400
|
+
parent.notify_observers(property)
|
401
|
+
|
402
|
+
parent.persist! if parent.persistence_strategy.respond_to?(:ancestors) &&
|
403
|
+
parent.persistence_strategy.ancestors.any? { |r| r.is_a?(ActiveTriples::List::ListResource) }
|
392
404
|
|
393
|
-
parent.persist! if parent.persistence_strategy.is_a? ParentStrategy
|
394
405
|
self
|
395
406
|
end
|
396
407
|
|
@@ -406,6 +417,9 @@ module ActiveTriples
|
|
406
417
|
#
|
407
418
|
# @note This casts symbols to a literals, which gets us symmetric behavior
|
408
419
|
# with `#set(:sym)`.
|
420
|
+
#
|
421
|
+
# @note This method treats all calls as changes for the purpose of observer
|
422
|
+
# notifications
|
409
423
|
# @see #delete
|
410
424
|
def subtract(*values)
|
411
425
|
values = values.first if values.first.is_a? Enumerable
|
@@ -415,6 +429,7 @@ module ActiveTriples
|
|
415
429
|
end
|
416
430
|
|
417
431
|
parent.delete(*statements)
|
432
|
+
parent.notify_observers(property)
|
418
433
|
self
|
419
434
|
end
|
420
435
|
|
@@ -451,7 +466,7 @@ module ActiveTriples
|
|
451
466
|
value
|
452
467
|
end
|
453
468
|
when RDF::Resource
|
454
|
-
make_node(value)
|
469
|
+
cast? ? make_node(value) : value
|
455
470
|
else
|
456
471
|
value
|
457
472
|
end
|
@@ -538,7 +553,6 @@ module ActiveTriples
|
|
538
553
|
parent.persistence_strategy.ancestors.find { |a| a == new_resource })
|
539
554
|
new_resource.set_persistence_strategy(ParentStrategy)
|
540
555
|
new_resource.parent = parent
|
541
|
-
new_resource.persist!
|
542
556
|
end
|
543
557
|
|
544
558
|
self.node_cache[resource.rdf_subject] = (resource == object ? new_resource : object)
|
@@ -561,22 +575,27 @@ module ActiveTriples
|
|
561
575
|
#
|
562
576
|
# @private
|
563
577
|
def make_node(value)
|
564
|
-
return value unless cast?
|
565
578
|
klass = class_for_value(value)
|
566
579
|
value = RDF::Node.new if value.nil?
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
580
|
+
|
581
|
+
return node_cache[value] if node_cache[value]
|
582
|
+
|
583
|
+
node = klass.from_uri(value, parent)
|
584
|
+
|
585
|
+
if is_property? && property_config[:persist_to]
|
586
|
+
node.set_persistence_strategy(property_config[:persist_to])
|
587
|
+
|
588
|
+
node.persistence_strategy.parent = parent if
|
589
|
+
node.persistence_strategy.is_a?(ParentStrategy)
|
590
|
+
end
|
591
|
+
|
572
592
|
self.node_cache[value] ||= node
|
573
|
-
node
|
574
593
|
end
|
575
594
|
|
576
595
|
##
|
577
596
|
# @private
|
578
597
|
def cast?
|
579
|
-
return true unless is_property? ||
|
598
|
+
return true unless is_property? || rel_args[:cast]
|
580
599
|
return rel_args[:cast] if rel_args.has_key?(:cast)
|
581
600
|
!!property_config[:cast]
|
582
601
|
end
|
@@ -605,7 +624,7 @@ module ActiveTriples
|
|
605
624
|
def uri_class(v)
|
606
625
|
v = RDF::URI.intern(v) if v.kind_of? String
|
607
626
|
type_uri = parent.query([v, RDF.type, nil]).to_a.first.try(:object)
|
608
|
-
|
627
|
+
RDFSource.type_registry[type_uri]
|
609
628
|
end
|
610
629
|
|
611
630
|
##
|
@@ -3,16 +3,35 @@ module ActiveTriples
|
|
3
3
|
##
|
4
4
|
# Super class which provides a simple property DSL for defining property ->
|
5
5
|
# predicate mappings.
|
6
|
+
#
|
7
|
+
# @example defining and applying a custom schema
|
8
|
+
# class MySchema < ActiveTriples::Schema
|
9
|
+
# property :title, predicate: RDF::Vocab::DC.title
|
10
|
+
# property :creator, predicate: RDF::Vocab::DC.creator, other: :options
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# resource = Class.new { include ActiveTriples::RDFSource }
|
14
|
+
# resource.apply_schema(MySchema)
|
15
|
+
#
|
6
16
|
class Schema
|
7
17
|
class << self
|
18
|
+
##
|
19
|
+
# Define a property.
|
20
|
+
#
|
8
21
|
# @param [Symbol] property The property name on the object.
|
9
22
|
# @param [Hash] options Options for the property.
|
23
|
+
# @option options [Boolean] :cast
|
24
|
+
# @option options [String, Class] :class_name
|
10
25
|
# @option options [RDF::URI] :predicate The predicate to map the property
|
11
26
|
# to.
|
27
|
+
#
|
28
|
+
# @see ActiveTriples::Property for more about options
|
12
29
|
def property(property, options)
|
13
30
|
properties << Property.new(options.merge(:name => property))
|
14
31
|
end
|
15
|
-
|
32
|
+
|
33
|
+
##
|
34
|
+
# @return [Array<ActiveTriples::Property>]
|
16
35
|
def properties
|
17
36
|
@properties ||= []
|
18
37
|
end
|
@@ -14,14 +14,25 @@ RSpec.describe ActiveTriples::ExtensionStrategy do
|
|
14
14
|
expect(asset).to have_received(:property).with(property.name, property.to_h)
|
15
15
|
end
|
16
16
|
|
17
|
+
it 'execute the block' do
|
18
|
+
block = Proc.new {}
|
19
|
+
asset = build_asset
|
20
|
+
property = build_property("name", {:predicate => RDF::Vocab::DC.title}, &block)
|
21
|
+
|
22
|
+
subject.apply(asset, property)
|
23
|
+
|
24
|
+
expect(asset).to have_received(:property).with(property.name, property.to_h, &block)
|
25
|
+
end
|
26
|
+
|
17
27
|
def build_asset
|
18
28
|
object_double(ActiveTriples::Resource, :property => nil)
|
19
29
|
end
|
20
30
|
|
21
|
-
def build_property(name, options)
|
31
|
+
def build_property(name, options, &block)
|
22
32
|
property = object_double(ActiveTriples::Property.new(:name => nil))
|
23
33
|
allow(property).to receive(:name).and_return(name)
|
24
34
|
allow(property).to receive(:to_h).and_return(options)
|
35
|
+
allow(property).to receive(:config).and_return(block)
|
25
36
|
property
|
26
37
|
end
|
27
38
|
end
|