active-triples 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -9
  3. data/CHANGES.md +69 -0
  4. data/Gemfile +0 -2
  5. data/Guardfile +1 -2
  6. data/active-triples.gemspec +3 -3
  7. data/lib/active/triples.rb +1 -0
  8. data/lib/active_triples.rb +4 -0
  9. data/lib/active_triples/configurable.rb +3 -1
  10. data/lib/active_triples/configuration.rb +1 -0
  11. data/lib/active_triples/configuration/item.rb +1 -0
  12. data/lib/active_triples/configuration/item_factory.rb +1 -0
  13. data/lib/active_triples/configuration/merge_item.rb +5 -2
  14. data/lib/active_triples/extension_strategy.rb +1 -0
  15. data/lib/active_triples/identifiable.rb +1 -0
  16. data/lib/active_triples/list.rb +2 -0
  17. data/lib/active_triples/nested_attributes.rb +1 -1
  18. data/lib/active_triples/node_config.rb +5 -3
  19. data/lib/active_triples/persistable.rb +1 -0
  20. data/lib/active_triples/persistence_strategies/parent_strategy.rb +104 -29
  21. data/lib/active_triples/persistence_strategies/persistence_strategy.rb +15 -7
  22. data/lib/active_triples/persistence_strategies/repository_strategy.rb +26 -22
  23. data/lib/active_triples/properties.rb +84 -6
  24. data/lib/active_triples/property.rb +35 -4
  25. data/lib/active_triples/property_builder.rb +38 -4
  26. data/lib/active_triples/rdf_source.rb +225 -75
  27. data/lib/active_triples/reflection.rb +42 -3
  28. data/lib/active_triples/relation.rb +330 -73
  29. data/lib/active_triples/repositories.rb +4 -2
  30. data/lib/active_triples/resource.rb +1 -0
  31. data/lib/active_triples/schema.rb +1 -0
  32. data/lib/active_triples/undefined_property_error.rb +27 -0
  33. data/lib/active_triples/version.rb +2 -1
  34. data/spec/active_triples/configurable_spec.rb +3 -2
  35. data/spec/active_triples/configuration_spec.rb +2 -1
  36. data/spec/active_triples/extension_strategy_spec.rb +2 -1
  37. data/spec/active_triples/identifiable_spec.rb +7 -11
  38. data/spec/active_triples/list_spec.rb +1 -4
  39. data/spec/active_triples/nested_attributes_spec.rb +4 -3
  40. data/spec/active_triples/persistable_spec.rb +4 -1
  41. data/spec/active_triples/persistence_strategies/parent_strategy_spec.rb +141 -11
  42. data/spec/active_triples/persistence_strategies/persistence_strategy_spec.rb +1 -0
  43. data/spec/active_triples/persistence_strategies/repository_strategy_spec.rb +32 -17
  44. data/spec/active_triples/properties_spec.rb +68 -33
  45. data/spec/active_triples/property_builder_spec.rb +36 -0
  46. data/spec/active_triples/property_spec.rb +15 -1
  47. data/spec/active_triples/rdf_source_spec.rb +544 -6
  48. data/spec/active_triples/reflection_spec.rb +78 -0
  49. data/spec/active_triples/relation_spec.rb +505 -3
  50. data/spec/active_triples/repositories_spec.rb +3 -1
  51. data/spec/active_triples/resource_spec.rb +90 -147
  52. data/spec/active_triples/schema_spec.rb +3 -2
  53. data/spec/active_triples_spec.rb +1 -0
  54. data/spec/integration/dummies/dummy_resource_a.rb +6 -0
  55. data/spec/integration/dummies/dummy_resource_b.rb +6 -0
  56. data/spec/integration/parent_persistence_spec.rb +18 -0
  57. data/spec/integration/reciprocal_properties_spec.rb +69 -0
  58. data/spec/pragmatic_context_spec.rb +10 -8
  59. data/spec/spec_helper.rb +5 -0
  60. data/spec/support/active_model_lint.rb +4 -6
  61. data/spec/support/dummies/basic_persistable.rb +2 -11
  62. data/spec/support/matchers.rb +11 -0
  63. data/spec/support/shared_examples/persistence_strategy.rb +3 -16
  64. metadata +20 -13
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveTriples
2
3
  ##
3
4
  # @abstract defines the basic interface for persistence of {RDFSource}'s.
4
5
  #
5
- # A `PersistenceStrategy` has an underlying object (`obj`) which should
6
+ # A `PersistenceStrategy` has an underlying resource which should
6
7
  # be an `RDFSource` or equivalent. Strategies can be injected into `RDFSource`
7
8
  # instances at runtime to change the target datastore, repository, or object
8
9
  # the instance syncs its graph with on save and reload operations.
@@ -16,14 +17,20 @@ module ActiveTriples
16
17
  #
17
18
  module PersistenceStrategy
18
19
  ##
19
- # Deletes the resource from the repository.
20
+ # Deletes the resource from the datastore / repository.
20
21
  #
21
22
  # @yield prior to persisting, yields to allow a block that performs
22
23
  # deletions in the persisted graph(s).
23
24
  # @return [Boolean] true if the resource was sucessfully destroyed
24
25
  def destroy(&block)
25
- obj.clear
26
26
  yield if block_given?
27
+
28
+ # Provide a warning for strategies relying on #destroy to clear the resource
29
+ if defined? obj
30
+ warn "DEPRECATION WARNING: #destroy implementations must now explicitly call 'source.clear'"
31
+ obj.clear
32
+ end
33
+
27
34
  persist!
28
35
  @destroyed = true
29
36
  end
@@ -38,7 +45,7 @@ module ActiveTriples
38
45
  end
39
46
 
40
47
  ##
41
- # Indicates if the resource is persisted to the repository
48
+ # Indicates if the resource is persisted to the datastore / repository
42
49
  #
43
50
  # @return [Boolean] true if persisted; else false.
44
51
  def persisted?
@@ -46,7 +53,7 @@ module ActiveTriples
46
53
  end
47
54
 
48
55
  ##
49
- # @abstract save the object according to the strategy and set the
56
+ # @abstract save the resource according to the strategy and set the
50
57
  # @persisted flag to `true`
51
58
  #
52
59
  # @see #persisted?
@@ -57,8 +64,9 @@ module ActiveTriples
57
64
  end
58
65
 
59
66
  ##
60
- # @abstract Clear out any old assertions in the repository about this node
61
- # or statement thus preparing to receive the updated assertions.
67
+ # @abstract Clear out any old assertions in the datastore / repository
68
+ # about this node or statement thus preparing to receive the updated
69
+ # assertions.
62
70
  #
63
71
  # @return [Boolean]
64
72
  def erase_old_resource
@@ -1,41 +1,49 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveTriples
2
3
  ##
3
4
  # Persistence strategy for projecting `RDFSource` to `RDF::Repositories`.
4
5
  class RepositoryStrategy
5
6
  include PersistenceStrategy
6
7
 
7
- # @!attribute [r] obj
8
- # the source to persist with this strategy
9
- attr_reader :obj
8
+ # @!attribute [r] source
9
+ # the resource to persist with this strategy
10
+ attr_reader :source
10
11
 
11
12
  ##
12
- # @param obj [RDFSource, RDF::Enumerable] the `RDFSource` (or other
13
+ # @param source [RDFSource, RDF::Enumerable] the `RDFSource` (or other
13
14
  # `RDF::Enumerable` to persist with the strategy.
14
- def initialize(obj)
15
- @obj = obj
15
+ def initialize(source)
16
+ @source = source
17
+ end
18
+
19
+ ##
20
+ # Deletes the resource from the repository.
21
+ #
22
+ def destroy
23
+ super { source.clear }
16
24
  end
17
25
 
18
26
  ##
19
27
  # Clear out any old assertions in the repository about this node or statement
20
28
  # thus preparing to receive the updated assertions.
21
29
  def erase_old_resource
22
- if obj.node?
30
+ if source.node?
23
31
  repository.statements.each do |statement|
24
32
  repository.send(:delete_statement, statement) if
25
- statement.subject == obj
33
+ statement.subject == source
26
34
  end
27
35
  else
28
- repository.delete [obj.to_term, nil, nil]
36
+ repository.delete [source.to_term, nil, nil]
29
37
  end
30
38
  end
31
39
 
32
40
  ##
33
- # Persists the object to the repository
41
+ # Persists the resource to the repository
34
42
  #
35
43
  # @return [true] returns true if the save did not error
36
44
  def persist!
37
45
  erase_old_resource
38
- repository << obj
46
+ repository << source
39
47
  @persisted = true
40
48
  end
41
49
 
@@ -44,13 +52,13 @@ module ActiveTriples
44
52
  #
45
53
  # @return [Boolean]
46
54
  def reload
47
- obj << repository.query(subject: obj)
48
- @persisted = true unless obj.empty?
55
+ source << repository.query(subject: source)
56
+ @persisted = true unless source.empty?
49
57
  true
50
58
  end
51
59
 
52
60
  ##
53
- # @return [RDF::Repository] The RDF::Repository that the object will project
61
+ # @return [RDF::Repository] The RDF::Repository that the resource will project
54
62
  # itself on when persisting.
55
63
  def repository
56
64
  @repository ||= set_repository
@@ -59,20 +67,16 @@ module ActiveTriples
59
67
  private
60
68
 
61
69
  ##
62
- # Finds an appropriate repository from the calling object's configuration.
70
+ # Finds an appropriate repository from the calling resource's configuration.
63
71
  # If no repository is configured, builds an ephemeral in-memory
64
72
  # repository and 'persists' there.
65
73
  #
66
74
  # @todo find a way to move this logic out (PersistenceStrategyBuilder?).
67
75
  # so the dependency on Repositories is externalized.
68
76
  def set_repository
69
- repo_sym = obj.class.repository || obj.singleton_class.repository
70
- if repo_sym.nil?
71
- RDF::Repository.new
72
- else
73
- repo = Repositories.repositories[repo_sym]
74
- repo || raise(RepositoryNotFoundError, "The class #{obj.class} expects a repository called #{repo_sym}, but none was declared")
75
- end
77
+ return RDF::Repository.new if source.class.repository.nil?
78
+ repo = Repositories.repositories[source.class.repository]
79
+ repo || raise(RepositoryNotFoundError, "The class #{source.class} expects a repository called #{source.class.repository}, but none was declared")
76
80
  end
77
81
  end
78
82
  end
@@ -1,32 +1,98 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/core_ext/hash'
2
3
 
3
4
  module ActiveTriples
4
5
  ##
5
- # Implements property configuration common to Rdf::Resource,
6
- # RDFDatastream, and others. It does its work at the class level,
7
- # and is meant to be extended.
6
+ # Implements property configuration in the style of RDFSource. It does its
7
+ # work at the class level, and is meant to be extended.
8
+ #
9
+ # Collaborates closely with ActiveTriples::Reflection
8
10
  #
9
11
  # Define properties at the class level with:
10
12
  #
11
13
  # property :title, predicate: RDF::DC.title, class_name: ResourceClass
12
14
  #
15
+ # @see {ActiveTriples::Reflection}
16
+ # @see {ActiveTriples::PropertyBuilder}
13
17
  module Properties
14
18
  extend ActiveSupport::Concern
15
19
 
16
20
  included do
21
+ include Reflection
17
22
  initialize_generated_modules
18
23
  end
19
24
 
25
+ private
26
+
27
+ ##
28
+ # Returns the properties registered and their configurations.
29
+ #
30
+ # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveTriples::NodeConfig}]
31
+ def properties
32
+ _active_triples_config
33
+ end
34
+
35
+ ##
36
+ # Lists fields registered as properties on the object.
37
+ #
38
+ # @return [Array<Symbol>] the list of registered properties.
39
+ def fields
40
+ properties.keys.map(&:to_sym).reject{ |x| x == :type }
41
+ end
42
+
43
+ ##
44
+ # List of RDF predicates registered as properties on the object.
45
+ #
46
+ # @return [Array<RDF::URI>]
47
+ def registered_predicates
48
+ properties.values.map { |config| config.predicate }
49
+ end
50
+
51
+ ##
52
+ # List of RDF predicates used in the Resource's triples, but not
53
+ # mapped to any property or accessor methods.
54
+ #
55
+ # @return [Array<RDF::URI>]
56
+ def unregistered_predicates
57
+ preds = registered_predicates
58
+ preds << RDF.type
59
+ predicates.select { |p| !preds.include? p }
60
+ end
61
+
62
+ public
63
+
64
+ ##
65
+ # Class methods for classes with `Properties`
20
66
  module ClassMethods
21
67
  def inherited(child_class) #:nodoc:
22
68
  child_class.initialize_generated_modules
23
69
  super
24
70
  end
25
71
 
26
- def initialize_generated_modules # :nodoc:
72
+ ##
73
+ # If the property methods are not yet present, generates them.
74
+ #
75
+ # @return [Module] a module self::GeneratedPropertyMethods which is
76
+ # included in self and defines the property methods
77
+ #
78
+ # @note this is an alias to #generated_property_methods. Use it when you
79
+ # intend to initialize, rather than retrieve, the methods for code
80
+ # readability
81
+ # @see #generated_property_methods
82
+ def initialize_generated_modules
27
83
  generated_property_methods
28
84
  end
29
85
 
86
+ ##
87
+ # Gives existing generated property methods. If the property methods are
88
+ # not yet present, generates them as a new Module and includes it.
89
+ #
90
+ # @return [Module] a module self::GeneratedPropertyMethods which is
91
+ # included in self and defines the property methods
92
+ #
93
+ # @note use the alias #initialize_generated_modules for clarity of intent
94
+ # where appropriate
95
+ # @see #initialize_generated_modules
30
96
  def generated_property_methods
31
97
  @generated_property_methods ||= begin
32
98
  mod = const_set(:GeneratedPropertyMethods, Module.new)
@@ -37,15 +103,27 @@ module ActiveTriples
37
103
 
38
104
  ##
39
105
  # Registers properties for Resource-like classes
106
+ #
40
107
  # @param [Symbol] name of the property (and its accessor methods)
41
108
  # @param [Hash] opts for this property, must include a :predicate
42
109
  # @yield [index] index sets solr behaviors for the property
110
+ #
111
+ # @return [Hash{String=>ActiveTriples::NodeConfig}] the full current
112
+ # property configuration for the class
43
113
  def property(name, opts={}, &block)
44
114
  raise ArgumentError, "#{name} is a keyword and not an acceptable property name." if protected_property_name? name
45
115
  reflection = PropertyBuilder.build(self, name, opts, &block)
46
116
  Reflection.add_reflection self, name, reflection
47
117
  end
48
118
 
119
+ ##
120
+ # Checks potential property names for conflicts with existing class
121
+ # instance methods. We avoid setting properties with these names to
122
+ # prevent catastrophic method overwriting.
123
+ #
124
+ # @param [Symblol] name A potential property name.
125
+ # @return [Boolean] true if the given name matches an existing instance
126
+ # method which is not an ActiveTriples property.
49
127
  def protected_property_name?(name)
50
128
  reject = self.instance_methods.map! { |s| s.to_s.gsub(/=$/, '').to_sym }
51
129
  reject -= properties.keys.map { |k| k.to_sym }
@@ -56,9 +134,9 @@ module ActiveTriples
56
134
  # Given a property name or a predicate, return the configuration
57
135
  # for the matching property.
58
136
  #
59
- # @param term [#to_sym, RDF::Resource] a property name to predicate
137
+ # @param [#to_sym, RDF::Resource] term property name or predicate
60
138
  #
61
- # @return [ActiveTriples::NodeConfig]
139
+ # @return [ActiveTriples::NodeConfig] the configuration for the property
62
140
  def config_for_term_or_uri(term)
63
141
  return properties[term.to_s] unless
64
142
  term.is_a?(RDF::Resource) && !term.is_a?(RDFSource)
@@ -1,13 +1,44 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveTriples
2
3
  ##
3
4
  # A value object to encapsulate what a Property is. Instantiate with a hash of
4
5
  # options.
5
- class Property < OpenStruct
6
+ #
7
+ # @todo Should we enforce the interface on the various attributes that are set?
8
+ class Property
9
+ def initialize(options = {})
10
+ self.name = options.fetch(:name)
11
+ self.attributes = options.except(:name)
12
+ end
13
+
14
+ # @return Symbol
15
+ attr_reader :name
16
+
17
+ # @return Boolean
18
+ def cast
19
+ attributes.fetch(:cast, false)
20
+ end
21
+
22
+ # @return Class
23
+ def class_name
24
+ attributes[:class_name]
25
+ end
26
+
27
+ # @return RDF::Vocabulary::Term
28
+ def predicate
29
+ attributes[:predicate]
30
+ end
31
+
32
+ private
33
+
34
+ attr_writer :name
35
+ attr_accessor :attributes
36
+
37
+ alias_method :to_h, :attributes
38
+
6
39
  # Returns the property's configuration values. Will not return #name, which is
7
40
  # meant to only be accessible via the accessor.
8
41
  # @return [Hash] Configuration values for this property.
9
- def to_h
10
- super.except(:name)
11
- end
42
+ public :to_h
12
43
  end
13
44
  end
@@ -1,22 +1,51 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveTriples
3
+ ##
4
+ # A builder for property `NodeConfig`s
5
+ #
6
+ # @example
7
+ # PropertyBuilder.build(:creator, predicate: RDF::Vocab::DC.creator)
8
+ #
9
+ # @see NodeConfig
2
10
  class PropertyBuilder
3
11
 
12
+ # @!attribute [r] name
13
+ # @return
14
+ # @!attribute [r] options
15
+ # @return
4
16
  attr_reader :name, :options
5
17
 
18
+ ##
19
+ # @param name []
20
+ # @param options []
6
21
  def initialize(name, options, &block)
7
22
  @name = name
8
23
  @options = options
9
24
  end
10
25
 
26
+ ##
27
+ # @param name [Symbol]
28
+ # @param options [Hash<Symbol>]
29
+ # @option options [RDF::URI] :predicate
30
+ # @option options [String, Class] :class_name
31
+ # @option options [Boolean] :cast
32
+ #
33
+ # @yield yields to block configuring index behaviors
34
+ # @yieldparam index_object [NodeConfig::IndexObject]
35
+ #
36
+ # @return [PropertyBuilder]
37
+ # @raise [ArgumentError] if name is not a symbol and/or :predicate can't be
38
+ # coerced into a URI
39
+ #
40
+ # @see #build
11
41
  def self.create_builder(name, options, &block)
12
42
  raise ArgumentError, "property names must be a Symbol" unless
13
43
  name.kind_of?(Symbol)
14
44
 
15
- options[:predicate] = RDF::Resource.new(options[:predicate])
16
- raise ArgumentError, "must provide an RDF::Resource to :predicate" unless
45
+ options[:predicate] = RDF::URI.new(options[:predicate])
46
+ raise ArgumentError, "must provide an RDF::URI to :predicate" unless
17
47
  options[:predicate].valid?
18
48
 
19
-
20
49
  new(name, options, &block)
21
50
  end
22
51
 
@@ -58,7 +87,12 @@ module ActiveTriples
58
87
  end
59
88
  CODE
60
89
  end
61
-
90
+
91
+ ##
92
+ # @yield yields to block configuring index behaviors
93
+ # @yieldparam index_object [NodeConfig::IndexObject]
94
+ #
95
+ # @return [NodeConfig] a new property node config
62
96
  def build(&block)
63
97
  NodeConfig.new(name,
64
98
  options[:predicate],
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_model'
2
3
  require 'active_support/core_ext/hash'
4
+ require 'active_support/core_ext/array/wrap'
3
5
 
4
6
  module ActiveTriples
5
7
  ##
@@ -34,7 +36,7 @@ module ActiveTriples
34
36
  # An `RDFSource` is an {RDF::Term}---it can be used as a subject, predicate,
35
37
  # object, or context in an {RDF::Statement}.
36
38
  #
37
- # @todo complete RDF::Value/RDF::Term/RDF::Resource interfaces
39
+ # @todo complete RDF::Value/RDF::Term/RDF::Resource interfaces
38
40
  #
39
41
  # @see ActiveModel
40
42
  # @see RDF::Resource
@@ -45,7 +47,6 @@ module ActiveTriples
45
47
  include NestedAttributes
46
48
  include Persistable
47
49
  include Properties
48
- include Reflection
49
50
  include RDF::Value
50
51
  include RDF::Queryable
51
52
  include ActiveModel::Validations
@@ -74,7 +75,7 @@ module ActiveTriples
74
75
  define_model_callbacks :persist
75
76
  end
76
77
 
77
- delegate :each, :load!, :count, :has_statement?, :to => :graph
78
+ delegate :query, :each, :load!, :count, :has_statement?, :to => :graph
78
79
  delegate :to_base, :term?, :escape, :to => :to_term
79
80
 
80
81
  ##
@@ -102,7 +103,7 @@ module ActiveTriples
102
103
  reload
103
104
 
104
105
  # Append type to graph if necessary.
105
- Array(self.class.type).each do |type|
106
+ Array.wrap(self.class.type).each do |type|
106
107
  unless self.get_values(:type).include?(type)
107
108
  self.get_values(:type) << type
108
109
  end
@@ -124,49 +125,57 @@ module ActiveTriples
124
125
  end
125
126
 
126
127
  ##
127
- # Delegate parent to the persistence strategy if possible
128
+ # Gives a hash containing both the registered and unregistered attributes of
129
+ # the resource. Unregistered attributes are given with full URIs.
130
+ #
131
+ # @example
132
+ # class WithProperties
133
+ # include ActiveTriples::RDFSource
134
+ # property :title, predicate: RDF::Vocab::DC.title
135
+ # property :creator, predicate: RDF::Vocab::DC.creator,
136
+ # class_name: 'Agent'
137
+ # end
128
138
  #
129
- # @todo establish a better pattern for this. `#parent` has been a public method
130
- # in the past, but it's probably time to deprecate it.
131
- def parent
132
- persistence_strategy.respond_to?(:parent) ? persistence_strategy.parent : nil
133
- end
134
-
135
- ##
136
- # @todo deprecate/remove
137
- # @see #parent
138
- def parent=(parent)
139
- persistence_strategy.respond_to?(:parent=) ? (persistence_strategy.parent = parent) : nil
140
- end
139
+ # class Agent; include ActiveTriples::RDFSource; end
140
+ #
141
+ # resource = WithProperties.new
142
+ #
143
+ # resource.attributes
144
+ # # => {"id"=>"g47123700054720", "title"=>[], "creator"=>[]}
145
+ #
146
+ # resource.creator.build
147
+ # resource.title << ['Comet in Moominland', 'Christmas in Moominvalley']
141
148
 
149
+ # resource.attributes
150
+ # # => {"id"=>"g47123700054720",
151
+ # # "title"=>["Comet in Moominland", "Christmas in Moominvalley"],
152
+ # # "creator"=>[#<Agent:0x2adbd76f1a5c(#<Agent:0x0055b7aede34b8>)>]}
153
+ #
154
+ # resource << [resource, RDF::Vocab::DC.relation, 'Helsinki']
155
+ # # => {"id"=>"g47123700054720",
156
+ # # "title"=>["Comet in Moominland", "Christmas in Moominvalley"],
157
+ # # "creator"=>[#<Agent:0x2adbd76f1a5c(#<Agent:0x0055b7aede34b8>)>],
158
+ # # "http://purl.org/dc/terms/relation"=>["Helsinki"]}]}
159
+ #
160
+ # @return [Hash<String, Array<Object>>]
161
+ #
162
+ # @todo: should this, `#attributes=`, and `#serializable_hash` be moved out
163
+ # into a dedicated `Serializer` object?
142
164
  def attributes
143
165
  attrs = {}
144
- attrs['id'] = id if id
166
+ attrs['id'] = id
145
167
  fields.map { |f| attrs[f.to_s] = get_values(f) }
146
168
  unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) }
147
169
  attrs
148
170
  end
149
171
 
150
- def serializable_hash(options = nil)
151
- attrs = (fields.map { |f| f.to_s }) << 'id'
152
- hash = super(:only => attrs)
153
- unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
154
- hash
155
- end
156
-
157
- ##
158
- # @return [Class] gives `self#class`
159
- def reflections
160
- self.class
161
- end
162
-
163
172
  def attributes=(values)
164
173
  raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
165
174
  values = values.with_indifferent_access
166
175
  id = values.delete(:id)
167
- set_subject!(id) if id && node?
176
+ set_subject!(id) if node?
168
177
  values.each do |key, value|
169
- if reflections.reflect_on_property(key)
178
+ if reflections.has_property?(key)
170
179
  set_value(rdf_subject, key, value)
171
180
  elsif nested_attributes_options.keys.map { |k| "#{k}_attributes" }.include?(key)
172
181
  send("#{key}=".to_sym, value)
@@ -176,6 +185,13 @@ module ActiveTriples
176
185
  end
177
186
  end
178
187
 
188
+ def serializable_hash(options = nil)
189
+ attrs = (fields.map { |f| f.to_s }) << 'id'
190
+ hash = super(:only => attrs)
191
+ unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
192
+ hash
193
+ end
194
+
179
195
  ##
180
196
  # Returns a serialized string representation of self.
181
197
  # Extends the base implementation builds a JSON-LD context if the
@@ -195,7 +211,23 @@ module ActiveTriples
195
211
  end
196
212
 
197
213
  ##
198
- # Gives the representation of this
214
+ # Delegate parent to the persistence strategy if possible
215
+ #
216
+ # @todo establish a better pattern for this. `#parent` has been a public
217
+ # method in the past, but it's probably time to deprecate it.
218
+ def parent
219
+ persistence_strategy.respond_to?(:parent) ? persistence_strategy.parent : nil
220
+ end
221
+
222
+ ##
223
+ # @todo deprecate/remove
224
+ # @see #parent
225
+ def parent=(parent)
226
+ persistence_strategy.respond_to?(:parent=) ? (persistence_strategy.parent = parent) : nil
227
+ end
228
+
229
+ ##
230
+ # Gives the representation of this RDFSource as an RDF::Term
199
231
  #
200
232
  # @return [RDF::URI, RDF::Node] the URI that identifies this `RDFSource`;
201
233
  # or a bnode identifier
@@ -206,6 +238,21 @@ module ActiveTriples
206
238
  end
207
239
  alias_method :to_term, :rdf_subject
208
240
 
241
+ ##
242
+ # Returns `nil` as the `graph_name`. This behavior mimics an `RDF::Graph`
243
+ # with no graph name, or one without named graph support.
244
+ #
245
+ # @note: it's possible to think of an `RDFSource` as "supporting named
246
+ # graphs" in the sense that the `#rdf_subject` is an implied graph name.
247
+ # For RDF.rb's purposes, however, it has a nil graph name: when
248
+ # enumerating statements, we treat them as triples.
249
+ #
250
+ # @return [nil]
251
+ # @sse RDF::Graph.graph_name
252
+ def graph_name
253
+ nil
254
+ end
255
+
209
256
  ##
210
257
  # @return [String] A string identifier for the resource; '' if the
211
258
  # resource is a node
@@ -216,7 +263,7 @@ module ActiveTriples
216
263
  ##
217
264
  # @return [RDF::URI] the uri
218
265
  def to_uri
219
- uri? ? rdf_subject : NullURI.new
266
+ rdf_subject if uri?
220
267
  end
221
268
 
222
269
  ##
@@ -251,7 +298,7 @@ module ActiveTriples
251
298
  end
252
299
 
253
300
  def type
254
- self.get_values(:type).to_a
301
+ self.get_values(:type)
255
302
  end
256
303
 
257
304
  def type=(type)
@@ -262,8 +309,10 @@ module ActiveTriples
262
309
  ##
263
310
  # Looks for labels in various default fields, prioritizing
264
311
  # configured label fields.
312
+ #
313
+ # @see #default_labels
265
314
  def rdf_label
266
- labels = Array(self.class.rdf_label)
315
+ labels = Array.wrap(self.class.rdf_label)
267
316
  labels += default_labels
268
317
  labels.each do |label|
269
318
  values = get_values(label)
@@ -272,34 +321,108 @@ module ActiveTriples
272
321
  node? ? [] : [rdf_subject.to_s]
273
322
  end
274
323
 
324
+ ##
325
+ # @return [Array<RDF::URI>] a group of properties to use for default labels.
326
+ def default_labels
327
+ [RDF::SKOS.prefLabel,
328
+ RDF::DC.title,
329
+ RDF::RDFS.label,
330
+ RDF::SKOS.altLabel,
331
+ RDF::SKOS.hiddenLabel]
332
+ end
333
+
275
334
  ##
276
335
  # Load data from the #rdf_subject URI. Retrieved data will be
277
336
  # parsed into the Resource's graph from available RDF::Readers
278
337
  # and available from property accessors if if predicates are
279
338
  # registered.
280
339
  #
340
+ # @example
281
341
  # osu = new('http://dbpedia.org/resource/Oregon_State_University')
282
342
  # osu.fetch
283
343
  # osu.rdf_label.first
284
344
  # # => "Oregon State University"
285
345
  #
286
- # @return [ActiveTriples::Entity] self
287
- def fetch
288
- load(rdf_subject)
346
+ # @example with default action block
347
+ # my_source = new('http://example.org/dead_url')
348
+ # my_source.fetch { |obj| obj.status = 'dead link' }
349
+ #
350
+ # @yield gives self to block if this is a node, or an error is raised during
351
+ # load
352
+ # @yieldparam [ActiveTriples::RDFSource] resource self
353
+ #
354
+ # @return [ActiveTriples::RDFSource] self
355
+ def fetch(*args, &block)
356
+ begin
357
+ load(rdf_subject, *args)
358
+ rescue => e
359
+ if block_given?
360
+ yield(self)
361
+ else
362
+ raise "#{self} is a blank node; Cannot fetch a resource without a URI" if
363
+ node?
364
+ raise e
365
+ end
366
+ end
289
367
  self
290
368
  end
291
369
 
292
370
  ##
293
- # Adds or updates a property with supplied values.
371
+ # Adds or updates a property by creating triples for each of the supplied
372
+ # values.
294
373
  #
295
- # Handles two argument patterns. The recommended pattern is:
296
- # set_value(property, values)
374
+ # The `property` argument may be either a symbol representing a registered
375
+ # property name, or an RDF::Term to use as the predicate.
297
376
  #
298
- # For backwards compatibility, there is support for explicitly
299
- # passing the rdf_subject to be used in the statement:
300
- # set_value(uri, property, values)
377
+ # @example setting with a property name
378
+ # class Thing
379
+ # include ActiveTriples::RDFSource
380
+ # property :creator, predicate: RDF::DC.creator
381
+ # end
301
382
  #
302
- # @note This method will delete existing statements with the correct subject and predicate from the graph
383
+ # t = Thing.new
384
+ # t.set_value(:creator, 'Tove Jansson') # => ['Tove Jansson']
385
+ #
386
+ #
387
+ # @example setting with a predicate
388
+ # t = Thing.new
389
+ # t.set_value(RDF::DC.creator, 'Tove Jansson') # => ['Tove Jansson']
390
+ #
391
+ #
392
+ # The recommended pattern, which sets properties directly on this
393
+ # RDFSource, is: `set_value(property, values)`
394
+ #
395
+ # @overload set_value(property, values)
396
+ # Updates the values for the property, using this RDFSource as the subject
397
+ #
398
+ # @param [RDF::Term, #to_sym] property a symbol with the property name
399
+ # or an RDF::Term to use as a predicate.
400
+ # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
401
+ # or a single value. If not an {RDF::Resource}, the values will be
402
+ # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
403
+ #
404
+ # @overload set_value(subject, property, values)
405
+ # Updates the values for the property, using the given term as the subject
406
+ #
407
+ # @param [RDF::Term] subject the term representing the
408
+ # @param [RDF::Term, #to_sym] property a symbol with the property name
409
+ # or an RDF::Term to use as a predicate.
410
+ # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
411
+ # or a single value. If not an {RDF::Resource}, the values will be
412
+ # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
413
+ #
414
+ # @return [ActiveTriples::Relation] an array {Relation} containing the
415
+ # values of the property
416
+ #
417
+ # @raise [ActiveTriples::Relation::ValueError] when the given value can't be
418
+ # coerced into an acceptable `RDF::Term`.
419
+ #
420
+ # @note This method will delete existing statements with the given
421
+ # subject and predicate from the graph
422
+ #
423
+ # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For
424
+ # documentation on {RDF::Statement} and the handling of
425
+ # non-{RDF::Resource} values.
303
426
  def set_value(*args)
304
427
  # Add support for legacy 3-parameter syntax
305
428
  if args.length > 3 || args.length < 2
@@ -309,38 +432,61 @@ module ActiveTriples
309
432
  get_relation(args).set(values)
310
433
  end
311
434
 
312
- ##
313
- # Adds or updates a property with supplied values.
314
- #
315
- # @note This method will delete existing statements with the correct subject and predicate from the graph
316
- def []=(uri_or_term_property, value)
317
- self[uri_or_term_property].set(value)
318
- end
319
-
320
435
  ##
321
436
  # Returns an array of values belonging to the property
322
437
  # requested. Elements in the array may RdfResource objects or a
323
438
  # valid datatype.
324
439
  #
325
- # Handles two argument patterns. The recommended pattern is:
440
+ # Handles two argument patterns. The recommended pattern, which accesses
441
+ # properties directly on this RDFSource, is:
326
442
  # get_values(property)
327
443
  #
328
- # For backwards compatibility, there is support for explicitly
329
- # passing the rdf_subject to be used in th statement:
330
- # get_values(uri, property)
444
+ # @overload get_values(property)
445
+ # Gets values on the RDFSource for the given property
446
+ # @param [String, #to_term] property the property for the values
447
+ #
448
+ # @overload get_values(uri, property)
449
+ # For backwards compatibility, explicitly passing the term used as the
450
+ # subject {ActiveTriples::Relation#rdf_subject} of the returned relation.
451
+ # @param [RDF::Term] uri the term to use as the subject
452
+ # @param [String, #to_term] property the property for the values
453
+ #
454
+ # @return [ActiveTriples::Relation] an array {Relation} containing the
455
+ # values of the property
456
+ #
457
+ # @todo should this raise an error when the property argument is not an
458
+ # {RDF::Term} or a registered property key?
331
459
  def get_values(*args)
332
460
  get_relation(args)
333
461
  end
334
462
 
335
463
  ##
336
- # Returns an array of values belonging to the property
337
- # requested. Elements in the array may RdfResource objects or a
338
- # valid datatype.
339
- def [](uri_or_term_property)
340
- get_relation([uri_or_term_property])
464
+ # Returns an array of values belonging to the property requested. Elements
465
+ # in the array may RdfResource objects or a valid datatype.
466
+ #
467
+ # @param [RDF::Term, :to_s] term_or_property
468
+ def [](term_or_property)
469
+ get_relation([term_or_property])
470
+ end
471
+
472
+ ##
473
+ # Adds or updates a property with supplied values.
474
+ #
475
+ # @param [RDF::Term, :to_s] term_or_property
476
+ # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
477
+ # or a single value to set the property to.
478
+ #
479
+ # @note This method will delete existing statements with the correct
480
+ # subject and predicate from the graph
481
+ def []=(term_or_property, value)
482
+ self[term_or_property].set(value)
341
483
  end
342
484
 
485
+ ##
486
+ # @see #get_values
487
+ # @todo deprecate and remove? this is an alias to `#get_values`
343
488
  def get_relation(args)
489
+ reload if (persistence_strategy.respond_to? :loaded?) && !persistence_strategy.loaded?
344
490
  @relation_cache ||= {}
345
491
  rel = Relation.new(self, args)
346
492
  @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] ||= rel
@@ -378,12 +524,6 @@ module ActiveTriples
378
524
  end
379
525
  end
380
526
 
381
- def destroy_child(child)
382
- statements.each do |statement|
383
- delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject
384
- end
385
- end
386
-
387
527
  ##
388
528
  # Indicates if the record is 'new' (has not yet been persisted).
389
529
  #
@@ -402,11 +542,21 @@ module ActiveTriples
402
542
 
403
543
  private
404
544
 
545
+ ##
546
+ # This gives the {RDF::Graph} which represents the current state of this
547
+ # resource.
548
+ #
549
+ # @return [RDF::Graph] the underlying graph representation of the
550
+ # `RDFSource`.
551
+ #
552
+ # @see http://www.w3.org/TR/2014/REC-rdf11-concepts-20140225/#change-over-time
553
+ # RDF Concepts and Abstract Syntax comment on "RDF source"
405
554
  def graph
406
555
  @graph
407
556
  end
408
557
 
409
558
  ##
559
+
410
560
  # Lists fields registered as properties on the object.
411
561
  #
412
562
  # @return [Array<Symbol>] the list of registered properties.
@@ -442,11 +592,11 @@ module ActiveTriples
442
592
  end
443
593
 
444
594
  def default_labels
445
- [RDF::SKOS.prefLabel,
446
- RDF::DC.title,
595
+ [RDF::Vocab::SKOS.prefLabel,
596
+ RDF::Vocab::DC.title,
447
597
  RDF::RDFS.label,
448
- RDF::SKOS.altLabel,
449
- RDF::SKOS.hiddenLabel]
598
+ RDF::Vocab::SKOS.altLabel,
599
+ RDF::Vocab::SKOS.hiddenLabel]
450
600
  end
451
601
 
452
602
  ##
@@ -494,7 +644,7 @@ module ActiveTriples
494
644
 
495
645
  ##
496
646
  # Apply a predicate mapping using a given strategy.
497
- #
647
+ #
498
648
  # @param [ActiveTriples::Schema, #properties] schema A schema to apply.
499
649
  # @param [#apply!] strategy A strategy for applying. Defaults
500
650
  # to ActiveTriples::ExtensionStrategy