kalimba 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +132 -0
- data/Rakefile +8 -0
- data/kalimba-redlander.gemspec +19 -0
- data/kalimba.gemspec +22 -0
- data/lib/kalimba.rb +24 -0
- data/lib/kalimba/attribute_assignment.rb +148 -0
- data/lib/kalimba/callbacks.rb +30 -0
- data/lib/kalimba/exceptions.rb +18 -0
- data/lib/kalimba/localized_attributes.rb +36 -0
- data/lib/kalimba/persistence.rb +225 -0
- data/lib/kalimba/railtie.rb +11 -0
- data/lib/kalimba/railties/repository.rake +7 -0
- data/lib/kalimba/reflection.rb +54 -0
- data/lib/kalimba/resource.rb +266 -0
- data/lib/kalimba/validations.rb +69 -0
- data/lib/kalimba/version.rb +3 -0
- data/spec/lib/kalimba/attributes_spec.rb +145 -0
- data/spec/lib/kalimba/callbacks_spec.rb +90 -0
- data/spec/lib/kalimba/persistence_spec.rb +344 -0
- data/spec/lib/kalimba/resource_spec.rb +130 -0
- data/spec/lib/kalimba/validations_spec.rb +25 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/resource_ext.rb +45 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 51492726fdd0d0303d00af8376c1bef0af951d48
|
4
|
+
data.tar.gz: c4bdf88223e9efd0591d447a152b9f16896d0626
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1543ecdd70a064ca87f4328d5e6729297d28cc93174917c2907911512191746b28c0bff95bcff08fe190956522bb6926c515231541e986ce0bc6ccb04af5304e
|
7
|
+
data.tar.gz: 61c0b9a9a146b8da73499ea763b21a02e2405fda113c2dd8ba589479f126d61ea9bffaadda60e17fffe0bf71768732f85a64ddff88b653f86be84ae3278cecc7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Slava Kravchenko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Kalimba
|
2
|
+
|
3
|
+
Kalimba is an clone of ActiveRecord, based on ActiveModel framework.
|
4
|
+
Combined with the raw power of Redlander gem, it introduces the world of Ruby on Rails
|
5
|
+
to the world of RDF, triple storages, LinkedData and Semantic Web.
|
6
|
+
The resources of semantic graph storages become accessible in a customary form of "models".
|
7
|
+
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'kalimba'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install kalimba
|
22
|
+
|
23
|
+
|
24
|
+
## Backends
|
25
|
+
|
26
|
+
You won't be able to do much without a backend to handle your RDF statements.
|
27
|
+
Please add "kalimba-redlander" gem dependency to your Gemfile, and make sure
|
28
|
+
to "require 'kalimba-redlander'" before invoking "require 'kalimba'".
|
29
|
+
|
30
|
+
For now, the backends are developed as a part of Kalimba gem for convenience.
|
31
|
+
However, you are free to develop your own backend as a separate gem.
|
32
|
+
|
33
|
+
|
34
|
+
### Kalimba::Persistence::Redlander
|
35
|
+
|
36
|
+
Redlander adapter for [Kalimba](https://github.com/cordawyn/kalimba). It provides the RDF storage backend for Kalimba.
|
37
|
+
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
Your model must be inherited from Kalimba::Resource:
|
42
|
+
|
43
|
+
class Person < Kalimba::Resource
|
44
|
+
# Note that type is *not* inherited,
|
45
|
+
# it must be explicitly declared for every subclass.
|
46
|
+
# And types better be unique!
|
47
|
+
type "http://schema.org/Person"
|
48
|
+
|
49
|
+
# Define base URI for the instances of this resource
|
50
|
+
base_uri "http://example.org/people"
|
51
|
+
|
52
|
+
property :name, :predicate => NS::FOAF["name"], :datatype => NS:XMLSchema["string"]
|
53
|
+
|
54
|
+
has_many :friends, :predicate => "http://schema.org/Person", :datatype => :Person
|
55
|
+
end
|
56
|
+
|
57
|
+
From this point on, you may treat your model just like
|
58
|
+
any fully-fledged clone of ActiveModel (i.e. ActiveRecord model)
|
59
|
+
|
60
|
+
$ alice = Person.new(:name => "Alice")
|
61
|
+
$ alice.valid?
|
62
|
+
$ alice.save!
|
63
|
+
...
|
64
|
+
$ alice.friends << bob
|
65
|
+
|
66
|
+
> Note that Kalimba associations are not fully API-compliant with ActiveRecord associations (yet?).
|
67
|
+
> One major feature missing is "association proxy" which would enable tricks like
|
68
|
+
> `alice.friends.destroy_all`. Presently, Kalimba "associations" return a simple collection (Array).
|
69
|
+
|
70
|
+
For other details refer to YARD documentation for Kalimba::Resource module.
|
71
|
+
|
72
|
+
|
73
|
+
## Regarding RDFS/OWL features
|
74
|
+
|
75
|
+
It should be also noted that "special" features of RDFS/OWL like inverse properties or
|
76
|
+
transitive properties and so on, are *not* specifically handled by Kalimba (or Redlander backend).
|
77
|
+
Availability of any "virtual" data which is supposed to be available as a product of reasoning,
|
78
|
+
is up to the graph storage that you use with the *backend*.
|
79
|
+
|
80
|
+
So (provided that "hasFriend" is "owl:inverseOf" "isFriendOf") you may end with something like this:
|
81
|
+
|
82
|
+
alice.has_friend # => bob
|
83
|
+
bob.is_friend_of # => nil
|
84
|
+
|
85
|
+
... unless your graph storage provides reasoning by default.
|
86
|
+
|
87
|
+
That said, certain graph storages that are said to have reasoning capabilities,
|
88
|
+
do not have reasoning enabled by default (e.g. [Virtuoso](http://virtuoso.openlinksw.com/)),
|
89
|
+
and require that you explicitly enable it using special options or a custom SPARQL syntax.
|
90
|
+
While it is possible to "hack" and modify the options or SPARQL queries that are generated
|
91
|
+
by Kalimba (or its backend), this is not currently available.
|
92
|
+
|
93
|
+
|
94
|
+
## Validations
|
95
|
+
|
96
|
+
For details, refer to ActionModel::Validations documentation.
|
97
|
+
|
98
|
+
class Human < Kalimba::Resource
|
99
|
+
base_uri "http://example.com/people/"
|
100
|
+
property :name, :predicate => NS::FOAF["name"], :datatype => NS:XMLSchema["string"]
|
101
|
+
|
102
|
+
validates_presence_of :name
|
103
|
+
end
|
104
|
+
|
105
|
+
$ bob = Human.create # => bob will have an error on :name
|
106
|
+
|
107
|
+
## Callbacks
|
108
|
+
|
109
|
+
Kalimba supports :before, :after and :around callbacks for :save, :create, :update and
|
110
|
+
:destroy actions.
|
111
|
+
|
112
|
+
class Human < Kalimba::Resource
|
113
|
+
base_uri "http://example.com/people/"
|
114
|
+
|
115
|
+
before_save :shout
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def shout
|
120
|
+
puts "Hey!"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
For details, refer to ActionModel::Callbacks documentation.
|
125
|
+
|
126
|
+
## Contributing
|
127
|
+
|
128
|
+
1. Fork it
|
129
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
130
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
131
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
132
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path('../lib/kalimba/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "kalimba-redlander"
|
5
|
+
gem.version = Kalimba::VERSION
|
6
|
+
gem.authors = ["Slava Kravchenko"]
|
7
|
+
gem.email = ["slava.kravchenko@gmail.com"]
|
8
|
+
gem.description = %q{Redlander adapter for Kalimba. It provides the RDF storage backend for Kalimba.}
|
9
|
+
gem.summary = %q{Redlander adapter for Kalimba}
|
10
|
+
gem.homepage = "https://github.com/cordawyn/kalimba-redlander"
|
11
|
+
|
12
|
+
gem.files = ["lib/kalimba/persistence/redlander.rb"]
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
|
17
|
+
gem.add_runtime_dependency "kalimba"
|
18
|
+
gem.add_runtime_dependency "redlander", "~> 0.6.0"
|
19
|
+
end
|
data/kalimba.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../lib/kalimba/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Slava Kravchenko"]
|
5
|
+
gem.email = ["slava.kravchenko@gmail.com"]
|
6
|
+
gem.description = %q{ActiveModel-based framework, which allows the developer to combine RDF resources into ActiveRecord-like models.}
|
7
|
+
gem.summary = %q{Kalimba provides ActiveRecord-like capabilities for RDF resources.}
|
8
|
+
gem.homepage = "https://github.com/cordawyn/kalimba"
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($\) - ["lib/kalimba/persistence/redlander.rb"]
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "kalimba"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Kalimba::VERSION
|
16
|
+
|
17
|
+
gem.add_runtime_dependency "activemodel", "~> 3.2"
|
18
|
+
gem.add_runtime_dependency "activesupport", "~> 3.2"
|
19
|
+
|
20
|
+
gem.add_development_dependency "rspec", "~> 2.11.0"
|
21
|
+
gem.add_development_dependency "kalimba-redlander"
|
22
|
+
end
|
data/lib/kalimba.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "set"
|
2
|
+
require "active_model" # TODO: not all is required?
|
3
|
+
|
4
|
+
require "kalimba/version"
|
5
|
+
require "kalimba/exceptions"
|
6
|
+
require "kalimba/resource"
|
7
|
+
|
8
|
+
module Kalimba
|
9
|
+
class << self
|
10
|
+
def repository
|
11
|
+
@repository ||= Persistence.repository(@repository_options || {})
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set repository options
|
15
|
+
#
|
16
|
+
# @param [Hash] options options to be passed to the repository constructor
|
17
|
+
# @return [void]
|
18
|
+
def set_repository_options(options = {})
|
19
|
+
@repository_options = options
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require "kalimba/railtie" if defined?(Rails)
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Kalimba
|
2
|
+
module AttributeAssignment
|
3
|
+
# Assign attributes from the given hash
|
4
|
+
#
|
5
|
+
# @param [Hash<[Symbol, String] => Any>] new_attributes
|
6
|
+
# @param [Hash] options
|
7
|
+
# @return [void]
|
8
|
+
def assign_attributes(new_attributes = {}, options = {})
|
9
|
+
return if new_attributes.blank?
|
10
|
+
|
11
|
+
attributes = new_attributes.stringify_keys
|
12
|
+
multi_parameter_attributes = []
|
13
|
+
nested_parameter_attributes = []
|
14
|
+
|
15
|
+
attributes.each do |k, v|
|
16
|
+
if k.include?("(")
|
17
|
+
multi_parameter_attributes << [ k, v ]
|
18
|
+
elsif respond_to?("#{k}=")
|
19
|
+
if v.is_a?(Hash)
|
20
|
+
nested_parameter_attributes << [ k, v ]
|
21
|
+
else
|
22
|
+
send("#{k}=", v)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
raise UnknownAttributeError, "unknown attribute: #{k}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# assign any deferred nested attributes after the base attributes have been set
|
30
|
+
nested_parameter_attributes.each do |k,v|
|
31
|
+
send("#{k}=", v)
|
32
|
+
end
|
33
|
+
|
34
|
+
assign_multiparameter_attributes(multi_parameter_attributes)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
40
|
+
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
41
|
+
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
42
|
+
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
43
|
+
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
|
44
|
+
# f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
|
45
|
+
# attribute will be set to nil.
|
46
|
+
def assign_multiparameter_attributes(pairs)
|
47
|
+
execute_callstack_for_multiparameter_attributes(
|
48
|
+
extract_callstack_for_multiparameter_attributes(pairs)
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute_callstack_for_multiparameter_attributes(callstack)
|
53
|
+
errors = []
|
54
|
+
callstack.each do |name, values_with_empty_parameters|
|
55
|
+
begin
|
56
|
+
send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
|
57
|
+
rescue => ex
|
58
|
+
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
unless errors.empty?
|
62
|
+
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def read_value_from_parameter(name, values_hash_from_param)
|
67
|
+
case self.class.properties[name][:datatype]
|
68
|
+
when NS::XMLSchema["string"]
|
69
|
+
read_other_parameter_value(LocalizedString, name, values_hash_from_param)
|
70
|
+
when NS::XMLSchema["dateTime"], NS::XMLSchema["time"]
|
71
|
+
read_time_parameter_value(name, values_hash_from_param)
|
72
|
+
when NS::XMLSchema["date"]
|
73
|
+
read_date_parameter_value(name, values_hash_from_param)
|
74
|
+
else
|
75
|
+
values_hash_from_param
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_other_parameter_value(klass, name, values_hash_from_param)
|
80
|
+
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
|
81
|
+
values = (1..max_position).collect do |position|
|
82
|
+
raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
|
83
|
+
values_hash_from_param[position]
|
84
|
+
end
|
85
|
+
klass.new(*values)
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
|
89
|
+
[values_hash_from_param.keys.max,upper_cap].min
|
90
|
+
end
|
91
|
+
|
92
|
+
def extract_callstack_for_multiparameter_attributes(pairs)
|
93
|
+
pairs.inject({}) do |attributes, (multiparameter_name, value)|
|
94
|
+
attribute_name = multiparameter_name.split("(").first
|
95
|
+
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
|
96
|
+
|
97
|
+
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
98
|
+
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
|
99
|
+
attributes
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def type_cast_attribute_value(multiparameter_name, value)
|
104
|
+
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_parameter_position(multiparameter_name)
|
108
|
+
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
|
109
|
+
end
|
110
|
+
|
111
|
+
def read_time_parameter_value(name, values_hash_from_param)
|
112
|
+
if values_hash_from_param.size == 2
|
113
|
+
instantiate_time_using_two_fields(name, values_hash_from_param)
|
114
|
+
else
|
115
|
+
instantiate_time_using_many_fields(name, values_hash_from_param)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def read_date_parameter_value(name, values_hash_from_param)
|
120
|
+
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
|
121
|
+
set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
|
122
|
+
begin
|
123
|
+
Date.new(*set_values)
|
124
|
+
rescue ArgumentError # if Date.new raises an exception on an invalid date
|
125
|
+
# we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
126
|
+
Time.new(*set_values).to_date
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def instantiate_time_using_two_fields(name, values_hash_from_param)
|
131
|
+
value = [values_hash_from_param[1], values_hash_from_param[2]].join(" ")
|
132
|
+
Time.parse(value) rescue nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def instantiate_time_using_many_fields(name, values_hash_from_param)
|
136
|
+
# If Date bits were not provided, error
|
137
|
+
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
|
138
|
+
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
|
139
|
+
# If Date bits were provided but blank, then return nil
|
140
|
+
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
|
141
|
+
|
142
|
+
set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
|
143
|
+
# If Time bits are not there, then default to 0
|
144
|
+
(3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
|
145
|
+
Time.new(*set_values)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "active_model/callbacks"
|
2
|
+
|
3
|
+
module Kalimba
|
4
|
+
module Callbacks
|
5
|
+
def destroy
|
6
|
+
run_callbacks :destroy do
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(options = {})
|
12
|
+
run_callbacks :save do
|
13
|
+
persistence_callback_type = new_record? ? :create : :update
|
14
|
+
run_callbacks persistence_callback_type do
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.included(klass)
|
23
|
+
super
|
24
|
+
klass.class_eval do
|
25
|
+
extend ActiveModel::Callbacks
|
26
|
+
define_model_callbacks :create, :update, :destroy, :save
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|