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.
@@ -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` instead.
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
- private
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
- # @return [Array<RDF::URI>] a group of properties to use for default labels.
550
- def default_labels
551
- [RDF::Vocab::SKOS.prefLabel,
552
- RDF::Vocab::DC.title,
553
- RDF::RDFS.label,
554
- RDF::Vocab::SKOS.altLabel,
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
- # {RDFSource}. Each relation is a set ({Array}) of {RDF::Terms} that are
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
- # <{#parent}> <{#predicate}> [term] .
10
+ # <{#parent}> <{#predicate}> [term] .
11
11
  #
12
- # Relations express a set of binary relationships (on a predicate) between
13
- # the parent node and a term.
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 [rw] rel_args
30
+ # @!attribute [r] rel_args
33
31
  # @return [Hash]
34
32
  # @!attribute [r] reflections
35
33
  # @return [Class]
36
- attr_accessor :parent, :value_arguments, :rel_args
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
- self.rel_args ||= {}
49
- self.rel_args = value_arguments.pop if
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
- self.value_arguments = value_arguments
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 = 0
117
- other = other.to_a
118
- this = each
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
- cur = this.next
122
+ current = this.next
123
123
  rescue StopIteration
124
- return other.length == length ? 0 : nil
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
- return nil if other.length < length || !other.include?(cur)
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
- # Adds values to the relation
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 {RDF::Resource}, the values will be
374
- # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
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 {RDF::Term} and is not defined in `#property_config`
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 {RDF::Statement} and the handling of
383
- # non-{RDF::Resource} values.
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
- node = node_cache[value] if node_cache[value]
568
- node ||= klass.from_uri(value,parent)
569
- node.set_persistence_strategy(property_config[:persist_to]) if
570
- is_property? && property_config[:persist_to]
571
- return nil if (is_property? && property_config[:class_name]) && (class_for_value(value) != class_for_property)
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? || (rel_args && rel_args[:cast])
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
- Resource.type_registry[type_uri]
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ActiveTriples
3
- VERSION = '0.11.0'.freeze
3
+ VERSION = '1.0.0.rc1'.freeze
4
4
  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