active-triples 0.11.0 → 1.0.0.rc1

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