rdf-mapper 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.
data/README.rdoc ADDED
@@ -0,0 +1,188 @@
1
+ = RDFMapper -- Object-relation mapping for RDF data
2
+
3
+ RDFMapper is an ORM[http://en.wikipedia.org/wiki/Object-relational_mapping]
4
+ written in Ruby that is designed to play nicely with RDF data.
5
+
6
+ == Features
7
+
8
+ - 100% Ruby code based on a slim & smart {RDF.rb}[http://rdf.rubyforge.org/] library
9
+ - All the usual Rails methods: find, create, belongs_to, has_many -- you name it
10
+ - Built with performance in mind: all objects are lazy-loaded by default
11
+ - Supports REST, SPARQL and ActiveRecord as RDF data sources
12
+ - Supports XML, N-Triples and JSON out of the box
13
+
14
+ == Installation
15
+
16
+ The prefered method of installing RDFMapper is through its gem file (requires
17
+ RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl]):
18
+
19
+ % [sudo] gem install rdf-mapper
20
+
21
+ The latest version of RDFMapper can be found at
22
+
23
+ * http://github.com/42cities/rdf-mapper
24
+
25
+
26
+ == Contribute
27
+
28
+ Please note that RDFMapper in under heavy development right now, it's not yet
29
+ production safe. Any contribution (bug tickets, code patches) is more than
30
+ welcome. Email us at team@42cities.com or submit a ticket on
31
+ GitHub[http://github.com/42cities/rdf-mapper/issues]
32
+
33
+
34
+ = 5-minute crash course
35
+
36
+ === Idea behind RDF models
37
+
38
+ Models in RDFMapper are essentially RDF nodes that have an ID and at least one triple
39
+ with an rdf:type predicate. Consider the following example:
40
+
41
+ <http://example.org/people/237643> rdf:type <http://www.example.org/schema#Person>
42
+ <http://example.org/people/237643> example:name "John Smith"
43
+ <http://example.org/people/237643> example:age "27"^^xsd:integer
44
+
45
+ This set of triples defines a node (with an ID of <http://example.org/people/237643>)
46
+ that has three 'attributes': `example:name`, `example:age`, and `rdf:type`. Now `rdf:type`
47
+ predicate tells us that there's a class (<http://www.example.org/schema#Person>)
48
+ with more or less predefined behavior. And our node (<http://example.org/people/237643>)
49
+ is an instance of that class. We could replicate the same logic in Ruby:
50
+
51
+ class Person
52
+ attr_accessor :id
53
+ attr_accessor :name
54
+ attr_accessor :age
55
+ end
56
+
57
+ person = Person.new
58
+ person.id = "http://example.org/people/237643"
59
+ person.name = "John Smith"
60
+ person.age = 27
61
+
62
+ That's essentially what RDFMapper does. It accepts RDF triples (XML or N-triples),
63
+ creates instances, assigns attributes and binds models together (via Rails-like
64
+ belongs_to and has_many associations).
65
+
66
+
67
+ === Defining a model
68
+
69
+ Before you start working with RDFMapper, you need to define at least one model. The only
70
+ required setting is its namespace (think XML namespace) or type (think rdf:type). If you
71
+ specify the namespace, it will be used by the model itself (to figure out its rdf:type)
72
+ and by its attributes (to figure out RDF predicates).
73
+
74
+ class Person < RDFMapper::Model
75
+ namespace "http://example.org/#"
76
+ attribute :name, :type => :text
77
+ attribute :homepage, :type => :uri, :predicate => 'http://xmlns.com/foaf/0.1/homepage'
78
+ end
79
+
80
+ Person.namespace #=> #<RDF::Vocabulary(http://example.org/#)>
81
+ Person.type #=> #<RDF::URI(http://example.org/#Person)>
82
+
83
+ Person.name.type #=> #<RDF::URI(http://example.org/#name")>
84
+ Person.homepage.type #=> #<RDF::URI(http://xmlns.com/foaf/0.1/homepage)>
85
+
86
+ For more information on {RDF::URI}[http://rdf.rubyforge.org/RDF/URI.html],
87
+ {RDF::Vocabulary}[http://rdf.rubyforge.org/RDF/Vocabulary.html] and other
88
+ classes within RDF namespace, refer to {RDF.rb documentation}[http://rdf.rubyforge.org/].
89
+
90
+
91
+ === Defining the data source
92
+
93
+ By this moment you can work with RDFMapper models with no additional settings.
94
+ However, if you want to load, save and search for your objects, you need to
95
+ specify their data source. RDFMapper comes with 3 different flavors of data
96
+ sources: REST, SPARQL and Rails.
97
+
98
+ * SPARQL [read-only] -- the standard for RDF data.
99
+ RDFMapper will query specified SPARQL server over HTTP using standard SPARQL
100
+ syntax. Currently it supports only a few functions (no subqueries, updates,
101
+ aggregates, etc.)
102
+
103
+ * REST [read-only] -- good old HTTP-based data
104
+ storage. It assumes that an object's ID (which is an URI) is the place to
105
+ look when you want to get object's properties. For example, if an object has
106
+ an ID `http://example.org/people/237643`, RDFMapper will download data from
107
+ this address and parse any RDF triples it finds along the way.
108
+
109
+ * Rails [read/write] -- gets the data from an
110
+ ActiveRecord model (that is Rails model). This adapter assumes an RDFMapper
111
+ model has a 'mirror' ActiveRecord model with the same attributes and
112
+ associations.
113
+
114
+ Assigning data source to a model is easy:
115
+
116
+ class Person < RDFMapper::Model
117
+ adapter :rails # There should be a `Person` class that subclasses ActiveRecord::Base
118
+ end
119
+
120
+ class Person < RDFMapper
121
+ adapter :rails, :class_name => 'Employee' # ActiveRecord::Base model is called `Employee`
122
+ end
123
+
124
+ class Person < RDFMapper
125
+ adapter :sparql, {
126
+ :server => 'http://some-sparql-server.com'
127
+ :headers => { 'API-Key' => '89d7sfd9sfs' }
128
+ }
129
+ end
130
+
131
+
132
+ === Searching
133
+
134
+ If you search objects by an ID, it's up to the adapter (REST, SPARQL, or Rails) to
135
+ decide what type of ID it requires (an URI, a database column or something else).
136
+ Check out the documentation for each adapter to see how works.
137
+
138
+ Person.all #=> #<PersonCollection:23784623>
139
+ Person.find('132987') #=> #<Person:217132856>
140
+ Person.find(:all, :conditions => { :name => 'John' }) #=> #<PersonCollection:32462387>
141
+
142
+ Note, the objects above are not loaded. RDFMapper will load them once you
143
+ access an attribute of a collection or an object. The following 3 objects are
144
+ loaded instantly, since RDFMapper needs to figure out what their attributes are
145
+ (in this case `nil?`, `name` and `length`).
146
+
147
+ Person.find('132987').nil? #=> false
148
+ Person.find('132987').name #=> "John"
149
+ Person.find(:all, :conditions => { :name => 'John' }).length #=> 3
150
+
151
+ You should take extra care when dealing with lazy-loaded models, since
152
+ exceptions may occur when a model is not found:
153
+
154
+ Person.find('132987') #=> #<Person:217132856>
155
+ Person.find('132987').name #=> NoMethodError: undefined method `name' for nil:NilClass
156
+
157
+ Instead, you should first check if a model exists:
158
+
159
+ @person = Person.find('132987')
160
+ @person.name unless @person.nil?
161
+
162
+
163
+ === Working with attributes
164
+
165
+ Attributes in RDFMapper work just as you would expect them to work with just one
166
+ small exception. Since any attribute of a model is essentially an RDF triple, you
167
+ can access attributes by their predicates as well:
168
+
169
+ class Person < RDFMapper::Model
170
+ namespace "http://example.org/#"
171
+ attribute :name, :type => :text
172
+ attribute :homepage, :type => :uri, :predicate => 'http://xmlns.com/foaf/0.1/homepage'
173
+ end
174
+
175
+ instance = Person.new
176
+ instance.name #=> "John Smith"
177
+ instance[:name] #=> "John Smith"
178
+ instance['http://example.org/#name'] #=> "John Smith"
179
+ instance.homepage #=> #<RDF::URI(http://johnsmith.com/")>
180
+ instance['http://xmlns.com/foaf/0.1/homepage'] #=> #<RDF::URI(http://johnsmith.com/")>
181
+
182
+
183
+ That's pretty much all you need to know. Go try and let us know what you think!
184
+
185
+ == License
186
+
187
+ RDFMapper is free and unencumbered public domain software. For more information,
188
+ see http://unlicense.org or the accompanying UNLICENSE file.
data/UNLICENSE ADDED
@@ -0,0 +1,25 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
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 NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, contact Alex Serebryakov [serebryakov@gmail.com]
25
+ or visit <http://unlicense.org/>
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,83 @@
1
+ module RDFMapper
2
+ module Adapters
3
+
4
+ autoload :Rails, 'lib/adapters/rails'
5
+ autoload :REST, 'lib/adapters/rest'
6
+ autoload :SPARQL, 'lib/adapters/sparql'
7
+
8
+ ##
9
+ # Instantiates and returns an instance of an adapter.
10
+ #
11
+ # @param [Symbol] name (:rails, :sparql, :rest)
12
+ # @param [Object] cls subclass of RDFMapper::Model
13
+ # @param [Hash] options options to pass on to the adapter constructor
14
+ #
15
+ # @return [Object] instance of an adapter
16
+ ##
17
+ def self.register(name, cls, options = {})
18
+ self[name].new(cls, options)
19
+ end
20
+
21
+ ##
22
+ # Returns adapter's class based on specified `name` (:rails, :sparql, :rest)
23
+ #
24
+ # @return [Object]
25
+ ##
26
+ def self.[](name)
27
+ case name
28
+ when :rails then Rails
29
+ when :sparql then SPARQL
30
+ when :rest then REST
31
+ else raise NameError, 'Adapter `%s` not recognized' % value.inspect
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Parent class for all adapters. Contains default constructor method
37
+ # and interface methods that each adapter should override.
38
+ ##
39
+ class Base
40
+
41
+ ##
42
+ # All adapters implement Logger
43
+ ##
44
+ include RDFMapper::Logger
45
+
46
+ ##
47
+ # Adapter implementation should override this method
48
+ ##
49
+ def load(query)
50
+ raise NotImplementedError, 'Expected adapter to override `load`'
51
+ end
52
+
53
+ ##
54
+ # Adapter implementation should override this method
55
+ ##
56
+ def save(instance)
57
+ raise NotImplementedError, 'Expected adapter to override `save`'
58
+ end
59
+
60
+ ##
61
+ # Adapter implementation should override this method
62
+ ##
63
+ def reload(instance)
64
+ raise NotImplementedError, 'Expected adapter to override `save`'
65
+ end
66
+
67
+ ##
68
+ # Adapter implementation should override this method
69
+ ##
70
+ def update(instance)
71
+ raise NotImplementedError, 'Expected adapter to override `save`'
72
+ end
73
+
74
+ ##
75
+ # Adapter implementation should override this method
76
+ ##
77
+ def create(instance)
78
+ raise NotImplementedError, 'Expected adapter to override `save`'
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,307 @@
1
+ module RDFMapper
2
+ module Adapters
3
+ ##
4
+ # [-]
5
+ ##
6
+ class Rails < Base
7
+
8
+ ##
9
+ # [-]
10
+ ##
11
+ def initialize(cls, options = {})
12
+ @rdf, @options = cls, options
13
+ @options[:skip] ||= []
14
+ @options[:substitute] ||= { }
15
+ @options[:substitute][:id] ||= :uid
16
+ end
17
+
18
+
19
+ ##
20
+ # [-]
21
+ ##
22
+ def load(query)
23
+ @rdf.associations.values.select do |assoc|
24
+ assoc.belongs_to?
25
+ end.map do |assoc|
26
+ assoc.name
27
+ end.reject do |name|
28
+ @options[:skip].include?(name)
29
+ end.each do |name|
30
+ query.include!(name)
31
+ end
32
+ Query.new(query, @options).find
33
+ end
34
+
35
+ ##
36
+ # [-]
37
+ ##
38
+ def save(instance)
39
+ if instance[:rails_id].nil?
40
+ obj = instance.class.find(instance.id.to_s).from(:rails)
41
+ instance[:rails_id] = obj.rails_id unless obj.nil?
42
+ end
43
+ if instance[:rails_id].nil?
44
+ create(instance)
45
+ else
46
+ update(instance)
47
+ end
48
+ end
49
+
50
+ ##
51
+ # [-]
52
+ ##
53
+ def reload(instance)
54
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => { :id => instance.id })
55
+ Query.new(query, @options).find.first
56
+ end
57
+
58
+ ##
59
+ # [-]
60
+ ##
61
+ def update(instance)
62
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => instance.attributes)
63
+ Query.new(query, @options).update
64
+ end
65
+
66
+ ##
67
+ # [-]
68
+ ##
69
+ def create(instance)
70
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => instance.attributes)
71
+ Query.new(query, @options).create
72
+ end
73
+
74
+
75
+ private
76
+
77
+ def check_for_rails_id(instance)
78
+ end
79
+
80
+ class Query
81
+
82
+ include RDFMapper::Logger
83
+
84
+ def initialize(query, options = {})
85
+ @query, @options = query, options
86
+ @rails = (@options[:class_name] || @query.cls.to_s.demodulize).constantize
87
+ setup_replacements
88
+ end
89
+
90
+ ##
91
+ # [-]
92
+ ##
93
+ def update
94
+ record = @rails.update(@query[:rails_id], save_options)
95
+ record_attributes(record)
96
+ end
97
+
98
+ ##
99
+ # [-]
100
+ ##
101
+ def create
102
+ record = @rails.create(save_options)
103
+ record_attributes(record)
104
+ end
105
+
106
+ ##
107
+ # [-]
108
+ ##
109
+ def find
110
+ @query.check(:rails_id)
111
+ #
112
+ debug 'Searching for %s with %s' % [@rails, @query.inspect]
113
+ debug 'Query: %s' % find_options.inspect
114
+ #
115
+ @rails.find(:all, find_options).map do |record|
116
+ record_attributes(record)
117
+ end
118
+ end
119
+
120
+
121
+ private
122
+
123
+ ##
124
+ # [-]
125
+ ##
126
+ def record_attributes(record)
127
+ record_id = [:id, :rails_id].map do |name|
128
+ [name, record_value(record, name)]
129
+ end
130
+ record_props = record_properties(record)
131
+ record_assoc = record_associations(record)
132
+ Hash[record_id + record_props + record_assoc]
133
+ end
134
+
135
+ ##
136
+ # [-]
137
+ ##
138
+ def save_options
139
+ Hash[@query.to_a.map do |condition|
140
+ name = @replace[condition.name]
141
+ [name, validate(condition.value)]
142
+ end.reject do |name, value|
143
+ name.nil? or value.nil?
144
+ end]
145
+ end
146
+
147
+ ##
148
+ # Substitutes names of those attributes specified in `options[:substitute]`
149
+ # and raises a runtime error for attributes that could not be found in the
150
+ # database. Returns an object which can then be used with ActiveRecord::Base.find
151
+ ##
152
+ def find_options #nodoc
153
+ { :conditions => SQL.new(@query, @replace).to_a,
154
+ :order => @query.order,
155
+ :limit => @query.limit,
156
+ :offset => @query.offset,
157
+ :include => @query.include
158
+ }.delete_if { |name, value| value.nil? }
159
+ end
160
+
161
+ ##
162
+ # [-]
163
+ ##
164
+ def setup_replacements #nodoc
165
+ @replace = default_replacements
166
+ @query.flatten.map do |condition|
167
+ # Original RDF name
168
+ rdf_name = condition.name
169
+ # Expected name in the DB
170
+ expected_name = @replace[rdf_name] || rdf_name
171
+ # Silently ignore attributes that are not in the DB
172
+ rails_name = activerecord_attribute?(expected_name)
173
+ @replace[rdf_name] = rails_name unless rails_name.nil?
174
+ end
175
+ end
176
+
177
+ ##
178
+ # [-]
179
+ ##
180
+ def default_replacements
181
+ @options[:substitute].merge({ :rails_id => :id })
182
+ end
183
+
184
+ ##
185
+ # [-]
186
+ ##
187
+ def record_properties(record) #nodoc
188
+ @query.cls.properties.keys.map do |name|
189
+ value = record_value(record, name)
190
+ [name, value]
191
+ end
192
+ end
193
+
194
+ ##
195
+ # [-]
196
+ ##
197
+ def record_associations(record) #nodoc
198
+ @query.include.map do |name|
199
+ value = record_value(record, name)
200
+ value = value.nil? ? nil : value[@replace[:id]]
201
+ [name, value]
202
+ end
203
+ end
204
+
205
+ ##
206
+ # [-]
207
+ ##
208
+ def record_value(record, rdf_name) #nodoc
209
+ name = default_replacements[rdf_name] || rdf_name
210
+ unless record.respond_to?(name)
211
+ nil
212
+ else
213
+ record.send(name)
214
+ end
215
+ end
216
+
217
+ ##
218
+ # [-]
219
+ ##
220
+ def activerecord_attribute?(name) #nodoc
221
+ activerecord_property?(name) || activerecord_association?(name)
222
+ end
223
+
224
+ ##
225
+ # [-]
226
+ ##
227
+ def activerecord_association?(name) #nodoc
228
+ reflection = @rails.reflections[name.to_sym]
229
+ if reflection.nil? or not reflection.belongs_to?
230
+ return nil
231
+ end
232
+ reflection.primary_key_name.to_sym
233
+ end
234
+
235
+ ##
236
+ # [-]
237
+ ##
238
+ def activerecord_property?(name) #nodoc
239
+ @rails.column_names.include?(name.to_s) ? name.to_sym : nil
240
+ end
241
+
242
+ class SQL
243
+
244
+ def initialize(query, replace)
245
+ @query, @replace = query, replace
246
+ @text, @values = [], []
247
+
248
+ @query.to_a.map do |condition|
249
+ if condition.kind_of?(query.class)
250
+ add_query(condition)
251
+ else
252
+ add_condition(condition)
253
+ end
254
+ end
255
+ end
256
+
257
+ def add_query(query)
258
+ child = SQL.new(query, @replace)
259
+ unless child.text.empty?
260
+ @text.push("(%s)" % child.text)
261
+ @values.push(*child.values)
262
+ end
263
+ end
264
+
265
+ def add_condition(condition)
266
+ name = @replace[condition.name]
267
+ if name.nil?
268
+ return nil
269
+ end
270
+ if condition.value.kind_of?(Array)
271
+ @text << "%s IN (?)" % name
272
+ else
273
+ @text << "%s %s ?" % [name, condition.eq]
274
+ end
275
+ @values << validate(condition.value)
276
+ end
277
+
278
+ def validate(value) #nodoc
279
+ if value.kind_of? Array
280
+ return value.map do |item|
281
+ validate(item)
282
+ end
283
+ end
284
+ if value.kind_of? RDFMapper::Model
285
+ value[:rails_id]
286
+ else
287
+ value
288
+ end
289
+ end
290
+
291
+ def text
292
+ @text.join(' %s ' % @query.modifier)
293
+ end
294
+
295
+ def values
296
+ @values
297
+ end
298
+
299
+ def to_a
300
+ [text] + values
301
+ end
302
+
303
+ end # SQL
304
+ end # Query
305
+ end # Rails
306
+ end # Adapters
307
+ end # RDFMapper
@@ -0,0 +1,45 @@
1
+ module RDFMapper
2
+ module Adapters
3
+ ##
4
+ # Not yet implemented
5
+ ##
6
+ class REST < Base
7
+
8
+ ##
9
+ # @todo. Not implemented
10
+ ##
11
+ def load(query)
12
+ raise NotImplementedError, 'REST adapter is not yet implemented'
13
+ end
14
+
15
+ ##
16
+ # @todo. Not implemented
17
+ ##
18
+ def save(instance)
19
+ raise NotImplementedError, 'REST adapter is not yet implemented'
20
+ end
21
+
22
+ ##
23
+ # @todo. Not implemented
24
+ ##
25
+ def reload(instance)
26
+ raise NotImplementedError, 'REST adapter is not yet implemented'
27
+ end
28
+
29
+ ##
30
+ # @todo. Not implemented
31
+ ##
32
+ def update(instance)
33
+ raise NotImplementedError, 'REST adapter is not yet implemented'
34
+ end
35
+
36
+ ##
37
+ # @todo. Not implemented
38
+ ##
39
+ def create(instance)
40
+ raise NotImplementedError, 'REST adapter is not yet implemented'
41
+ end
42
+
43
+ end # REST
44
+ end # Adapters
45
+ end # RDFMapper