rdf-mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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