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,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/core_ext/class'
2
3
 
3
4
  module ActiveTriples
@@ -9,22 +10,60 @@ module ActiveTriples
9
10
  self._active_triples_config = {}
10
11
  end
11
12
 
13
+ ##
14
+ # Gives access to a `Reflection` of the properties configured on this class
15
+ #
16
+ # @example
17
+ # my_source.reflections.has_property?(:title)
18
+ # my_source.reflections.reflect_on_property(:title)
19
+ # @return [Class] gives `self#class`
20
+ #
21
+ def reflections
22
+ self.class
23
+ end
24
+
12
25
  def self.add_reflection(model, name, reflection)
13
- model._active_triples_config = model._active_triples_config.merge(name.to_s => reflection)
26
+ model._active_triples_config =
27
+ model._active_triples_config.merge(name.to_s => reflection)
14
28
  end
15
29
 
16
30
  module ClassMethods
17
- def reflect_on_property(term)
18
- _active_triples_config[term.to_s]
31
+ ##
32
+ # @param [#to_s] property
33
+ #
34
+ # @return [ActiveTriples::NodeConfig] the configuration for the property
35
+ #
36
+ # @raise [ActiveTriples::UndefinedPropertyError] when the property does
37
+ # not exist
38
+ def reflect_on_property(property)
39
+ _active_triples_config.fetch(property.to_s) do
40
+ raise ActiveTriples::UndefinedPropertyError.new(property.to_s, self)
41
+ end
19
42
  end
20
43
 
44
+ ##
45
+ # @return [Hash{String=>ActiveTriples::NodeConfig}] a hash of property
46
+ # names and their configurations
21
47
  def properties
22
48
  _active_triples_config
23
49
  end
24
50
 
51
+ ##
52
+ # @param [Hash{String=>ActiveTriples::NodeConfig}] a complete config hash
53
+ # to set the properties to.
54
+ # @return [Hash{String=>ActiveTriples::NodeConfig}] a hash of property
55
+ # names and their configurations
25
56
  def properties=(val)
26
57
  self._active_triples_config = val
27
58
  end
59
+
60
+ ##
61
+ # @param [#to_s] property
62
+ #
63
+ # @return [Boolean] true if the property exsits; false otherwise
64
+ def has_property?(property)
65
+ _active_triples_config.keys.include? property.to_s
66
+ end
28
67
  end
29
68
  end
30
69
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/core_ext/module/delegation'
2
3
 
3
4
  module ActiveTriples
@@ -8,63 +9,179 @@ module ActiveTriples
8
9
  #
9
10
  # <{#parent}> <{#predicate}> [term] .
10
11
  #
11
- # Relations, then, express n binary relationships between the parent node and
12
- # a term.
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) ...
19
+ #
13
20
  #
14
21
  # @see RDF::Term
15
22
  class Relation
16
-
17
- attr_accessor :parent, :value_arguments, :node_cache, :rel_args
23
+ include Enumerable
24
+
25
+ # @!attribute [rw] parent
26
+ # @return [RDFSource] the resource that is the domain of this relation
27
+ # @!attribute [rw] value_arguments
28
+ # @return [Array<Object>]
29
+ # @!attribute [rw] rel_args
30
+ # @return [Hash]
31
+ # @!attribute [r] reflections
32
+ # @return [Class]
33
+ attr_accessor :parent, :value_arguments, :rel_args
18
34
  attr_reader :reflections
19
35
 
20
- delegate *(Array.public_instance_methods - [:send, :__send__, :__id__, :class, :object_id] + [:as_json]), :to => :result
36
+ delegate :<=>, :==, :===, :[], :each, :empty?, :equal, :inspect, :last,
37
+ :to_a, :to_ary, :size, :join, :to => :result
21
38
 
39
+ ##
40
+ # @param [ActiveTriples::RDFSource] parent_source
41
+ # @param [Array<Symbol, Hash>] value_arguments if a Hash is passed as the
42
+ # final element, it is removed and set to `@rel_args`.
22
43
  def initialize(parent_source, value_arguments)
23
44
  self.parent = parent_source
24
45
  @reflections = parent_source.reflections
25
46
  self.rel_args ||= {}
47
+ self.rel_args = value_arguments.pop if value_arguments.is_a?(Array) &&
48
+ value_arguments.last.is_a?(Hash)
26
49
  self.value_arguments = value_arguments
27
50
  end
28
51
 
29
- def value_arguments=(value_args)
30
- if value_args.kind_of?(Array) && value_args.last.kind_of?(Hash)
31
- self.rel_args = value_args.pop
32
- end
33
- @value_arguments = value_args
34
- end
35
-
52
+ ##
53
+ # Empties the `Relation`, deleting any associated triples from `parent`.
54
+ #
55
+ # @return [Relation] self; a now empty relation
36
56
  def clear
37
- set(nil)
57
+ parent.delete([rdf_subject, predicate, nil])
58
+
59
+ self
38
60
  end
39
61
 
62
+ ##
63
+ # Gives an {Array} containing the result set for the {Relation}.
64
+ #
65
+ # By default, {RDF::URI} and {RDF::Node} results are cast to `RDFSource`.
66
+ # {Literal} results are given as their `#object` representations (e.g.
67
+ # {String}, {Date}.
68
+ #
69
+ # @example results with default casting
70
+ # parent << [parent.rdf_subject, predicate, 'my value']
71
+ # parent << [parent.rdf_subject, predicate, Date.today]
72
+ # parent << [parent.rdf_subject, predicate, RDF::URI('http://ex.org/#me')]
73
+ # parent << [parent.rdf_subject, predicate, RDF::Node.new]
74
+ # relation.result
75
+ # # => ["my_value",
76
+ # # Fri, 25 Sep 2015,
77
+ # # #<ActiveTriples::Resource:0x3f8...>,
78
+ # # #<ActiveTriples::Resource:0x3f8...>]
79
+ #
80
+ # When `cast?` is `false`, {RDF::Resource} values are left in their raw
81
+ # form. Similarly, when `#return_literals?` is `true`, literals are
82
+ # returned in their {RDF::Literal} form, preserving language tags,
83
+ # datatype, and value.
84
+ #
85
+ # @example results with `cast?` set to `false`
86
+ # relation.result
87
+ # # => ["my_value",
88
+ # # Fri, 25 Sep 2015,
89
+ # # #<RDF::URI:0x3f8... URI:http://ex.org/#me>,
90
+ # # #<RDF::Node:0x3f8...(_:g69843536054680)>]
91
+ #
92
+ # @example results with `return_literals?` set to `true`
93
+ # relation.result
94
+ # # => [#<RDF::Literal:0x3f8...("my_value")>,
95
+ # # #<RDF::Literal::Date:0x3f8...("2015-09-25"^^<http://www.w3.org/2001/XMLSchema#date>)>,
96
+ # # #<ActiveTriples::Resource:0x3f8...>,
97
+ # # #<ActiveTriples::Resource:0x3f8...>]
98
+ #
99
+ # @return [Array<Object>] the result set
40
100
  def result
41
- parent.query(:subject => rdf_subject, :predicate => predicate)
42
- .each_with_object([]) do |x, collector|
43
- converted_object = convert_object(x.object)
44
- collector << converted_object unless converted_object.nil?
101
+ return [] if predicate.nil?
102
+ statements = parent.query(:subject => rdf_subject,
103
+ :predicate => predicate)
104
+ statements.each_with_object([]) do |x, collector|
105
+ converted_object = convert_object(x.object)
106
+ collector << converted_object unless converted_object.nil?
45
107
  end
46
108
  end
47
109
 
110
+ ##
111
+ # Adds values to the relation
112
+ #
113
+ # @param [Array<RDF::Resource>, RDF::Resource] values an array of values
114
+ # or a single value. If not an {RDF::Resource}, the values will be
115
+ # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement}
116
+ #
117
+ # @return [Relation] a relation containing the set values; i.e. `self`
118
+ #
119
+ # @raise [ActiveTriples::UndefinedPropertyError] if the property is not
120
+ # already an {RDF::Term} and is not defined in `#property_config`
121
+ #
122
+ # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For
123
+ # documentation on {RDF::Statement} and the handling of
124
+ # non-{RDF::Resource} values.
48
125
  def set(values)
126
+ raise UndefinedPropertyError.new(property, reflections) if predicate.nil?
127
+ values = values.to_a if values.is_a? Relation
49
128
  values = [values].compact unless values.kind_of?(Array)
50
- values = values.to_a if values.class == Relation
51
- empty_property
52
- values.each do |val|
53
- set_value(val)
54
- end
55
- parent.persist! if parent.persistence_strategy.is_a? ParentStrategy
56
- end
57
129
 
58
- def empty_property
59
- parent.query([rdf_subject, predicate, nil]).each_statement do |statement|
60
- if !uri_class(statement.object) || uri_class(statement.object) == class_for_property
61
- parent.delete(statement)
62
- end
63
- end
130
+ clear
131
+ values.each { |val| set_value(val) }
132
+
133
+ parent.persist! if parent.persistence_strategy.is_a? ParentStrategy
134
+ self
64
135
  end
65
136
 
137
+ ##
138
+ # Builds a node with the given attributes, adding it to the relation.
139
+ #
140
+ # @param attributes [Hash] a hash of attribute names and values for the
141
+ # built node.
142
+ #
143
+ # @example building an empty generic node
144
+ # resource = ActiveTriples::Resource.new
145
+ # resource.resource.get_values(RDF::Vocab::DC.relation).build
146
+ # # => #<ActiveTriples::Resource:0x2b0(#<ActiveTriples::Resource:0x005>)>)
147
+ #
148
+ # resource.dump :ttl
149
+ # # => "\n [ <http://purl.org/dc/terms/relation> []] .\n"
150
+ #
151
+ # Nodes are built using the configured `class_name` for the relation.
152
+ # Attributes passed in the Hash argument are set on the new node through
153
+ # `RDFSource#attributes=`. If the attribute keys are not valid properties
154
+ # on the built node, we raise an error.
155
+ #
156
+ # @example building a node with attributes
157
+ # class WithRelation
158
+ # include ActiveTriples::RDFSource
159
+ # property :relation, predicate: RDF::Vocab::DC.relation,
160
+ # class_name: 'WithTitle'
161
+ # end
162
+ #
163
+ # class WithTitle
164
+ # include ActiveTriples::RDFSource
165
+ # property :title, predicate: RDF::Vocab::DC.title
166
+ # end
167
+ #
168
+ # resource = WithRelation.new
169
+ # attributes = { id: 'http://ex.org/moomin', title: 'moomin' }
170
+ #
171
+ # resource.get_values(:relation).build(attributes)
172
+ # # => #<ActiveTriples::Resource:0x2b0(#<ActiveTriples::Resource:0x005>)>)
173
+ #
174
+ # resource.dump :ttl
175
+ # # => "\n<http://ex.org/moomin> <http://purl.org/dc/terms/title> \"moomin\" .\n\n [ <http://purl.org/dc/terms/relation> <http://ex.org/moomin>] .\n"
176
+ #
177
+ # @todo: clarify class behavior; it is actually tied to type, in some cases.
178
+ #
179
+ # @see RDFSource#attributes=
180
+ # @see http://guides.rubyonrails.org/active_model_basics.html for some
181
+ # context on ActiveModel attributes.
66
182
  def build(attributes={})
67
183
  new_subject = attributes.fetch('id') { RDF::Node.new }
184
+
68
185
  make_node(new_subject).tap do |node|
69
186
  node.attributes = attributes.except('id')
70
187
  if parent.kind_of? List::ListResource
@@ -77,84 +194,198 @@ module ActiveTriples
77
194
  end
78
195
  end
79
196
 
80
- def first_or_create(attributes={})
81
- result.first || build(attributes)
197
+ ##
198
+ # @note this method behaves somewhat differently from `Array#delete`.
199
+ # It succeeds on deletion of non-existing values, always returning
200
+ # `self` unless an error is raised. There is no option to pass a block
201
+ # to evaluate if the value is not present. This is because access for
202
+ # `value` depends on query time. i.e. the `Relation` set does not have an
203
+ # underlying efficient data structure allowing a reliably cheap existence
204
+ # check.
205
+ #
206
+ # @note symbols are treated as RDF::Nodes by default in
207
+ # `RDF::Mutable#delete`, but may also represent tokens in statements.
208
+ # This casts symbols to a literals, which gets us symmetric behavior
209
+ # between `#set(:sym)` and `#delete(:sym)`.
210
+ #
211
+ # @example deleting a value
212
+ # resource = MySource.new
213
+ # resource.title = ['moomin', 'valley']
214
+ # resource.title.delete('moomin') # => ["valley"]
215
+ # resource.title # => ['valley']
216
+ #
217
+ # @example note the behavior of unmatched values
218
+ # resource = MySource.new
219
+ # resource.title = 'moomin'
220
+ # resource.title.delete('valley') # => ["moomin"]
221
+ # resource.title # => ['moomin']
222
+ #
223
+ # @param value [Object] the value to delete from the relation
224
+ # @return [ActiveTriples::Relation] self
225
+ def delete(value)
226
+ value = RDF::Literal(value) if value.is_a? Symbol
227
+ parent.delete([rdf_subject, predicate, value])
228
+
229
+ self
82
230
  end
83
231
 
84
- def delete(*values)
85
- values.each do |value|
86
- parent.delete([rdf_subject, predicate, value])
232
+ ##
233
+ # A variation on `#delete`. This queries the relation for matching
234
+ # values before running the deletion, returning `nil` if it does not exist.
235
+ #
236
+ # @param value [Object] the value to delete from the relation
237
+ #
238
+ # @return [Object, nil] `nil` if the value doesn't exist; the value
239
+ # otherwise
240
+ # @see #delete
241
+ def delete?(value)
242
+ value = RDF::Literal(value) if value.is_a? Symbol
243
+
244
+ return nil if parent.query([rdf_subject, predicate, value]).empty?
245
+
246
+ delete(value)
247
+ value
248
+ end
249
+
250
+ ##
251
+ # @overload subtract(enum)
252
+ # Deletes objects in the enumerable from the relation
253
+ # @param values [Enumerable] an enumerable of objects to delete
254
+ # @overload subtract(*values)
255
+ # Deletes each argument value from the relation
256
+ # @param *values [Array<Object>] the objects to delete
257
+ #
258
+ # @return [Relation] self
259
+ #
260
+ # @note This casts symbols to a literals, which gets us symmetric behavior
261
+ # with `#set(:sym)`.
262
+ # @see #delete
263
+ def subtract(*values)
264
+ values = values.first if values.first.is_a? Enumerable
265
+ statements = values.map do |value|
266
+ value = RDF::Literal(value) if value.is_a? Symbol
267
+ [rdf_subject, predicate, value]
87
268
  end
269
+
270
+ parent.delete(*statements)
271
+ self
88
272
  end
89
273
 
90
- def << (values)
91
- values = Array.wrap(result) | Array.wrap(values)
92
- self.set(values)
274
+ ##
275
+ # Replaces the first argument with the second as a value within the
276
+ # relation.
277
+ #
278
+ # @example
279
+ #
280
+ #
281
+ # @param swap_out [Object] the value to delete
282
+ # @param swap_in [Object] the replacement value
283
+ #
284
+ # @return [Relation] self
285
+ def swap(swap_out, swap_in)
286
+ self.<<(swap_in) if delete?(swap_out)
93
287
  end
94
288
 
95
- alias_method :push, :<<
289
+ ##
290
+ # @return [Object] the first result, if present; else a newly built node
291
+ #
292
+ # @see #build
293
+ def first_or_create(attributes={})
294
+ result.first || build(attributes)
295
+ end
96
296
 
97
- def []=(index, value)
98
- values = Array.wrap(result)
99
- raise IndexError, "Index #{index} out of bounds." if values[index].nil?
100
- values[index] = value
297
+ ##
298
+ # Adds values to the result set
299
+ #
300
+ # @param values [Object, Array<Object>] values to add
301
+ #
302
+ # @return [Relation] a relation containing the set values; i.e. `self`
303
+ def <<(values)
304
+ values = Array.wrap(result) | Array.wrap(values)
101
305
  self.set(values)
102
306
  end
307
+ alias_method :push, :<<
103
308
 
309
+ ##
310
+ # @return [Hash<Symbol, ]
311
+ # @todo find a way to simplify this?
104
312
  def property_config
105
- return type_property if (property == RDF.type || property.to_s == "type") && (!reflections.kind_of?(RDFSource) || !reflections.reflect_on_property(property))
313
+ return type_property if is_type?
314
+
106
315
  reflections.reflect_on_property(property)
107
316
  end
108
317
 
109
- def type_property
110
- { :predicate => RDF.type, :cast => false }
111
- end
112
-
113
- def reset!
114
- end
115
-
318
+ ##
319
+ # Returns the property for the Relation. This may be a registered
320
+ # property key or an {RDF::URI}.
321
+ #
322
+ # @return [Symbol, RDF::URI] the property for this Relation.
323
+ # @see #predicate
116
324
  def property
117
325
  value_arguments.last
118
326
  end
119
327
 
328
+ ##
329
+ # Gives the predicate used by the Relation. Values of this object are
330
+ # those that match the pattern `<rdf_subject> <predicate> [value] .`
331
+ #
332
+ # @return [RDF::Term, nil] the predicate for this relation; nil if
333
+ # no predicate can be found
334
+ #
335
+ # @see #property
336
+ def predicate
337
+ return property if property.is_a?(RDF::Term)
338
+ property_config[:predicate] if is_property?
339
+ end
340
+
120
341
  protected
121
342
 
122
343
  def node_cache
123
344
  @node_cache ||= {}
124
345
  end
125
346
 
347
+ def is_property?
348
+ reflections.has_property?(property) || is_type?
349
+ end
350
+
351
+ def is_type?
352
+ (property == RDF.type || property.to_s == "type") &&
353
+ (!reflections.kind_of?(RDFSource) || !is_property?)
354
+ end
355
+
126
356
  def set_value(val)
127
- object = val
128
- val = val.resource if val.respond_to?(:resource)
129
- val = value_to_node(val)
357
+ val = value_to_node(val.respond_to?(:resource) ? val.resource : val)
130
358
  if val.kind_of? RDFSource
131
359
  node_cache[val.rdf_subject] = nil
132
- add_child_node(val, object)
360
+ add_child_node(val)
133
361
  return
134
362
  end
135
363
  val = val.to_uri if val.respond_to? :to_uri
136
- raise "value must be an RDF URI, Node, Literal, or a valid datatype." \
137
- " See RDF::Literal.\n\tYou provided #{val.inspect}" unless
138
- val.kind_of? RDF::Term
364
+ raise ValueError, val unless val.kind_of? RDF::Term
139
365
  parent.insert [rdf_subject, predicate, val]
140
366
  end
141
367
 
368
+ def type_property
369
+ { :predicate => RDF.type, :cast => false }
370
+ end
371
+
142
372
  def value_to_node(val)
143
373
  valid_datatype?(val) ? RDF::Literal(val) : val
144
374
  end
145
375
 
146
- def add_child_node(resource,object=nil)
376
+ def add_child_node(resource)
147
377
  parent.insert [rdf_subject, predicate, resource.rdf_subject]
148
- unless resource.frozen?
378
+
379
+ resource = resource.dup
380
+ unless resource == parent ||
381
+ (parent.persistence_strategy.is_a?(ParentStrategy) &&
382
+ parent.persistence_strategy.ancestors.find { |a| a == resource })
149
383
  resource.set_persistence_strategy(ParentStrategy)
150
384
  resource.parent = parent
151
385
  end
152
- self.node_cache[resource.rdf_subject] = (object ? object : resource)
153
- resource.persist! if resource.persistence_strategy.is_a? ParentStrategy
154
- end
155
386
 
156
- def predicate
157
- property.kind_of?(RDF::URI) ? property : property_config[:predicate]
387
+ self.node_cache[resource.rdf_subject] = resource
388
+ resource.persist! if resource.persistence_strategy.is_a? ParentStrategy
158
389
  end
159
390
 
160
391
  def valid_datatype?(val)
@@ -189,13 +420,15 @@ module ActiveTriples
189
420
  value = RDF::Node.new if value.nil?
190
421
  node = node_cache[value] if node_cache[value]
191
422
  node ||= klass.from_uri(value,parent)
192
- return nil if (property_config && property_config[:class_name]) && (class_for_value(value) != class_for_property)
423
+ node.set_persistence_strategy(property_config[:persist_to]) if
424
+ is_property? && property_config[:persist_to]
425
+ return nil if (is_property? && property_config[:class_name]) && (class_for_value(value) != class_for_property)
193
426
  self.node_cache[value] ||= node
194
427
  node
195
428
  end
196
429
 
197
430
  def cast?
198
- return true unless property_config || (rel_args && rel_args[:cast])
431
+ return true unless is_property? || (rel_args && rel_args[:cast])
199
432
  return rel_args[:cast] if rel_args.has_key?(:cast)
200
433
  !!property_config[:cast]
201
434
  end
@@ -226,20 +459,44 @@ module ActiveTriples
226
459
  end
227
460
 
228
461
  def class_for_property
229
- klass = property_config[:class_name] if property_config
462
+ klass = property_config[:class_name] if is_property?
230
463
  klass ||= Resource
231
464
  klass = ActiveTriples.class_from_string(klass, final_parent.class) if
232
465
  klass.kind_of? String
233
466
  klass
234
467
  end
235
468
 
469
+ ##
470
+ # @return [RDF::Term] the subject of the relation
236
471
  def rdf_subject
237
- raise ArgumentError, "wrong number of arguments (#{value_arguments.length} for 1-2)" if value_arguments.length < 1 || value_arguments.length > 2
238
- if value_arguments.length > 1
239
- value_arguments.first
240
- else
241
- parent.rdf_subject
472
+ if value_arguments.length < 1 || value_arguments.length > 2
473
+ raise(ArgumentError,
474
+ "wrong number of arguments (#{value_arguments.length} for 1-2)")
242
475
  end
476
+
477
+ value_arguments.length > 1 ? value_arguments.first : parent.rdf_subject
243
478
  end
479
+
480
+ public
481
+
482
+ ##
483
+ # An error class for unallowable values in relations.
484
+ class ValueError < ArgumentError
485
+ # @!attribute [r] value
486
+ attr_reader :value
487
+
488
+ ##
489
+ # @param value [Object]
490
+ def initialize(value)
491
+ @value = value
492
+ end
493
+
494
+ ##
495
+ # @return [String]
496
+ def message
497
+ 'value must be an RDF URI, Node, Literal, or a valid datatype. '\
498
+ "See RDF::Literal.\n\tYou provided #{value.inspect}"
499
+ end
500
+ end
244
501
  end
245
502
  end