active-triples 0.0.1

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.
@@ -0,0 +1,132 @@
1
+ require 'active_support'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/class'
4
+
5
+ module ActiveTriples
6
+ module NestedAttributes
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :nested_attributes_options, :instance_writer => false
11
+ self.nested_attributes_options = {}
12
+ end
13
+
14
+ private
15
+
16
+ UNASSIGNABLE_KEYS = %w(_destroy )
17
+
18
+ # @param [Symbol] association_name
19
+ # @param [Hash, Array] attributes_collection
20
+ # @example
21
+ #
22
+ # assign_nested_attributes_for_collection_association(:people, {
23
+ # '1' => { id: '1', name: 'Peter' },
24
+ # '2' => { name: 'John' },
25
+ # '3' => { id: '2', _destroy: true }
26
+ # })
27
+ #
28
+ # Will update the name of the Person with ID 1, build a new associated
29
+ # person with the name 'John', and mark the associated Person with ID 2
30
+ # for destruction.
31
+ #
32
+ # Also accepts an Array of attribute hashes:
33
+ #
34
+ # assign_nested_attributes_for_collection_association(:people, [
35
+ # { id: '1', name: 'Peter' },
36
+ # { name: 'John' },
37
+ # { id: '2', _destroy: true }
38
+ # ])
39
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
40
+ options = self.nested_attributes_options[association_name]
41
+
42
+ # TODO
43
+ #check_record_limit!(options[:limit], attributes_collection)
44
+
45
+ if attributes_collection.is_a?(Hash)
46
+ attributes_collection = attributes_collection.values
47
+ end
48
+
49
+ association = self.send(association_name)
50
+
51
+ attributes_collection.each do |attributes|
52
+ attributes = attributes.with_indifferent_access
53
+
54
+ if attributes['id'] && existing_record = association.detect { |record| record.rdf_subject.to_s == attributes['id'].to_s }
55
+ if !call_reject_if(association_name, attributes)
56
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
57
+ end
58
+ else
59
+ attributes = attributes.with_indifferent_access
60
+ association.build(attributes.except(*UNASSIGNABLE_KEYS))
61
+ end
62
+ end
63
+ end
64
+
65
+ # Updates a record with the +attributes+ or marks it for destruction if
66
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
67
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
68
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
69
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
70
+ end
71
+
72
+ def call_reject_if(association_name, attributes)
73
+ return false if has_destroy_flag?(attributes)
74
+ case callback = self.nested_attributes_options[association_name][:reject_if]
75
+ when Symbol
76
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
77
+ when Proc
78
+ callback.call(attributes)
79
+ end
80
+ end
81
+
82
+ # Determines if a hash contains a truthy _destroy key.
83
+ def has_destroy_flag?(hash)
84
+ ["1", "true"].include?(hash['_destroy'].to_s)
85
+ end
86
+
87
+ module ClassMethods
88
+ def accepts_nested_attributes_for *attr_names
89
+ options = { :allow_destroy => false, :update_only => false }
90
+ options.update(attr_names.extract_options!)
91
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
92
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
93
+
94
+ attr_names.each do |association_name|
95
+ nested_attributes_options = self.nested_attributes_options.dup
96
+ nested_attributes_options[association_name] = options
97
+ self.nested_attributes_options = nested_attributes_options
98
+
99
+ generate_association_writer(association_name)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Generates a writer method for this association. Serves as a point for
106
+ # accessing the objects in the association. For example, this method
107
+ # could generate the following:
108
+ #
109
+ # def pirate_attributes=(attributes)
110
+ # assign_nested_attributes_for_collection_association(:pirate, attributes)
111
+ # end
112
+ #
113
+ # This redirects the attempts to write objects in an association through
114
+ # the helper methods defined below. Makes it seem like the nested
115
+ # associations are just regular associations.
116
+ def generate_association_writer(association_name)
117
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
118
+ if method_defined?(:#{association_name}_attributes=)
119
+ remove_method(:#{association_name}_attributes=)
120
+ end
121
+ def #{association_name}_attributes=(attributes)
122
+ assign_nested_attributes_for_collection_association(:#{association_name}, attributes)
123
+ ## in lieu of autosave_association_callbacks just save all of em.
124
+ send(:#{association_name}).each {|obj| obj.marked_for_destruction? ? obj.destroy : nil}
125
+ send(:#{association_name}).reset!
126
+ end
127
+ eoruby
128
+ end
129
+ end
130
+ end
131
+ end
132
+
@@ -0,0 +1,56 @@
1
+ module ActiveTriples
2
+ class NodeConfig
3
+ attr_accessor :predicate, :term, :class_name, :type, :behaviors, :multivalue
4
+
5
+ def initialize(term, predicate, args={})
6
+ self.term = term
7
+ self.predicate = predicate
8
+ self.class_name = args.delete(:class_name)
9
+ self.multivalue = args.delete(:multivalue) { true }
10
+ raise ArgumentError, "Invalid arguments for Rdf Node configuration: #{args} on #{predicate}" unless args.empty?
11
+ end
12
+
13
+ def [](value)
14
+ value = value.to_sym
15
+ self.respond_to?(value) ? self.send(value) : nil
16
+ end
17
+
18
+ def class_name
19
+ if @class_name.kind_of?(String)
20
+ begin
21
+ new_class = @class_name.constantize
22
+ @class_name = new_class
23
+ rescue NameError
24
+ end
25
+ end
26
+ @class_name
27
+ end
28
+
29
+ def with_index (&block)
30
+ # needed for solrizer integration
31
+ iobj = IndexObject.new
32
+ yield iobj
33
+ self.type = iobj.data_type
34
+ self.behaviors = iobj.behaviors
35
+ end
36
+
37
+ # this enables a cleaner API for solr integration
38
+ class IndexObject
39
+ attr_accessor :data_type, :behaviors
40
+ def initialize
41
+ @behaviors = []
42
+ @data_type = :string
43
+ end
44
+ def as(*args)
45
+ @behaviors = args
46
+ end
47
+ def type(sym)
48
+ @data_type = sym
49
+ end
50
+ def defaults
51
+ :noop
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,96 @@
1
+ require 'deprecation'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module ActiveTriples
5
+ ##
6
+ # Implements property configuration common to Rdf::Resource,
7
+ # RDFDatastream, and others. It does its work at the class level,
8
+ # and is meant to be extended.
9
+ #
10
+ # Define properties at the class level with:
11
+ #
12
+ # property :title, predicate: RDF::DC.title, class_name: ResourceClass
13
+ #
14
+ # or with the 'old' style:
15
+ #
16
+ # map_predicates do |map|
17
+ # map.title(in: RDF::DC)
18
+ # end
19
+ #
20
+ # You can pass a block to either to set index behavior.
21
+ module Properties
22
+ extend Deprecation
23
+ attr_accessor :config
24
+
25
+ ##
26
+ # Registers properties for Resource-like classes
27
+ # @param [Symbol] name of the property (and its accessor methods)
28
+ # @param [Hash] opts for this property, must include a :predicate
29
+ # @yield [index] index sets solr behaviors for the property
30
+ def property(name, opts={}, &block)
31
+ self.config[name] = NodeConfig.new(name, opts[:predicate], opts.except(:predicate)).tap do |config|
32
+ config.with_index(&block) if block_given?
33
+ end
34
+ behaviors = config[name].behaviors.flatten if config[name].behaviors and not config[name].behaviors.empty?
35
+ register_property(name)
36
+ end
37
+
38
+ def config
39
+ @config ||= if superclass.respond_to? :config
40
+ superclass.config.dup
41
+ else
42
+ {}.with_indifferent_access
43
+ end
44
+ end
45
+
46
+ alias_method :properties, :config
47
+ alias_method :properties=, :config=
48
+
49
+ def config_for_term_or_uri(term)
50
+ return config[term.to_sym] unless term.kind_of? RDF::Resource
51
+ config.each { |k, v| return v if v.predicate == term.to_uri }
52
+ end
53
+
54
+ def fields
55
+ properties.keys.map(&:to_sym)
56
+ end
57
+
58
+ private
59
+
60
+ ##
61
+ # Private method for creating accessors for a given property.
62
+ # @param [#to_s] name Name of the accessor to be created, get/set_value is called on the resource using this.
63
+ def register_property(name)
64
+ parent = Proc.new{self}
65
+ # parent = Proc.new{resource} if self < ActiveFedora::Datastream
66
+ define_method "#{name}=" do |*args|
67
+ instance_eval(&parent).set_value(name.to_sym, *args)
68
+ end
69
+ define_method name do
70
+ instance_eval(&parent).get_values(name.to_sym)
71
+ end
72
+ end
73
+
74
+ public
75
+ # Mapper is for backwards compatibility with ActiveFedora::RDFDatastream
76
+ class Mapper
77
+ attr_accessor :parent
78
+ def initialize(parent)
79
+ @parent = parent
80
+ end
81
+ def method_missing(name, *args, &block)
82
+ properties = args.first || {}
83
+ vocab = properties.delete(:in)
84
+ to = properties.delete(:to) || name
85
+ predicate = vocab.send(to)
86
+ parent.property(name, properties.merge(predicate: predicate), &block)
87
+ end
88
+ end
89
+ def map_predicates
90
+ Deprecation.warn Properties, "map_predicates is deprecated and will be removed in active-fedora 8.0.0. Use property :name, predicate: predicate instead.", caller
91
+ mapper = Mapper.new(self)
92
+ yield(mapper)
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveTriples
2
+ ##
3
+ # Defines module methods for registering an RDF::Repository for
4
+ # persistence of Resources.
5
+ #
6
+ # This allows any triplestore (or other storage platform) with an
7
+ # RDF::Repository implementation to be used for persistence of
8
+ # resources that will be shared between ActiveFedora::Base objects.
9
+ #
10
+ # ActiveFedora::Rdf::Repositories.add_repository :blah, RDF::Repository.new
11
+ #
12
+ # Multiple repositories can be registered to keep different kinds of
13
+ # resources seperate. This is configurable on subclasses of Resource
14
+ # at the class level.
15
+ #
16
+ # @see Configurable
17
+ module Repositories
18
+
19
+ def add_repository(name, repo)
20
+ raise "Repositories must be an RDF::Repository" unless repo.kind_of? RDF::Repository
21
+ repositories[name] = repo
22
+ end
23
+ module_function :add_repository
24
+
25
+ def clear_repositories!
26
+ @repositories = {}
27
+ end
28
+ module_function :clear_repositories!
29
+
30
+ def repositories
31
+ @repositories ||= {}
32
+ end
33
+ module_function :repositories
34
+
35
+ end
36
+ end
@@ -0,0 +1,369 @@
1
+ require 'deprecation'
2
+ require 'active_model'
3
+ require 'active_support/core_ext/hash'
4
+
5
+ module ActiveTriples
6
+ ##
7
+ # Defines a generic RDF `Resource` as an RDF::Graph with property
8
+ # configuration, accessors, and some other methods for managing
9
+ # resources as discrete subgraphs which can be maintained by a Hydra
10
+ # datastream model.
11
+ #
12
+ # Resources can be instances of ActiveTriples::Resource
13
+ # directly, but more often they will be instances of subclasses with
14
+ # registered properties and configuration. e.g.
15
+ #
16
+ # class License < Resource
17
+ # configure repository: :default
18
+ # property :title, predicate: RDF::DC.title, class_name: RDF::Literal do |index|
19
+ # index.as :displayable, :facetable
20
+ # end
21
+ # end
22
+ class Resource < RDF::Graph
23
+ @@type_registry
24
+ extend Configurable
25
+ extend Properties
26
+ extend Deprecation
27
+ extend ActiveModel::Naming
28
+ include ActiveModel::Conversion
29
+ include ActiveModel::Serialization
30
+ include ActiveModel::Serializers::JSON
31
+ include NestedAttributes
32
+ attr_accessor :parent
33
+
34
+ class << self
35
+ def type_registry
36
+ @@type_registry ||= {}
37
+ end
38
+
39
+ ##
40
+ # Adapter for a consistent interface for creating a new node from a URI.
41
+ # Similar functionality should exist in all objects which can become a node.
42
+ def from_uri(uri,vals=nil)
43
+ new(uri, vals)
44
+ end
45
+ end
46
+
47
+ def writable?
48
+ !frozen?
49
+ end
50
+
51
+ ##
52
+ # Initialize an instance of this resource class. Defaults to a
53
+ # blank node subject. In addition to RDF::Graph parameters, you
54
+ # can pass in a URI and/or a parent to build a resource from a
55
+ # existing data.
56
+ #
57
+ # You can pass in only a parent with:
58
+ # Resource.new(nil, parent)
59
+ #
60
+ # @see RDF::Graph
61
+ def initialize(*args, &block)
62
+ resource_uri = args.shift unless args.first.is_a?(Hash)
63
+ self.parent = args.shift unless args.first.is_a?(Hash)
64
+ set_subject!(resource_uri) if resource_uri
65
+ super(*args, &block)
66
+ reload
67
+ # Append type to graph if necessary.
68
+ self.get_values(:type) << self.class.type if self.class.type.kind_of?(RDF::URI) && type.empty?
69
+ end
70
+
71
+ def graph
72
+ Deprecation.warn Resource, "graph is redundant & deprecated. It will be removed in active-fedora 8.0.0.", caller
73
+ self
74
+ end
75
+
76
+ def final_parent
77
+ @final_parent ||= begin
78
+ parent = self.parent
79
+ while parent && parent.parent && parent.parent != parent
80
+ parent = parent.parent
81
+ end
82
+ parent
83
+ end
84
+ end
85
+
86
+ def attributes
87
+ attrs = {}
88
+ attrs['id'] = id if id
89
+ fields.map { |f| attrs[f.to_s] = get_values(f) }
90
+ unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) }
91
+ attrs
92
+ end
93
+
94
+ def serializable_hash(options = nil)
95
+ attrs = (fields.map { |f| f.to_s }) << 'id'
96
+ hash = super(:only => attrs)
97
+ unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) }
98
+ hash
99
+ end
100
+
101
+ def attributes=(values)
102
+ raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
103
+ values = values.with_indifferent_access
104
+ set_subject!(values.delete(:id)) if values.has_key?(:id) and node?
105
+ values.each do |key, value|
106
+ if properties.keys.include?(key)
107
+ set_value(rdf_subject, key, value)
108
+ elsif self.singleton_class.nested_attributes_options.keys.map{ |k| "#{k}_attributes"}.include?(key)
109
+ send("#{key}=".to_sym, value)
110
+ else
111
+ raise ArgumentError, "No association found for name `#{key}'. Has it been defined yet?"
112
+ end
113
+ end
114
+ end
115
+
116
+ def rdf_subject
117
+ @rdf_subject ||= RDF::Node.new
118
+ end
119
+
120
+ def id
121
+ node? ? nil : rdf_subject.to_s
122
+ end
123
+
124
+ def node?
125
+ return true if rdf_subject.kind_of? RDF::Node
126
+ false
127
+ end
128
+
129
+ def to_term
130
+ rdf_subject
131
+ end
132
+
133
+ def base_uri
134
+ self.class.base_uri
135
+ end
136
+
137
+ def type
138
+ self.get_values(:type).to_a.map{|x| x.rdf_subject}
139
+ end
140
+
141
+ def type=(type)
142
+ raise "Type must be an RDF::URI" unless type.kind_of? RDF::URI
143
+ self.update(RDF::Statement.new(rdf_subject, RDF.type, type))
144
+ end
145
+
146
+ ##
147
+ # Look for labels in various default fields, prioritizing
148
+ # configured label fields
149
+ def rdf_label
150
+ labels = Array(self.class.rdf_label)
151
+ labels += default_labels
152
+ labels.each do |label|
153
+ values = get_values(label)
154
+ return values unless values.empty?
155
+ end
156
+ node? ? [] : [rdf_subject.to_s]
157
+ end
158
+
159
+ def fields
160
+ properties.keys.map(&:to_sym).reject{|x| x == :type}
161
+ end
162
+
163
+ ##
164
+ # Load data from URI
165
+ def fetch
166
+ load(rdf_subject)
167
+ self
168
+ end
169
+
170
+ def persist!
171
+ raise "failed when trying to persist to non-existant repository or parent resource" unless repository
172
+ repository.delete [rdf_subject,nil,nil] unless node?
173
+ if node?
174
+ repository.statements.each do |statement|
175
+ repository.send(:delete_statement, statement) if statement.subject == rdf_subject
176
+ end
177
+ end
178
+ repository << self
179
+ @persisted = true
180
+ end
181
+
182
+ def persisted?
183
+ @persisted ||= false
184
+ return (@persisted and parent.persisted?) if parent
185
+ @persisted
186
+ end
187
+
188
+ ##
189
+ # Repopulates the graph from the repository or parent resource.
190
+ def reload
191
+ @term_cache ||= {}
192
+ if self.class.repository == :parent
193
+ return false if final_parent.nil?
194
+ end
195
+ self << repository.query(subject: rdf_subject)
196
+ unless empty?
197
+ @persisted = true
198
+ end
199
+ true
200
+ end
201
+
202
+ ##
203
+ # Adds or updates a property with supplied values.
204
+ #
205
+ # Handles two argument patterns. The recommended pattern is:
206
+ # set_value(property, values)
207
+ #
208
+ # For backwards compatibility, there is support for explicitly
209
+ # passing the rdf_subject to be used in the statement:
210
+ # set_value(uri, property, values)
211
+ #
212
+ # @note This method will delete existing statements with the correct subject and predicate from the graph
213
+ def set_value(*args)
214
+ # Add support for legacy 3-parameter syntax
215
+ if args.length > 3 || args.length < 2
216
+ raise ArgumentError("wrong number of arguments (#{args.length} for 2-3)")
217
+ end
218
+ values = args.pop
219
+ get_term(args).set(values)
220
+ end
221
+
222
+ ##
223
+ # Returns an array of values belonging to the property
224
+ # requested. Elements in the array may RdfResource objects or a
225
+ # valid datatype.
226
+ #
227
+ # Handles two argument patterns. The recommended pattern is:
228
+ # get_values(property)
229
+ #
230
+ # For backwards compatibility, there is support for explicitly
231
+ # passing the rdf_subject to be used in th statement:
232
+ # get_values(uri, property)
233
+ def get_values(*args)
234
+ get_term(args)
235
+ end
236
+
237
+ def get_term(args)
238
+ @term_cache ||= {}
239
+ term = Term.new(self, args)
240
+ @term_cache["#{term.rdf_subject}/#{term.property}"] ||= term
241
+ @term_cache["#{term.rdf_subject}/#{term.property}"]
242
+ end
243
+
244
+ ##
245
+ # Set a new rdf_subject for the resource.
246
+ #
247
+ # This raises an error if the current subject is not a blank node,
248
+ # and returns false if it can't figure out how to make a URI from
249
+ # the param. Otherwise it creates a URI for the resource and
250
+ # rebuilds the graph with the updated URI.
251
+ #
252
+ # Will try to build a uri as an extension of the class's base_uri
253
+ # if appropriate.
254
+ #
255
+ # @param [#to_uri, #to_s] uri_or_str the uri or string to use
256
+ def set_subject!(uri_or_str)
257
+ raise "Refusing update URI when one is already assigned!" unless node?
258
+ # Refusing set uri to an empty string.
259
+ return false if uri_or_str.nil? or uri_or_str.to_s.empty?
260
+ # raise "Refusing update URI! This object is persisted to a datastream." if persisted?
261
+ old_subject = rdf_subject
262
+ @rdf_subject = get_uri(uri_or_str)
263
+
264
+ each_statement do |statement|
265
+ if statement.subject == old_subject
266
+ delete(statement)
267
+ self << RDF::Statement.new(rdf_subject, statement.predicate, statement.object)
268
+ elsif statement.object == old_subject
269
+ delete(statement)
270
+ self << RDF::Statement.new(statement.subject, statement.predicate, rdf_subject)
271
+ end
272
+ end
273
+ end
274
+
275
+ def destroy
276
+ clear
277
+ persist! if repository
278
+ parent.destroy_child(self) if parent
279
+ @destroyed = true
280
+ end
281
+ alias_method :destroy!, :destroy
282
+
283
+ def destroyed?
284
+ @destroyed ||= false
285
+ end
286
+
287
+ def destroy_child(child)
288
+ statements.each do |statement|
289
+ delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject
290
+ end
291
+ end
292
+
293
+ def new_record?
294
+ not persisted?
295
+ end
296
+
297
+ ##
298
+ # @return [String] the string to index in solr
299
+ def solrize
300
+ node? ? rdf_label : rdf_subject.to_s
301
+ end
302
+
303
+ def mark_for_destruction
304
+ @marked_for_destruction = true
305
+ end
306
+
307
+ def marked_for_destruction?
308
+ @marked_for_destruction
309
+ end
310
+
311
+ private
312
+
313
+ def properties
314
+ self.singleton_class.properties
315
+ end
316
+
317
+ def registered_predicates
318
+ properties.values.map { |config| config.predicate }
319
+ end
320
+
321
+ def unregistered_predicates
322
+ preds = registered_predicates
323
+ preds << RDF.type
324
+ predicates.select { |p| !preds.include? p }
325
+ end
326
+
327
+ def property_for_predicate(predicate)
328
+ properties.each do |property, values|
329
+ return property if values[:predicate] == predicate
330
+ end
331
+ return nil
332
+ end
333
+
334
+ def default_labels
335
+ [RDF::SKOS.prefLabel,
336
+ RDF::DC.title,
337
+ RDF::RDFS.label,
338
+ RDF::SKOS.altLabel,
339
+ RDF::SKOS.hiddenLabel]
340
+ end
341
+
342
+ ##
343
+ # Return the repository (or parent) that this resource should
344
+ # write to when persisting.
345
+ def repository
346
+ @repository ||=
347
+ if self.class.repository == :parent
348
+ final_parent
349
+ else
350
+ Repositories.repositories[self.class.repository]
351
+ end
352
+ end
353
+
354
+ ##
355
+ # Takes a URI or String and aggressively tries to create a valid RDF URI.
356
+ # Combines the input with base_uri if appropriate.
357
+ #
358
+ # @TODO: URI.scheme_list is naive and incomplete. Find a better way to check for an existing scheme.
359
+ def get_uri(uri_or_str)
360
+ return uri_or_str.to_uri if uri_or_str.respond_to? :to_uri
361
+ return uri_or_str if uri_or_str.kind_of? RDF::Node
362
+ uri_or_str = uri_or_str.to_s
363
+ return RDF::Node(uri_or_str[2..-1]) if uri_or_str.start_with? '_:'
364
+ return RDF::URI(uri_or_str) if RDF::URI(uri_or_str).valid? and (URI.scheme_list.include?(RDF::URI.new(uri_or_str).scheme.upcase) or RDF::URI.new(uri_or_str).scheme == 'info')
365
+ return RDF::URI(self.base_uri.to_s + (self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/') + uri_or_str) if base_uri && !uri_or_str.start_with?(base_uri.to_s)
366
+ raise RuntimeError, "could not make a valid RDF::URI from #{uri_or_str}"
367
+ end
368
+ end
369
+ end