rod-rest 0.0.1.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  .bundle
2
+ pkg
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- rod-rest (0.0.1)
15
+ rod-rest (0.0.1.1)
16
16
  faraday
17
17
  rod
18
18
  sinatra
data/Readme.md CHANGED
@@ -1,3 +1,113 @@
1
1
  # rod-rest
2
2
 
3
3
  REST API for [Ruby Object Database](https://github.com/apohllo/rod)
4
+
5
+
6
+ ## Server
7
+
8
+ Starting the server is as simple as:
9
+
10
+ ```ruby
11
+ SomeDatabase.instance.open_database("path/to/rod/database")
12
+ Rod::Rest::API.start_with_database(SomeDatabase.instance)
13
+ ```
14
+
15
+ It starts Sinatra application listening by default on port 4567.
16
+
17
+ ## Client
18
+
19
+ The client requires a `http_client` to be passed to the constructor. We
20
+ recommend Faraday, e.g.
21
+
22
+ ```ruby
23
+ faraday = Faraday.new(url: "http://localhost:4567")
24
+ client = Rod::Rest::Client.new(http_client: faraday)
25
+ ```
26
+
27
+ The client automatically fetches metadata, so there is no need to set it up.
28
+ Assuming you have the following Rod classes defined on the server side:
29
+
30
+ ```ruby
31
+ class Person < Rod::Model
32
+ field :name, :string, index: :hash
33
+ field :surname, :string, index: :hash
34
+ end
35
+
36
+ class Car < Rod::Model
37
+ field :brand, :string, index: :hash
38
+ has_one :owner, class_name: "Person"
39
+ has_many :drivers, class_name: "Person"
40
+ end
41
+ ```
42
+
43
+
44
+ The client provides the following calls:
45
+
46
+ ```ruby
47
+ # return people count
48
+ client.people_count()
49
+
50
+ # find person by ROD id
51
+ client.find_person(1)
52
+
53
+ # find several people by their ROD ids
54
+ client.find_people(1,2,3)
55
+ # or
56
+ client.find_people(1..3)
57
+
58
+ # find people by name
59
+ client.find_people_by_name("Albert")
60
+
61
+ # find people by surname
62
+ client.find_people_by_surname("Einstein")
63
+
64
+ # return cars count
65
+ client.cars_count()
66
+
67
+ # find cars by brand
68
+ car = client.find_cars_by_brand("Mercedes").first
69
+ car.owner # returns proxy to singular association
70
+ car.drivers # returns collection proxy
71
+ car.drivers.each do |driver|
72
+ puts driver.name
73
+ end
74
+
75
+ puts car.drivers.first.name
76
+
77
+ car.drivers[1..2].each do |driver| # negative indices are not yet supported
78
+ puts driver.surname
79
+ end
80
+ ```
81
+
82
+ There are also some more low-level API calls supported, by usually when you get the
83
+ first object of some larger graph, there is no need to use them.
84
+
85
+
86
+ ## License
87
+
88
+ (The MIT/X11 License)
89
+
90
+ Copyright (c) 2014 Aleksander Pohl
91
+
92
+ Permission is hereby granted, free of charge, to any person obtaining
93
+ a copy of this software and associated documentation files (the
94
+ 'Software'), to deal in the Software without restriction, including
95
+ without limitation the rights to use, copy, modify, merge, publish,
96
+ distribute, sublicense, and/or sell copies of the Software, and to
97
+ permit persons to whom the Software is furnished to do so, subject to
98
+ the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be
101
+ included in all copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
104
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
105
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
106
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
107
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
108
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
109
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
110
+
111
+ ## Feedback
112
+
113
+ * mailto:apohllo@o2.pl
@@ -0,0 +1,11 @@
1
+ 0.5.0
2
+ #inspect & #to_s for major classes
3
+ ProxyCache
4
+ Improved readme
5
+ Caching in CollectionProxy
6
+ Support for index ranges and collections
7
+ Support for id ranges and collections
8
+ 0.0.1.1
9
+ Fix Ruby restriction in gemspec
10
+ 0.0.1
11
+ Working version of the API
@@ -12,3 +12,4 @@ require 'rod/rest/metadata'
12
12
  require 'rod/rest/property_metadata'
13
13
  require 'rod/rest/resource_metadata'
14
14
  require 'rod/rest/proxy_factory'
15
+ require 'rod/rest/proxy_cache'
@@ -15,59 +15,13 @@ module Rod
15
15
  def build_api_for(resource,options={})
16
16
  serializer = options[:serializer] || JsonSerializer.new
17
17
  resource_name = options[:resource_name] || plural_resource_name(resource)
18
- get "/#{resource_name}" do
19
- if params.empty?
20
- serializer.serialize({count: resource.count})
21
- elsif params.size == 1
22
- name, value = params.first
23
- if resource.respond_to?("find_all_by_#{name}")
24
- serializer.serialize(resource.send("find_all_by_#{name}",value))
25
- else
26
- status 404
27
- serializer.serialize(nil)
28
- end
29
- else
30
- status 404
31
- serializer.serialize(nil)
32
- end
33
- end
34
18
 
35
- get "/#{resource_name}/:id" do
36
- object = resource.find_by_rod_id(params[:id].to_i)
37
- if object
38
- serializer.serialize(object)
39
- else
40
- status 404
41
- serializer.serialize(nil)
42
- end
43
- end
19
+ define_index(resource,resource_name,serializer)
20
+ define_show(resource,resource_name,serializer)
44
21
 
45
22
  resource.plural_associations.each do |property|
46
- get "/#{resource_name}/:id/#{property.name}" do
47
- object = resource.find_by_rod_id(params[:id].to_i)
48
- if object
49
- serializer.serialize({count: object.send("#{property.name}_count") })
50
- else
51
- status 404
52
- serializer.serialize(nil)
53
- end
54
- end
55
-
56
- get "/#{resource_name}/:id/#{property.name}/:index" do
57
- object = resource.find_by_rod_id(params[:id].to_i)
58
- if object
59
- related_object = object.send(property.name)[params[:index].to_i]
60
- if related_object
61
- serializer.serialize(related_object)
62
- else
63
- status 404
64
- serializer.serialize(nil)
65
- end
66
- else
67
- status 404
68
- serializer.serialize(nil)
69
- end
70
- end
23
+ define_association_index(resource,resource_name,property,serializer)
24
+ define_association_show(resource,resource_name,property,serializer)
71
25
  end
72
26
  end
73
27
 
@@ -94,6 +48,150 @@ module Rod
94
48
  end
95
49
  run!(web_options)
96
50
  end
51
+
52
+ protected
53
+ # GET /cars
54
+ # GET /cars?name=Mercedes
55
+ def define_index(resource,resource_name,serializer)
56
+ get index_path(resource_name) do
57
+ case params.size
58
+ when 0
59
+ respond_with_count(resource,serializer)
60
+ when 1
61
+ index_name, searched_value = params.first
62
+ respond_with_indexed_resource(resource,index_name,searched_value,serializer)
63
+ else
64
+ respond_with_nil(serializer)
65
+ end
66
+ end
67
+ end
68
+
69
+ # GET /cars/1
70
+ # GET /cars/1..3
71
+ # GET /cars/1,2,3
72
+ def define_show(resource,resource_name,serializer)
73
+ get show_path(resource_name) do
74
+ respond_with_resource(params[:id],resource,serializer)
75
+ end
76
+ end
77
+
78
+ # GET /cars/1/drivers
79
+ def define_association_index(resource,resource_name,property,serializer)
80
+ get association_index_path(resource_name,property.name) do
81
+ respond_with_related_count(resource,property.name,params[:id].to_i,serializer)
82
+ end
83
+ end
84
+
85
+ # GET /cars/1/drivers/0
86
+ # GET /cars/1/drivers/0..2
87
+ # GET /cars/1/drivers/0,1,2
88
+ def define_association_show(resource,resource_name,property,serializer)
89
+ get association_show_path(resource_name,property.name) do
90
+ respond_with_related_resource(params[:id].to_i,params[:index],resource,property,serializer)
91
+ end
92
+ end
93
+
94
+ def index_path(resource_name)
95
+ "/#{resource_name}"
96
+ end
97
+
98
+ def show_path(resource_name)
99
+ "/#{resource_name}/:id"
100
+ end
101
+
102
+ def association_index_path(resource_name,property_name)
103
+ "/#{resource_name}/:id/#{property_name}"
104
+ end
105
+
106
+ def association_show_path(resource_name,property_name)
107
+ "/#{resource_name}/:id/#{property_name}/:index"
108
+ end
109
+ end
110
+
111
+ protected
112
+ def respond_with_resource(id_param,resource,serializer)
113
+ id_or_range = extract_elements(id_param)
114
+ result =
115
+ if Integer === id_or_range
116
+ fetch_one(id_or_range,resource)
117
+ else
118
+ fetch_collection(id_or_range,resource)
119
+ end
120
+ serializer.serialize(result)
121
+ end
122
+
123
+ def respond_with_related_resource(id,index_param,resource,property,serializer)
124
+ object = resource.find_by_rod_id(id)
125
+ if object
126
+ index_or_range = extract_elements(index_param)
127
+ result =
128
+ if Integer === index_or_range
129
+ fetch_one_related(index_or_range,object,property)
130
+ else
131
+ fetch_related_collection(index_or_range,object,property)
132
+ end
133
+ serializer.serialize(result)
134
+ else
135
+ respond_with_nil(serializer)
136
+ end
137
+ end
138
+
139
+ def respond_with_count(resource,serializer)
140
+ serializer.serialize({count: resource.count})
141
+ end
142
+
143
+ def respond_with_related_count(resource,property_name,id,serializer)
144
+ object = resource.find_by_rod_id(id)
145
+ if object
146
+ serializer.serialize({count: object.send("#{property_name}_count") })
147
+ else
148
+ respond_with_nil(serializer)
149
+ end
150
+ end
151
+
152
+ def respond_with_indexed_resource(resource,index_name,searched_value,serializer)
153
+ if resource.respond_to?("find_all_by_#{index_name}")
154
+ serializer.serialize(resource.send("find_all_by_#{index_name}",searched_value))
155
+ else
156
+ respond_with_nil(serializer)
157
+ end
158
+ end
159
+
160
+ def fetch_collection(ids,resource)
161
+ ids.map{|id| resource.find_by_rod_id(id) }.compact
162
+ end
163
+
164
+ def fetch_one(id,resource)
165
+ resource.find_by_rod_id(id) || report_not_found
166
+ end
167
+
168
+ def fetch_related_collection(indices,object,property)
169
+ indices.map{|index| object.send(property.name)[index] }.compact
170
+ end
171
+
172
+ def fetch_one_related(index,object,property)
173
+ object.send(property.name)[index] || report_not_found
174
+ end
175
+
176
+ def extract_elements(id)
177
+ case id
178
+ when /^(\d+)\.\.(\d+)/
179
+ ($~[1].to_i..$~[2].to_i)
180
+ when /,/
181
+ id.split(",").map(&:to_i)
182
+ else
183
+ id.to_i
184
+ end
185
+ end
186
+
187
+ def report_not_found
188
+ status 404
189
+ nil
190
+ end
191
+
192
+ def respond_with_nil(serializer)
193
+ report_not_found
194
+ serializer.serialize(nil)
97
195
  end
98
196
  end
99
197
  end
@@ -18,11 +18,18 @@ module Rod
18
18
  # be provided).
19
19
  # * metadata_factory - factory used to build the metadata (used only if
20
20
  # metadata was not provided).
21
+ # * proxy_cache - used to cache proxied objects. By default it is
22
+ # ProxyCache. Might be disabled by passing +nil+.
21
23
  def initialize(options={})
22
24
  @web_client = options.fetch(:http_client)
23
25
  @parser = options[:parser] || JSON
24
26
  @proxy_factory_class = options[:factory] || ProxyFactory
25
27
  @url_encoder = options[:url_encoder] || CGI
28
+ if options.has_key?(:proxy_cache)
29
+ @proxy_cache = options[:proxy_cache]
30
+ else
31
+ @proxy_cache = ProxyCache.new
32
+ end
26
33
 
27
34
  @metadata = options[:metadata]
28
35
  if @metadata
@@ -46,7 +53,7 @@ module Rod
46
53
  def fetch_object(object_stub)
47
54
  check_stub(object_stub)
48
55
  check_method(object_stub)
49
- __send__(primary_finder_method_name(object_stub[:type]),object_stub[:rod_id])
56
+ __send__(primary_finder_method(object_stub[:type]),object_stub[:rod_id])
50
57
  end
51
58
 
52
59
  # Fetch object related via the association to the +subject+.
@@ -54,10 +61,19 @@ module Rod
54
61
  # the +index+-th element in the collection.
55
62
  def fetch_related_object(subject,association_name,index)
56
63
  check_subject_and_association(subject,association_name)
57
- __send__(association_method_name(subject.type,association_name),subject.rod_id,index)
64
+ __send__(association_method(subject.type,association_name),subject.rod_id,index)
65
+ end
66
+
67
+ # Fetch objects related via the association to the +subject+.
68
+ # The association name is +association_name+ and the objects are the
69
+ # objects indicated by the idices. This might be a range or a comma
70
+ # separated list of indices.
71
+ def fetch_related_objects(subject,association_name,*indices)
72
+ check_subject_and_association(subject,association_name)
73
+ __send__(plural_association_method(subject.type,association_name),subject.rod_id,*indices)
58
74
  end
59
75
 
60
- # Overrided in order to fetch the metadata when it was not provided in the
76
+ # Overrided in order to fetch the metadata if it was not provided in the
61
77
  # constructor.
62
78
  def method_missing(*args)
63
79
  unless @metadata.nil?
@@ -68,6 +84,16 @@ module Rod
68
84
  self.send(*args)
69
85
  end
70
86
 
87
+ # Detailed description of the client.
88
+ def inspect
89
+ "Rod::Rest::Client<port: #{@web_client.port}, host: #{@web_client.host}>"
90
+ end
91
+
92
+ # Short description of the client.
93
+ def to_s
94
+ "ROD REST API client"
95
+ end
96
+
71
97
  private
72
98
  def fetch_metadata
73
99
  response = @web_client.get(metadata_path())
@@ -81,17 +107,17 @@ module Rod
81
107
  define_counters(metadata)
82
108
  define_finders(metadata)
83
109
  define_relations(metadata)
84
- @factory = @proxy_factory_class.new(metadata.resources,self)
110
+ @factory = @proxy_factory_class.new(metadata.resources,self,cache: @proxy_cache)
85
111
  end
86
112
 
87
113
  def define_counters(metadata)
88
114
  metadata.resources.each do |resource|
89
- self.define_singleton_method("#{plural_resource_name(resource)}_count") do
90
- get_parsed_response(resource_path(resource))[:count]
115
+ self.define_singleton_method(count_method(resource)) do
116
+ return_count(count_path(resource))
91
117
  end
92
118
  resource.plural_associations.each do |association|
93
- self.define_singleton_method(association_count_method_name(resource,association.name)) do |id|
94
- get_parsed_response(association_count_path(resource,id,association.name))[:count]
119
+ self.define_singleton_method(association_count_method(resource,association.name)) do |id|
120
+ return_count(association_count_path(resource,id,association.name))
95
121
  end
96
122
  end
97
123
  end
@@ -99,12 +125,15 @@ module Rod
99
125
 
100
126
  def define_finders(metadata)
101
127
  metadata.resources.each do |resource|
102
- self.define_singleton_method(primary_finder_method_name(resource)) do |id|
103
- @factory.build(get_parsed_response(primary_resource_finder_path(resource,id)))
128
+ self.define_singleton_method(primary_finder_method(resource)) do |id|
129
+ return_single(primary_resource_finder_path(resource,id))
130
+ end
131
+ self.define_singleton_method(plural_finder_method(resource)) do |*id|
132
+ return_collection(plural_resource_finder_path(resource,id))
104
133
  end
105
134
  resource.indexed_properties.each do |property|
106
- self.define_singleton_method(finder_method_name(resource,property.name)) do |value|
107
- get_parsed_response(resource_finder_path(resource,property.name,value)).map{|hash| @factory.build(hash) }
135
+ self.define_singleton_method(finder_method(resource,property.name)) do |value|
136
+ return_collection(resource_finder_path(resource,property.name,value))
108
137
  end
109
138
  end
110
139
  end
@@ -113,13 +142,28 @@ module Rod
113
142
  def define_relations(metadata)
114
143
  metadata.resources.each do |resource|
115
144
  resource.plural_associations.each do |association|
116
- self.define_singleton_method(association_method_name(resource,association.name)) do |id,index|
117
- @factory.build(get_parsed_response(association_path(resource,association.name,id,index)))
145
+ self.define_singleton_method(association_method(resource,association.name)) do |id,index|
146
+ return_single(association_path(resource,association.name,id,index))
147
+ end
148
+ self.define_singleton_method(plural_association_method(resource,association.name)) do |id,*indices|
149
+ return_collection(plural_association_path(resource,association.name,id,*indices))
118
150
  end
119
151
  end
120
152
  end
121
153
  end
122
154
 
155
+ def return_count(path)
156
+ get_parsed_response(path)[:count]
157
+ end
158
+
159
+ def return_single(path)
160
+ @factory.build(get_parsed_response(path))
161
+ end
162
+
163
+ def return_collection(path)
164
+ get_parsed_response(path).map{|hash| @factory.build(hash) }
165
+ end
166
+
123
167
  def get_parsed_response(path)
124
168
  result = @web_client.get(path)
125
169
  check_status(result,path)
@@ -144,18 +188,18 @@ module Rod
144
188
  end
145
189
 
146
190
  def check_method(object_stub)
147
- unless self.respond_to?(primary_finder_method_name(object_stub[:type]))
148
- raise APIError.new(invalid_method_error(primary_finder_method_name(object_stub[:type])))
191
+ unless self.respond_to?(primary_finder_method(object_stub[:type]))
192
+ raise APIError.new(invalid_method_error(primary_finder_method(object_stub[:type])))
149
193
  end
150
194
  end
151
195
 
152
196
  def check_subject_and_association(subject,association_name)
153
- unless self.respond_to?(association_method_name(subject.type,association_name))
154
- raise APIError.new(invalid_method_error(association_method_name(subject.type,association_name)))
197
+ unless self.respond_to?(association_method(subject.type,association_name))
198
+ raise APIError.new(invalid_method_error(association_method(subject.type,association_name)))
155
199
  end
156
200
  end
157
201
 
158
- def resource_path(resource)
202
+ def count_path(resource)
159
203
  "/#{plural_resource_name(resource)}"
160
204
  end
161
205
 
@@ -163,6 +207,10 @@ module Rod
163
207
  "/#{plural_resource_name(resource)}/#{id}"
164
208
  end
165
209
 
210
+ def plural_resource_finder_path(resource,*ids)
211
+ "/#{plural_resource_name(resource)}/#{convert_path_elements(*ids)}"
212
+ end
213
+
166
214
  def resource_finder_path(resource,property_name,value)
167
215
  "/#{plural_resource_name(resource)}#{finder_query(property_name,value)}"
168
216
  end
@@ -175,26 +223,50 @@ module Rod
175
223
  "/#{plural_resource_name(resource)}/#{id}/#{association_name}/#{index}"
176
224
  end
177
225
 
226
+ def plural_association_path(resource,association_name,id,*indices)
227
+ "/#{plural_resource_name(resource)}/#{id}/#{association_name}/#{convert_path_elements(*indices)}"
228
+ end
229
+
178
230
  def metadata_path
179
231
  "/metadata"
180
232
  end
181
233
 
182
- def primary_finder_method_name(resource)
234
+ def convert_path_elements(*elements)
235
+ if elements.size == 1 && Range === elements.first
236
+ elements.first.to_s
237
+ else
238
+ elements.join(",")
239
+ end
240
+ end
241
+
242
+ def count_method(resource)
243
+ "#{plural_resource_name(resource)}_count"
244
+ end
245
+
246
+ def primary_finder_method(resource)
183
247
  "find_#{singular_resource_name(resource)}"
184
248
  end
185
249
 
186
- def finder_method_name(resource,property_name)
250
+ def plural_finder_method(resource)
251
+ "find_#{plural_resource_name(resource)}"
252
+ end
253
+
254
+ def finder_method(resource,property_name)
187
255
  "find_#{plural_resource_name(resource)}_by_#{property_name}"
188
256
  end
189
257
 
190
- def association_count_method_name(resource,association_name)
258
+ def association_count_method(resource,association_name)
191
259
  "#{singular_resource_name(resource)}_#{association_name}_count"
192
260
  end
193
261
 
194
- def association_method_name(resource,association_name)
262
+ def association_method(resource,association_name)
195
263
  "#{singular_resource_name(resource)}_#{association_name.to_s.singularize}"
196
264
  end
197
265
 
266
+ def plural_association_method(resource,association_name)
267
+ "#{singular_resource_name(resource)}_#{association_name.to_s}"
268
+ end
269
+
198
270
  def finder_query(property_name,value)
199
271
  "?#{@url_encoder.escape(property_name)}=#{@url_encoder.escape(value)}"
200
272
  end