active-triples 0.8.1 → 0.8.2

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