active-triples 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,7 +9,13 @@ module ActiveTriples
9
9
  end
10
10
 
11
11
  def self.create_builder(name, options, &block)
12
- raise ArgumentError, "property names must be a Symbol" unless name.kind_of?(Symbol)
12
+ raise ArgumentError, "property names must be a Symbol" unless
13
+ name.kind_of?(Symbol)
14
+
15
+ options[:predicate] = RDF::Resource.new(options[:predicate])
16
+ raise ArgumentError, "must provide an RDF::Resource to :predicate" unless
17
+ options[:predicate].valid?
18
+
13
19
 
14
20
  new(name, options, &block)
15
21
  end
@@ -54,7 +60,9 @@ module ActiveTriples
54
60
  end
55
61
 
56
62
  def build(&block)
57
- NodeConfig.new(name, options[:predicate], options.except(:predicate)) do |config|
63
+ NodeConfig.new(name,
64
+ options[:predicate],
65
+ options.except(:predicate)) do |config|
58
66
  config.with_index(&block) if block_given?
59
67
  end
60
68
  end
@@ -0,0 +1,582 @@
1
+ require 'deprecation'
2
+ require 'active_model'
3
+ require 'active_support/core_ext/hash'
4
+
5
+ module ActiveTriples
6
+ ##
7
+ # Defines a concern for managing {RDF::Graph} driven Resources as discrete,
8
+ # stateful graphs using ActiveModel-style objects.
9
+ #
10
+ # An `RDFSource` models a resource ({RDF::Resource}) with a state that may
11
+ # change over time. The current state is represented by an {RDF::Graph},
12
+ # accessible as {#graph}. The source is an {RDF::Resource} represented by
13
+ # {#rdf_subject}, which may be either an {RDF::URI} or an {RDF::Node}.
14
+ #
15
+ # The graph of a source may contain contain arbitrary triples, including full
16
+ # representations of the state of other sources. The triples in the graph
17
+ # should be limited to statements that have bearing on the resource's state.
18
+ #
19
+ # Properties may be defined on inheriting classes to configure accessor
20
+ # methods for predicates.
21
+ #
22
+ # @example
23
+ # class License
24
+ # include Active::Triples::RDFSource
25
+ #
26
+ # configure repository: :default
27
+ # property :title, predicate: RDF::DC.title, class_name: RDF::Literal
28
+ # end
29
+ #
30
+ # @see http://www.w3.org/TR/2014/REC-rdf11-concepts-20140225/#change-over-time
31
+ # RDF Concepts and Abstract Syntax comment on "RDF source"
32
+ # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-rdf-source an
33
+ # example of the RDF source concept as defined in the LDP specification
34
+ #
35
+ # @see ActiveModel
36
+ # @see RDF::Resource
37
+ # @see RDF::Queryable
38
+ module RDFSource
39
+ extend ActiveSupport::Concern
40
+
41
+ include NestedAttributes
42
+ include Properties
43
+ include Reflection
44
+ include RDF::Value
45
+ include RDF::Countable
46
+ include RDF::Durable
47
+ include RDF::Enumerable
48
+ include RDF::Queryable
49
+ include RDF::Mutable
50
+ include ActiveModel::Validations
51
+ include ActiveModel::Conversion
52
+ include ActiveModel::Serialization
53
+ include ActiveModel::Serializers::JSON
54
+
55
+ attr_accessor :parent
56
+
57
+ def type_registry
58
+ @@type_registry ||= {}
59
+ end
60
+ module_function :type_registry
61
+
62
+ included do
63
+ extend Configurable
64
+ extend Deprecation
65
+ extend ActiveModel::Naming
66
+ extend ActiveModel::Translation
67
+ extend ActiveModel::Callbacks
68
+
69
+ delegate :each, :load!, :count, :has_statement?, :to => :@graph
70
+
71
+ define_model_callbacks :persist
72
+
73
+ protected
74
+
75
+ def insert_statement(*args)
76
+ @graph.send(:insert_statement, *args)
77
+ end
78
+
79
+ def delete_statement(*args)
80
+ @graph.send(:delete_statement, *args)
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Specifies whether the object is currently writable.
86
+ #
87
+ # @return [true, false]
88
+ def writable?
89
+ !frozen?
90
+ end
91
+
92
+ ##
93
+ # Initialize an instance of this resource class. Defaults to a
94
+ # blank node subject. In addition to RDF::Graph parameters, you
95
+ # can pass in a URI and/or a parent to build a resource from a
96
+ # existing data.
97
+ #
98
+ # You can pass in only a parent with:
99
+ # new(nil, parent)
100
+ #
101
+ # @see RDF::Graph
102
+ # @todo move this logic out to a Builder?
103
+ def initialize(*args, &block)
104
+ resource_uri = args.shift unless args.first.is_a?(Hash)
105
+ self.parent = args.shift unless args.first.is_a?(Hash)
106
+ @graph = RDF::Graph.new(*args, &block)
107
+ set_subject!(resource_uri) if resource_uri
108
+
109
+ reload
110
+ # Append type to graph if necessary.
111
+ Array(self.class.type).each do |type|
112
+ unless self.get_values(:type).include?(type)
113
+ self.get_values(:type) << type
114
+ end
115
+ end
116
+ end
117
+
118
+ def final_parent
119
+ @final_parent ||= begin
120
+ parent = self.parent
121
+ while parent && parent.parent && parent.parent != parent
122
+ parent = parent.parent
123
+ end
124
+ parent
125
+ end
126
+ end
127
+
128
+ def attributes
129
+ attrs = {}
130
+ attrs['id'] = id if id
131
+ fields.map { |f| attrs[f.to_s] = get_values(f) }
132
+ unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) }
133
+ attrs
134
+ end
135
+
136
+ def serializable_hash(options = nil)
137
+ attrs = (fields.map { |f| f.to_s }) << 'id'
138
+ hash = super(:only => attrs)
139
+ unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
140
+ hash
141
+ end
142
+
143
+ def reflections
144
+ self.class
145
+ end
146
+
147
+ def attributes=(values)
148
+ raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
149
+ values = values.with_indifferent_access
150
+ id = values.delete(:id)
151
+ set_subject!(id) if id && node?
152
+ values.each do |key, value|
153
+ if reflections.reflect_on_property(key)
154
+ set_value(rdf_subject, key, value)
155
+ elsif nested_attributes_options.keys.map { |k| "#{k}_attributes" }.include?(key)
156
+ send("#{key}=".to_sym, value)
157
+ else
158
+ raise ArgumentError, "No association found for name `#{key}'. Has it been defined yet?"
159
+ end
160
+ end
161
+ end
162
+
163
+ ##
164
+ # Returns a serialized string representation of self.
165
+ # Extends the base implementation builds a JSON-LD context if the
166
+ # specified format is :jsonld and a context is provided by
167
+ # #jsonld_context
168
+ #
169
+ # @see RDF::Enumerable#dump
170
+ #
171
+ # @param args [Array<Object>]
172
+ # @return [String]
173
+ def dump(*args)
174
+ if args.first == :jsonld and respond_to?(:jsonld_context)
175
+ args << {} unless args.last.is_a?(Hash)
176
+ args.last[:context] ||= jsonld_context
177
+ end
178
+ super
179
+ end
180
+
181
+ ##
182
+ # @return [RDF::URI, RDF::Node] a URI or Node which the resource's
183
+ # properties are about.
184
+ def rdf_subject
185
+ @rdf_subject ||= RDF::Node.new
186
+ end
187
+ alias_method :to_term, :rdf_subject
188
+
189
+ ##
190
+ # A string identifier for the resource
191
+ def id
192
+ node? ? nil : rdf_subject.to_s
193
+ end
194
+
195
+ ##
196
+ # @return [Boolean]
197
+ # @see RDF::Term#node?
198
+ def node?
199
+ rdf_subject.node?
200
+ end
201
+
202
+ ##
203
+ # @return [String, nil] the base URI the resource will use when
204
+ # setting its subject. `nil` if none is used.
205
+ def base_uri
206
+ self.class.base_uri
207
+ end
208
+
209
+ def type
210
+ self.get_values(:type).to_a
211
+ end
212
+
213
+ def type=(type)
214
+ raise "Type must be an RDF::URI" unless type.kind_of? RDF::URI
215
+ self.update(RDF::Statement.new(rdf_subject, RDF.type, type))
216
+ end
217
+
218
+ ##
219
+ # Looks for labels in various default fields, prioritizing
220
+ # configured label fields.
221
+ def rdf_label
222
+ labels = Array(self.class.rdf_label)
223
+ labels += default_labels
224
+ labels.each do |label|
225
+ values = get_values(label)
226
+ return values unless values.empty?
227
+ end
228
+ node? ? [] : [rdf_subject.to_s]
229
+ end
230
+
231
+ ##
232
+ # Lists fields registered as properties on the object.
233
+ #
234
+ # @return [Array<Symbol>] the list of registered properties.
235
+ def fields
236
+ properties.keys.map(&:to_sym).reject{|x| x == :type}
237
+ end
238
+
239
+ ##
240
+ # Load data from the #rdf_subject URI. Retrieved data will be
241
+ # parsed into the Resource's graph from available RDF::Readers
242
+ # and available from property accessors if if predicates are
243
+ # registered.
244
+ #
245
+ # osu = new('http://dbpedia.org/resource/Oregon_State_University')
246
+ # osu.fetch
247
+ # osu.rdf_label.first
248
+ # # => "Oregon State University"
249
+ #
250
+ # @return [ActiveTriples::Entity] self
251
+ def fetch
252
+ load(rdf_subject)
253
+ self
254
+ end
255
+
256
+ def persist!(opts={})
257
+ return if @persisting
258
+ return false if opts[:validate] && !valid?
259
+ @persisting = true
260
+ run_callbacks :persist do
261
+ raise "failed when trying to persist to non-existant repository or parent resource" unless repository
262
+ erase_old_resource
263
+ repository << self
264
+ @persisted = true
265
+ end
266
+ @persisting = false
267
+ true
268
+ end
269
+
270
+ ##
271
+ # Indicates if the resource is persisted.
272
+ #
273
+ # @see #persist
274
+ # @return [true, false]
275
+ def persisted?
276
+ @persisted ||= false
277
+ return (@persisted and parent.persisted?) if parent
278
+ @persisted
279
+ end
280
+
281
+ ##
282
+ # Repopulates the graph from the repository or parent resource.
283
+ #
284
+ # @return [true, false]
285
+ def reload
286
+ @relation_cache ||= {}
287
+ return false unless repository
288
+ self << repository.query(subject: rdf_subject)
289
+ unless empty?
290
+ @persisted = true
291
+ end
292
+ true
293
+ end
294
+
295
+ ##
296
+ # Adds or updates a property with supplied values.
297
+ #
298
+ # Handles two argument patterns. The recommended pattern is:
299
+ # set_value(property, values)
300
+ #
301
+ # For backwards compatibility, there is support for explicitly
302
+ # passing the rdf_subject to be used in the statement:
303
+ # set_value(uri, property, values)
304
+ #
305
+ # @note This method will delete existing statements with the correct subject and predicate from the graph
306
+ def set_value(*args)
307
+ # Add support for legacy 3-parameter syntax
308
+ if args.length > 3 || args.length < 2
309
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 2-3)"
310
+ end
311
+ values = args.pop
312
+ get_relation(args).set(values)
313
+ end
314
+
315
+ ##
316
+ # Adds or updates a property with supplied values.
317
+ #
318
+ # @note This method will delete existing statements with the correct subject and predicate from the graph
319
+ def []=(uri_or_term_property, value)
320
+ self[uri_or_term_property].set(value)
321
+ end
322
+
323
+ ##
324
+ # Returns an array of values belonging to the property
325
+ # requested. Elements in the array may RdfResource objects or a
326
+ # valid datatype.
327
+ #
328
+ # Handles two argument patterns. The recommended pattern is:
329
+ # get_values(property)
330
+ #
331
+ # For backwards compatibility, there is support for explicitly
332
+ # passing the rdf_subject to be used in th statement:
333
+ # get_values(uri, property)
334
+ def get_values(*args)
335
+ get_relation(args)
336
+ end
337
+
338
+ ##
339
+ # Returns an array of values belonging to the property
340
+ # requested. Elements in the array may RdfResource objects or a
341
+ # valid datatype.
342
+ def [](uri_or_term_property)
343
+ get_relation([uri_or_term_property])
344
+ end
345
+
346
+
347
+ def get_relation(args)
348
+ @relation_cache ||= {}
349
+ rel = Relation.new(self, args)
350
+ @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] ||= rel
351
+ @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"]
352
+ end
353
+
354
+ ##
355
+ # Set a new rdf_subject for the resource.
356
+ #
357
+ # This raises an error if the current subject is not a blank node,
358
+ # and returns false if it can't figure out how to make a URI from
359
+ # the param. Otherwise it creates a URI for the resource and
360
+ # rebuilds the graph with the updated URI.
361
+ #
362
+ # Will try to build a uri as an extension of the class's base_uri
363
+ # if appropriate.
364
+ #
365
+ # @param [#to_uri, #to_s] uri_or_str the uri or string to use
366
+ def set_subject!(uri_or_str)
367
+ raise "Refusing update URI when one is already assigned!" unless node? or rdf_subject == RDF::URI(nil)
368
+ # Refusing set uri to an empty string.
369
+ return false if uri_or_str.nil? or (uri_or_str.to_s.empty? and not uri_or_str.kind_of? RDF::URI)
370
+ # raise "Refusing update URI! This object is persisted to a datastream." if persisted?
371
+ old_subject = rdf_subject
372
+ @rdf_subject = get_uri(uri_or_str)
373
+
374
+ each_statement do |statement|
375
+ if statement.subject == old_subject
376
+ delete(statement)
377
+ self << RDF::Statement.new(rdf_subject, statement.predicate, statement.object)
378
+ elsif statement.object == old_subject
379
+ delete(statement)
380
+ self << RDF::Statement.new(statement.subject, statement.predicate, rdf_subject)
381
+ end
382
+ end
383
+ end
384
+
385
+ def destroy
386
+ clear
387
+ persist! if repository
388
+ parent.destroy_child(self) if parent
389
+ @destroyed = true
390
+ end
391
+ alias_method :destroy!, :destroy
392
+
393
+ ##
394
+ # Indicates if the Resource has been destroyed.
395
+ #
396
+ # @return [true, false]
397
+ def destroyed?
398
+ @destroyed ||= false
399
+ end
400
+
401
+ def destroy_child(child)
402
+ statements.each do |statement|
403
+ delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject
404
+ end
405
+ end
406
+
407
+ ##
408
+ # Indicates if the record is 'new' (has not yet been persisted).
409
+ #
410
+ # @return [true, false]
411
+ def new_record?
412
+ not persisted?
413
+ end
414
+
415
+ def mark_for_destruction
416
+ @marked_for_destruction = true
417
+ end
418
+
419
+ def marked_for_destruction?
420
+ @marked_for_destruction
421
+ end
422
+
423
+ protected
424
+
425
+ #Clear out any old assertions in the repository about this node or statement
426
+ # thus preparing to receive the updated assertions.
427
+ def erase_old_resource
428
+ if node?
429
+ repository.statements.each do |statement|
430
+ repository.send(:delete_statement, statement) if statement.subject == rdf_subject
431
+ end
432
+ else
433
+ repository.delete [rdf_subject, nil, nil]
434
+ end
435
+ end
436
+
437
+ private
438
+
439
+ ##
440
+ # Returns the properties registered and their configurations.
441
+ #
442
+ # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveTriples::NodeConfig}]
443
+ def properties
444
+ _active_triples_config
445
+ end
446
+
447
+ ##
448
+ # List of RDF predicates registered as properties on the object.
449
+ #
450
+ # @return [Array<RDF::URI>]
451
+ def registered_predicates
452
+ properties.values.map { |config| config.predicate }
453
+ end
454
+
455
+ ##
456
+ # List of RDF predicates used in the Resource's triples, but not
457
+ # mapped to any property or accessor methods.
458
+ #
459
+ # @return [Array<RDF::URI>]
460
+ def unregistered_predicates
461
+ preds = registered_predicates
462
+ preds << RDF.type
463
+ predicates.select { |p| !preds.include? p }
464
+ end
465
+
466
+ ##
467
+ # Given a predicate which has been registered to a property,
468
+ # returns the name of the matching property.
469
+ #
470
+ # @param predicate [RDF::URI]
471
+ #
472
+ # @return [String, nil] the name of the property mapped to the
473
+ # predicate provided
474
+ def property_for_predicate(predicate)
475
+ properties.each do |property, values|
476
+ return property if values[:predicate] == predicate
477
+ end
478
+ return nil
479
+ end
480
+
481
+ def default_labels
482
+ [RDF::SKOS.prefLabel,
483
+ RDF::DC.title,
484
+ RDF::RDFS.label,
485
+ RDF::SKOS.altLabel,
486
+ RDF::SKOS.hiddenLabel]
487
+ end
488
+
489
+ ##
490
+ # Return the repository (or parent) that this resource should
491
+ # write to when persisting.
492
+ #
493
+ # @return [RDF::Repository, ActiveTriples::Entity] the target
494
+ # repository
495
+ def repository
496
+ @repository ||=
497
+ if self.class.repository == :parent
498
+ final_parent
499
+ else
500
+ repo = Repositories.repositories[self.class.repository]
501
+ raise RepositoryNotFoundError, "The class #{self.class} expects a repository called #{self.class.repository}, but none was declared" unless repo
502
+ repo
503
+ end
504
+ end
505
+
506
+ ##
507
+ # Takes a URI or String and aggressively tries to convert it into
508
+ # an RDF term. If a String is given, first tries to interpret it
509
+ # as a valid URI, then tries to append it to base_uri. Finally,
510
+ # raises an error if no valid term can be built.
511
+ #
512
+ # The argument must be an RDF::Node, an object that responds to
513
+ # #to_uri, a String that represents a valid URI, or a String that
514
+ # appends to the Resource's base_uri to create a valid URI.
515
+ #
516
+ # @TODO: URI.scheme_list is naive and incomplete. Find a better
517
+ # way to check for an existing scheme.
518
+ #
519
+ # @param uri_or_str [RDF::Resource, String]
520
+ #
521
+ # @return [RDF::Resource] A term
522
+ # @raise [RuntimeError] no valid RDF term could be built
523
+ def get_uri(uri_or_str)
524
+ return uri_or_str.to_uri if uri_or_str.respond_to? :to_uri
525
+ return uri_or_str if uri_or_str.is_a? RDF::Node
526
+ uri_or_node = RDF::Resource.new(uri_or_str)
527
+ return uri_or_node if uri_or_node.valid?
528
+ uri_or_str = uri_or_str.to_s
529
+ return RDF::URI(base_uri.to_s) / uri_or_str if base_uri && !uri_or_str.start_with?(base_uri.to_s)
530
+ raise RuntimeError, "could not make a valid RDF::URI from #{uri_or_str}"
531
+ end
532
+
533
+ public
534
+
535
+ module ClassMethods
536
+ ##
537
+ # Adapter for a consistent interface for creating a new Resource
538
+ # from a URI. Similar functionality should exist in all objects
539
+ # which can become a Resource.
540
+ #
541
+ # @param uri [#to_uri, String]
542
+ # @param vals values to pass as arguments to ::new
543
+ #
544
+ # @return [ActiveTriples::Entity] a Resource with the given uri
545
+ def from_uri(uri, vals = nil)
546
+ new(uri, vals)
547
+ end
548
+
549
+ ##
550
+ # Test if the rdf_subject that would be generated using a
551
+ # specific ID is already in use in the triplestore.
552
+ #
553
+ # @param [Integer, #read] ID to test
554
+ #
555
+ # @return [TrueClass, FalseClass] true, if the ID is in
556
+ # use in the triplestore; otherwise, false.
557
+ # NOTE: If the ID is in use in an object not yet
558
+ # persisted, false will be returned presenting
559
+ # a window of opportunity for an ID clash.
560
+ def id_persisted?(test_id)
561
+ rdf_subject = self.new(test_id).rdf_subject
562
+ ActiveTriples::Repositories.has_subject?(rdf_subject)
563
+ end
564
+
565
+ ##
566
+ # Test if the rdf_subject that would be generated using a
567
+ # specific URI is already in use in the triplestore.
568
+ #
569
+ # @param [String, RDF::URI, #read] URI to test
570
+ #
571
+ # @return [TrueClass, FalseClass] true, if the URI is in
572
+ # use in the triplestore; otherwise, false.
573
+ # NOTE: If the URI is in use in an object not yet
574
+ # persisted, false will be returned presenting
575
+ # a window of opportunity for an ID clash.
576
+ def uri_persisted?(test_uri)
577
+ rdf_subject = test_uri.kind_of?(RDF::URI) ? test_uri : RDF::URI(test_uri)
578
+ ActiveTriples::Repositories.has_subject?(rdf_subject)
579
+ end
580
+ end
581
+ end
582
+ end