kalimba 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module Kalimba
2
+ class KalimbaError < StandardError; end
3
+ class UnknownAttributeError < KalimbaError; end
4
+ class AttributeAssignmentError < KalimbaError
5
+ attr_reader :exception, :attribute
6
+ def initialize(message, exception, attribute)
7
+ @exception = exception
8
+ @attribute = attribute
9
+ @message = message
10
+ end
11
+ end
12
+ class MultiparameterAssignmentErrors < KalimbaError
13
+ attr_reader :errors
14
+ def initialize(errors)
15
+ @errors = errors
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ require "active_support/concern"
2
+
3
+ module Kalimba
4
+ # This module handles localized literals of RDF such that:
5
+ # 1. they are presented as normal "single" attributes,
6
+ # 2. storing the attribute in the same language overwrites
7
+ # only the attribute value in that language and does not
8
+ # delete this attribute values in other languages,
9
+ module LocalizedAttributes
10
+ extend ActiveSupport::Concern
11
+
12
+ module ClassMethods
13
+ def localizable_property?(name)
14
+ !properties[name][:collection] && properties[name][:datatype] == NS::XMLSchema["string"]
15
+ end
16
+ end
17
+
18
+ def retrieve_localizable_property(name, predicate)
19
+ localized_names = send "localized_#{name.pluralize}"
20
+ Kalimba.repository.statements.each(:subject => subject, :predicate => predicate) do |statement|
21
+ value = statement.object.value
22
+ lang = value.respond_to?(:lang) ? value.lang : nil
23
+ localized_names[lang] = value
24
+ end
25
+ localized_names[I18n.locale] || localized_names[nil]
26
+ end
27
+
28
+ def store_localizable_property(name, value, predicate, datatype)
29
+ localized_names = send "localized_#{name.pluralize}"
30
+ lang = value.respond_to?(:lang) ? value.lang : nil
31
+ localized_names.merge!(lang => value).all? do |_, v|
32
+ store_single_value(v, predicate, datatype)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,225 @@
1
+ require "securerandom"
2
+ require "active_support/concern"
3
+
4
+ module Kalimba
5
+ # @abstract
6
+ # Backend implementations should override all methods
7
+ # that delegate processing to their parent class (invoking "super").
8
+ module Persistence
9
+ extend ActiveSupport::Concern
10
+
11
+ class << self
12
+ # Create an instance of the backend storage (repository)
13
+ #
14
+ # @param [Hash] options backend storage options
15
+ # @return [Any] instance of the backend storage
16
+ def repository(options = {})
17
+ raise NotImplementedError
18
+ end
19
+
20
+ # Module of the persistence backend
21
+ #
22
+ # @return [Module]
23
+ def backend
24
+ self
25
+ end
26
+
27
+ def logger
28
+ @logger ||= defined?(::Rails) && ::Rails.logger
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ # Create a new instance of RDFS class
34
+ #
35
+ # @param [Hash<Symbol, String> => Any] attributes
36
+ # @return [Resource, nil]
37
+ def create(attributes = {})
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # Check whether instances of the RDFS class exist in the repository
42
+ #
43
+ # @param [Hash<[Symbol, String] => Any>] attributes
44
+ # @return [Boolean]
45
+ def exist?(attributes = {})
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # Remove all instances of the RDFSClass from the repository
50
+ #
51
+ # @return [Boolean]
52
+ def destroy_all
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def find(scope, options = {})
57
+ case scope
58
+ when :first
59
+ find_each(options.merge(:limit => 1)).first
60
+ when :all
61
+ find_each(options).to_a
62
+ else
63
+ find_by_id(scope)
64
+ end
65
+ end
66
+
67
+ def find_by_id(id_value)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ def find_each(options = {})
72
+ raise NotImplementedError
73
+ end
74
+
75
+ def first(options = {})
76
+ find(:first, options)
77
+ end
78
+
79
+ def all(options = {})
80
+ find(:all, options)
81
+ end
82
+
83
+ def count(attributes = {})
84
+ raise NotImplementedError
85
+ end
86
+
87
+ def logger
88
+ Kalimba::Persistence.logger
89
+ end
90
+ end
91
+
92
+ def id
93
+ subject && subject.fragment
94
+ end
95
+
96
+ # Check whether the model has never been persisted
97
+ #
98
+ # @return [Boolean]
99
+ def new_record?
100
+ raise NotImplementedError
101
+ end
102
+
103
+ # Check whether the model has ever been persisted
104
+ #
105
+ # @return [Boolean]
106
+ def persisted?
107
+ raise NotImplementedError
108
+ end
109
+
110
+ # Check whether the model has been destroyed
111
+ # (remove from the storage)
112
+ #
113
+ # @return [Boolean]
114
+ def destroyed?
115
+ @destroyed
116
+ end
117
+
118
+ # Retrieve model attributes from the backend storage
119
+ #
120
+ # @return [self]
121
+ def reload
122
+ raise NotImplementedError
123
+ end
124
+
125
+ # Remove the resource from the backend storage
126
+ #
127
+ # @return [Boolean]
128
+ def destroy
129
+ @destroyed = true
130
+ freeze
131
+ end
132
+
133
+ # Assign attributes from the given hash and persist the model
134
+ #
135
+ # @param [Hash<[Symbol, String] => Any>] params
136
+ # @return [Boolean]
137
+ def update_attributes(params = {})
138
+ assign_attributes(params)
139
+ save
140
+ end
141
+
142
+ # Persist the model into the backend storage
143
+ #
144
+ # @raise [Kalimba::KalimbaError] if fails to obtain the subject for a new record
145
+ # @return [Boolean]
146
+ def save(options = {})
147
+ @previously_changed = changes
148
+ @changed_attributes.clear
149
+ true
150
+ end
151
+
152
+ def logger
153
+ self.class.logger
154
+ end
155
+
156
+ private
157
+
158
+ def read_attribute(name, *args)
159
+ value = attributes[name]
160
+ if value.nil?
161
+ self.class.properties[name][:collection] ? [] : nil
162
+ else
163
+ value
164
+ end
165
+ end
166
+ alias_method :attribute, :read_attribute
167
+
168
+ def write_attribute(name, value)
169
+ attribute_will_change!(name) unless value == attributes[name]
170
+ attributes[name] = value
171
+ end
172
+
173
+ # Overridden implementation should return URI for the subject, generated by
174
+ # using specific random/default/sequential URI generation capabilities.
175
+ # Otherwise it should return nil.
176
+ #
177
+ # @raise [Kalimba::KalimbaError] if cannot generate subject URI
178
+ # @return [URI, nil]
179
+ def generate_subject
180
+ if self.class.base_uri
181
+ s = self.class.base_uri.dup
182
+ s.fragment = SecureRandom.urlsafe_base64
183
+ s
184
+ else
185
+ raise Kalimba::KalimbaError, "Cannot generate subject without a base URI"
186
+ end
187
+ end
188
+
189
+ def type_cast_to_rdf(value, datatype)
190
+ if value.respond_to?(:to_rdf)
191
+ value.to_rdf
192
+ else
193
+ if XmlSchema.datatype_of(value) == datatype
194
+ value
195
+ else
196
+ v = XmlSchema.instantiate(value.to_s, datatype) rescue nil
197
+ !v.nil? && XmlSchema.datatype_of(v) == datatype ? v : nil
198
+ end
199
+ end
200
+ end
201
+
202
+ def type_cast_from_rdf(value, datatype)
203
+ if value.is_a?(URI)
204
+ klass = Kalimba::Resource.from_datatype(datatype)
205
+ if klass
206
+ klass.for(value.fragment)
207
+ else
208
+ anonymous_class_from(value, datatype).for(value.fragment)
209
+ end
210
+ else
211
+ value
212
+ end
213
+ end
214
+
215
+ def anonymous_class_from(uri, datatype)
216
+ (uri = uri.dup).fragment = nil
217
+ Class.new(Kalimba::Resource).tap do |klass|
218
+ klass.class_eval do
219
+ base_uri uri
220
+ type datatype
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,11 @@
1
+ module Kalimba
2
+ class Railtie < Rails::Railtie
3
+ initializer "kalimba.initialize_database" do |app|
4
+ Kalimba.set_repository_options app.config.database_configuration[Rails.env]
5
+ end
6
+
7
+ rake_tasks do
8
+ load "kalimba/railties/repository.rake"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ desc "Create RDF storage"
2
+ namespace :db do
3
+ task :setup do
4
+ options = Rails.configuration.database_configuration[Rails.env] || {}
5
+ Kalimba::Persistence.repository(options.merge(:new => true))
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ # Enables "association-like" behaviour that many
2
+ # Rails-dependent gems rely upon.
3
+ #
4
+ # The content is mostly copied from ActiveRecord::Reflection
5
+ #
6
+ module Kalimba
7
+ module Reflection
8
+ def reflections
9
+ @reflections ||= {}
10
+ end
11
+
12
+ def create_reflection(name, params = {})
13
+ reflections[name] = AssociationReflection.new(name, {class_name: params[:datatype]})
14
+ end
15
+
16
+ def reflect_on_association(association)
17
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
18
+ end
19
+
20
+ class AssociationReflection
21
+ attr_reader :macro, :name, :options
22
+
23
+ def initialize(name, options = {})
24
+ # only :has_many macro is available for RDF
25
+ # (Sets, Bags and Unions are thus "downgraded" to it)
26
+ @macro = :has_many
27
+ @name = name
28
+ @options = options
29
+ end
30
+
31
+ # Returns the class for the macro.
32
+ #
33
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
34
+ # <tt>has_many :clients</tt> returns the Client class
35
+ def klass
36
+ @klass ||= class_name.constantize
37
+ end
38
+
39
+ # Returns the class name for the macro.
40
+ #
41
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
42
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
43
+ def class_name
44
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
45
+ end
46
+
47
+ private
48
+
49
+ def derive_class_name
50
+ name.to_s.singularize.camelize
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,266 @@
1
+ require "active_support/core_ext/class/attribute"
2
+ require "active_support/core_ext/class/subclasses"
3
+ require "kalimba/persistence" # fallback to abstract backend
4
+ require "kalimba/validations"
5
+ require "kalimba/callbacks"
6
+ require "kalimba/reflection"
7
+ require "kalimba/attribute_assignment"
8
+ require "kalimba/localized_attributes"
9
+
10
+ module Kalimba
11
+ class Resource
12
+ include ActiveModel::AttributeMethods
13
+ include ActiveModel::Dirty
14
+ include ActiveModel::Conversion
15
+
16
+ extend Kalimba::Reflection
17
+ include Kalimba::AttributeAssignment
18
+ include Kalimba::LocalizedAttributes
19
+
20
+ include Kalimba::Persistence.backend
21
+
22
+ # Subject URI if the resource
23
+ attr_reader :subject
24
+
25
+ # Hash{String => any} with the resource attributes
26
+ #
27
+ # @note
28
+ # Do not modify it directly, unless you know what you are doing!
29
+ attr_accessor :attributes
30
+
31
+ # Properties with their options
32
+ #
33
+ # @return [Hash{String => Hash}]
34
+ class_attribute :properties, instance_writer: false, instance_reader: false
35
+ self.properties = {}
36
+
37
+ class << self
38
+ # Create a new record with the given subject URI
39
+ #
40
+ # @note
41
+ # In the world of RDF a resource cannot be instantly defined as "new",
42
+ # because any arbitrary subject that you specify might be already present
43
+ # in the storage.
44
+ # So you can use ID of an existing resource as well.
45
+ #
46
+ # Don't forget to {#reload} the resource, if you need its actual attributes
47
+ # (if any) pulled from the storage.
48
+ #
49
+ # @note
50
+ # The resource ID that you supply will be added as an URI
51
+ # fragment to base_uri (or raise an error if base_uri is not defined).
52
+ #
53
+ # @param [String] rid ID to use for the resource
54
+ # @param [Hash<[Symbol, String] => Any>] params (see {RDFSResource#initialize})
55
+ # @return [Object] instance of the model
56
+ def for(rid, params = {})
57
+ new(params.merge(:_subject => rid))
58
+ end
59
+
60
+ # Type URI of RDFS class
61
+ #
62
+ # @note Can be set only once
63
+ #
64
+ # @param [URI, String] uri
65
+ # @return [URI]
66
+ def type(uri = nil)
67
+ if uri
68
+ @type ||= URI(uri)
69
+ else
70
+ @type
71
+ end
72
+ end
73
+
74
+ # Base URI for the resource
75
+ #
76
+ # @param [String, URI] uri
77
+ # @return [URI]
78
+ def base_uri(uri = nil)
79
+ @base_uri ||= uri && URI(uri.to_s.sub(/\/?$/, "/"))
80
+ end
81
+
82
+ # Property declaration
83
+ #
84
+ # Model attributes should be declared using `property`.
85
+ # Two mandatory parameters are `:predicate` and `:datatype`,
86
+ # that can accept URIs as URI or String objects.
87
+ # You can also use "NS::" namespaces provided by `xml_schema` gem.
88
+ #
89
+ # @param [Symbol, String] name
90
+ # @param [Hash] params
91
+ # @option params [String, URI] :predicate
92
+ # @option params [String, URI, Symbol] :datatype
93
+ # @option params [Boolean] :collection
94
+ # @return [void]
95
+ def property(name, params = {})
96
+ name = name.to_s
97
+
98
+ params[:predicate] = URI(params[:predicate])
99
+ association = Kalimba::Resource.from_datatype(params[:datatype])
100
+ if association
101
+ params[:datatype] = association.type
102
+ class_eval <<-HERE, __FILE__, __LINE__
103
+ def #{name}_id
104
+ self.#{name}.try(:id)
105
+ end
106
+
107
+ def #{name}_id=(value)
108
+ self.#{name} = value.blank? ? nil : #{association}.for(value)
109
+ end
110
+ HERE
111
+ else
112
+ params[:datatype] = URI(params[:datatype])
113
+ end
114
+
115
+ define_collection(name, params) if params[:collection]
116
+
117
+ self.properties[name] = params
118
+
119
+ define_attribute_method name if self.is_a?(Class)
120
+
121
+ class_eval <<-HERE, __FILE__, __LINE__
122
+ def #{name}=(value)
123
+ write_attribute "#{name}", value
124
+ end
125
+ HERE
126
+
127
+ if localizable_property?(name)
128
+ class_eval <<-HERE, __FILE__, __LINE__
129
+ def localized_#{name.pluralize}
130
+ @localized_#{name.pluralize} ||= {}
131
+ end
132
+ HERE
133
+ end
134
+ end
135
+
136
+ # Collection definition
137
+ #
138
+ # "Has-many" relations/collections are declared with help of `has_many` method.
139
+ # It accepts the same parameters as `property` (basically, it is an alias to
140
+ # `property name, ..., collection: true`).
141
+ # Additionally, you can specify `:datatype` as a name of another model,
142
+ # as seen below. If you specify datatype as an URI, it will be automatically
143
+ # resolved to either a model (having the same `type`) or anonymous class.
144
+ #
145
+ # @example
146
+ # has_many :friends, :predicate => "http://schema.org/Person", :datatype => :Person
147
+ #
148
+ # You don't have to treat `has_many` as an association with other models, however.
149
+ # It is acceptable to declare a collection of strings or any other resources
150
+ # using `has_many`:
151
+ #
152
+ # @example
153
+ # has_many :duties, :predicate => "http://works.com#duty", :datatype => NS::XMLSchema["string"]
154
+ #
155
+ # @param (see #property)
156
+ def has_many(name, params = {})
157
+ property name, params.merge(:collection => true)
158
+ end
159
+
160
+ # Return Kalimba resource class associated with the given datatype
161
+ #
162
+ # @param [String, URI, Symbol] uri
163
+ # @return [Kalimba::Resource]
164
+ def from_datatype(datatype)
165
+ datatype =
166
+ case datatype
167
+ when URI
168
+ datatype
169
+ when Symbol
170
+ const_get(datatype).type
171
+ when String
172
+ if datatype =~ URI.regexp
173
+ URI(datatype)
174
+ else
175
+ const_get(datatype).type
176
+ end
177
+ else
178
+ if datatype.respond_to?(:uri)
179
+ datatype.uri
180
+ else
181
+ raise KalimbaError, "invalid datatype identifier"
182
+ end
183
+ end
184
+ Kalimba::Resource.descendants.detect {|a| a.type == datatype }
185
+ end
186
+
187
+
188
+ private
189
+
190
+ def inherited(child)
191
+ super
192
+ child.properties = properties.dup
193
+ end
194
+
195
+ def define_collection(name, params)
196
+ create_reflection(name, params)
197
+
198
+ class_eval <<-HERE, __FILE__, __LINE__
199
+ def #{name.singularize}_ids
200
+ self.#{name}.map(&:id)
201
+ end
202
+
203
+ def #{name.singularize}_ids=(ids)
204
+ klass = self.class.reflect_on_association(:#{name}).klass
205
+ self.#{name} = ids.reject(&:blank?).map {|i| klass.for(i) }
206
+ end
207
+ HERE
208
+ end
209
+ end
210
+
211
+ # Create a new record
212
+ #
213
+ # If given a block, yields the created object into it.
214
+ #
215
+ # @param [Hash<[Symbol, String] => Any>] params properties to assign
216
+ def initialize(params = {}, options = {})
217
+ params = params.stringify_keys
218
+
219
+ if params["_subject"]
220
+ if self.class.base_uri
221
+ @subject = self.class.base_uri.dup
222
+ @subject.fragment = params.delete("_subject")
223
+ else
224
+ raise KalimbaError, "Cannot assign an ID to a resource without base_uri"
225
+ end
226
+ end
227
+
228
+ @attributes = self.class.properties.inject({}) do |attrs, (name, options)|
229
+ value = options[:collection] ? [] : nil
230
+ attrs.merge(name => value)
231
+ end
232
+ assign_attributes(params, options)
233
+
234
+ @destroyed = false
235
+
236
+ yield self if block_given?
237
+ end
238
+
239
+ # Freeze the attributes hash such that associations are still accessible,
240
+ # even on destroyed records
241
+ #
242
+ # @return [self]
243
+ def freeze
244
+ @attributes.freeze; self
245
+ end
246
+
247
+ # Checks whether the attributes hash has been frozen
248
+ #
249
+ # @return [Boolean]
250
+ def frozen?
251
+ @attributes.frozen?
252
+ end
253
+
254
+ # RDF representation of the model
255
+ #
256
+ # @return [URI, nil] subject URI
257
+ def to_rdf
258
+ subject
259
+ end
260
+
261
+ private
262
+
263
+ include Kalimba::Callbacks
264
+ include Kalimba::Validations
265
+ end
266
+ end