rod-rest 0.0.1.1 → 0.5.0

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/.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