kalimba 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,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