rod-rest 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/.gitignore +1 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +69 -0
- data/Rakefile +21 -0
- data/Readme.md +3 -0
- data/lib/rod/rest.rb +14 -0
- data/lib/rod/rest/api.rb +100 -0
- data/lib/rod/rest/client.rb +215 -0
- data/lib/rod/rest/collection_proxy.rb +52 -0
- data/lib/rod/rest/constants.rb +5 -0
- data/lib/rod/rest/exception.rb +8 -0
- data/lib/rod/rest/json_serializer.rb +59 -0
- data/lib/rod/rest/metadata.rb +38 -0
- data/lib/rod/rest/naming.rb +20 -0
- data/lib/rod/rest/property_metadata.rb +20 -0
- data/lib/rod/rest/proxy.rb +121 -0
- data/lib/rod/rest/proxy_factory.rb +30 -0
- data/lib/rod/rest/resource_metadata.rb +32 -0
- data/rod-rest.gemspec +32 -0
- data/test/int/end_to_end.rb +138 -0
- data/test/spec/api.rb +210 -0
- data/test/spec/client.rb +248 -0
- data/test/spec/collection_proxy.rb +109 -0
- data/test/spec/json_serializer.rb +108 -0
- data/test/spec/metadata.rb +37 -0
- data/test/spec/property_metadata.rb +63 -0
- data/test/spec/proxy.rb +87 -0
- data/test/spec/proxy_factory.rb +44 -0
- data/test/spec/resource_metadata.rb +96 -0
- data/test/spec/test_helper.rb +28 -0
- metadata +173 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.bundle
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.2
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git://github.com/apohllo/rod.git
|
3
|
+
revision: 35949856fa6e54980b59c9d1317f99fb2cee71c9
|
4
|
+
tag: v0.7.2.4
|
5
|
+
specs:
|
6
|
+
rod (0.7.2.4)
|
7
|
+
RubyInline (>= 3.10.0, < 4.0.0)
|
8
|
+
activemodel (~> 3.2.2)
|
9
|
+
bsearch (>= 1.5.0, < 1.6.0)
|
10
|
+
english (>= 0.5.0, < 0.6.0)
|
11
|
+
|
12
|
+
PATH
|
13
|
+
remote: .
|
14
|
+
specs:
|
15
|
+
rod-rest (0.0.1)
|
16
|
+
faraday
|
17
|
+
rod
|
18
|
+
sinatra
|
19
|
+
|
20
|
+
GEM
|
21
|
+
remote: http://rubygems.org/
|
22
|
+
specs:
|
23
|
+
RubyInline (3.12.2)
|
24
|
+
ZenTest (~> 4.3)
|
25
|
+
ZenTest (4.9.5)
|
26
|
+
activemodel (3.2.17)
|
27
|
+
activesupport (= 3.2.17)
|
28
|
+
builder (~> 3.0.0)
|
29
|
+
activesupport (3.2.17)
|
30
|
+
i18n (~> 0.6, >= 0.6.4)
|
31
|
+
multi_json (~> 1.0)
|
32
|
+
bsearch (1.5.0)
|
33
|
+
builder (3.0.4)
|
34
|
+
diff-lcs (1.2.4)
|
35
|
+
english (0.5.0)
|
36
|
+
faraday (0.9.0)
|
37
|
+
multipart-post (>= 1.2, < 3)
|
38
|
+
i18n (0.6.9)
|
39
|
+
multi_json (1.8.4)
|
40
|
+
multipart-post (2.0.0)
|
41
|
+
rack (1.5.2)
|
42
|
+
rack-protection (1.5.2)
|
43
|
+
rack
|
44
|
+
rack-test (0.6.2)
|
45
|
+
rack (>= 1.0)
|
46
|
+
rr (1.1.2)
|
47
|
+
rspec (2.14.1)
|
48
|
+
rspec-core (~> 2.14.0)
|
49
|
+
rspec-expectations (~> 2.14.0)
|
50
|
+
rspec-mocks (~> 2.14.0)
|
51
|
+
rspec-core (2.14.5)
|
52
|
+
rspec-expectations (2.14.2)
|
53
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
54
|
+
rspec-mocks (2.14.3)
|
55
|
+
sinatra (1.4.4)
|
56
|
+
rack (~> 1.4)
|
57
|
+
rack-protection (~> 1.4)
|
58
|
+
tilt (~> 1.3, >= 1.3.4)
|
59
|
+
tilt (1.4.1)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
rack-test
|
66
|
+
rod!
|
67
|
+
rod-rest!
|
68
|
+
rr
|
69
|
+
rspec
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
task :default => ["test:spec", "test:int"]
|
2
|
+
|
3
|
+
namespace :test do
|
4
|
+
desc "Specs"
|
5
|
+
task :spec do
|
6
|
+
sh "rspec test/spec/api.rb"
|
7
|
+
sh "rspec test/spec/client.rb"
|
8
|
+
sh "rspec test/spec/proxy.rb"
|
9
|
+
sh "rspec test/spec/collection_proxy.rb"
|
10
|
+
sh "rspec test/spec/json_serializer.rb"
|
11
|
+
sh "rspec test/spec/metadata.rb"
|
12
|
+
sh "rspec test/spec/resource_metadata.rb"
|
13
|
+
sh "rspec test/spec/property_metadata.rb"
|
14
|
+
sh "rspec test/spec/proxy_factory.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Integration tests"
|
18
|
+
task :int do
|
19
|
+
sh "rspec test/int/end_to_end.rb"
|
20
|
+
end
|
21
|
+
end
|
data/Readme.md
ADDED
data/lib/rod/rest.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
require 'rod/rest/naming'
|
4
|
+
require 'rod/rest/api'
|
5
|
+
require 'rod/rest/client'
|
6
|
+
require 'rod/rest/proxy'
|
7
|
+
require 'rod/rest/collection_proxy'
|
8
|
+
require 'rod/rest/constants'
|
9
|
+
require 'rod/rest/exception'
|
10
|
+
require 'rod/rest/json_serializer'
|
11
|
+
require 'rod/rest/metadata'
|
12
|
+
require 'rod/rest/property_metadata'
|
13
|
+
require 'rod/rest/resource_metadata'
|
14
|
+
require 'rod/rest/proxy_factory'
|
data/lib/rod/rest/api.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'rod/rest/naming'
|
3
|
+
|
4
|
+
module Rod
|
5
|
+
module Rest
|
6
|
+
class API < Sinatra::Base
|
7
|
+
class << self
|
8
|
+
include Naming
|
9
|
+
|
10
|
+
# Build API for a given +resource+.
|
11
|
+
# Options:
|
12
|
+
# * +:resource_name+ - the name of the resource (resource.name by default)
|
13
|
+
# * +:serializer+ - the serializer used to serialize the ROD objects
|
14
|
+
# (instance of JsonSerializer by default)
|
15
|
+
def build_api_for(resource,options={})
|
16
|
+
serializer = options[:serializer] || JsonSerializer.new
|
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
|
+
|
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
|
44
|
+
|
45
|
+
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
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Build metadata API for the given +metadata+.
|
75
|
+
# Options:
|
76
|
+
# * +:serializer+ - the serializer used to serialize the ROD objects
|
77
|
+
def build_metadata_api(metadata,options={})
|
78
|
+
serializer = options[:serializer] || JSON
|
79
|
+
get "/metadata" do
|
80
|
+
serializer.dump(metadata)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Start the API for the +database+.
|
85
|
+
# Options:
|
86
|
+
# * +resource_serializer+ - serializer used for resources
|
87
|
+
# * +metadata_serializer+ - serializer used for metadata
|
88
|
+
# +web_options+ are passed to Sinatra run! method.
|
89
|
+
def start_with_database(database,options={},web_options={})
|
90
|
+
build_metadata_api(database.metadata,serializer: options[:metadata_serializer])
|
91
|
+
database.send(:classes).each do |resource|
|
92
|
+
next if database.special_class?(resource)
|
93
|
+
build_api_for(resource,serializer: options[:resource_serializer])
|
94
|
+
end
|
95
|
+
run!(web_options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'active_model/naming'
|
2
|
+
|
3
|
+
require 'rod/rest/exception'
|
4
|
+
require 'rod/rest/naming'
|
5
|
+
|
6
|
+
module Rod
|
7
|
+
module Rest
|
8
|
+
class Client
|
9
|
+
include Naming
|
10
|
+
|
11
|
+
# Options:
|
12
|
+
# * http_client - library used to talk via HTTP (e.g. Faraday)
|
13
|
+
# * parser - parser used to parse the incoming data (JSON by default)
|
14
|
+
# * factory - factory class used to build the proxy objects
|
15
|
+
# * url_encoder - encoder used to encode URL strings (CGI by default)
|
16
|
+
# * metadata - metadata describing the remote database (optional - it is
|
17
|
+
# retrieved via the API if not given; in that case metadata_factory must
|
18
|
+
# be provided).
|
19
|
+
# * metadata_factory - factory used to build the metadata (used only if
|
20
|
+
# metadata was not provided).
|
21
|
+
def initialize(options={})
|
22
|
+
@web_client = options.fetch(:http_client)
|
23
|
+
@parser = options[:parser] || JSON
|
24
|
+
@proxy_factory_class = options[:factory] || ProxyFactory
|
25
|
+
@url_encoder = options[:url_encoder] || CGI
|
26
|
+
|
27
|
+
@metadata = options[:metadata]
|
28
|
+
if @metadata
|
29
|
+
configure_with_metadata(@metadata)
|
30
|
+
else
|
31
|
+
@metadata_factory = options[:metadata_factory] || Metadata
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the Database metadata.
|
36
|
+
def metadata
|
37
|
+
return @metadata unless @metadata.nil?
|
38
|
+
@metadata = fetch_metadata
|
39
|
+
configure_with_metadata(@metadata)
|
40
|
+
@metadata
|
41
|
+
end
|
42
|
+
|
43
|
+
# Fetch the object from the remote API. The method requires the stub of
|
44
|
+
# the object to be proviede, i.e. a hash containing its +rod_id+ and
|
45
|
+
# +type+, e.g. {rod_id: 1, type: "Car"}.
|
46
|
+
def fetch_object(object_stub)
|
47
|
+
check_stub(object_stub)
|
48
|
+
check_method(object_stub)
|
49
|
+
__send__(primary_finder_method_name(object_stub[:type]),object_stub[:rod_id])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Fetch object related via the association to the +subject+.
|
53
|
+
# The association name is +association_name+ and the object returned is
|
54
|
+
# the +index+-th element in the collection.
|
55
|
+
def fetch_related_object(subject,association_name,index)
|
56
|
+
check_subject_and_association(subject,association_name)
|
57
|
+
__send__(association_method_name(subject.type,association_name),subject.rod_id,index)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Overrided in order to fetch the metadata when it was not provided in the
|
61
|
+
# constructor.
|
62
|
+
def method_missing(*args)
|
63
|
+
unless @metadata.nil?
|
64
|
+
super
|
65
|
+
end
|
66
|
+
@metadata = fetch_metadata
|
67
|
+
configure_with_metadata(@metadata)
|
68
|
+
self.send(*args)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def fetch_metadata
|
73
|
+
response = @web_client.get(metadata_path())
|
74
|
+
if response.status != 200
|
75
|
+
raise APIError.new(no_metadata_error())
|
76
|
+
end
|
77
|
+
@metadata = @metadata_factory.new(description: response.body)
|
78
|
+
end
|
79
|
+
|
80
|
+
def configure_with_metadata(metadata)
|
81
|
+
define_counters(metadata)
|
82
|
+
define_finders(metadata)
|
83
|
+
define_relations(metadata)
|
84
|
+
@factory = @proxy_factory_class.new(metadata.resources,self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def define_counters(metadata)
|
88
|
+
metadata.resources.each do |resource|
|
89
|
+
self.define_singleton_method("#{plural_resource_name(resource)}_count") do
|
90
|
+
get_parsed_response(resource_path(resource))[:count]
|
91
|
+
end
|
92
|
+
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]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def define_finders(metadata)
|
101
|
+
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)))
|
104
|
+
end
|
105
|
+
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) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def define_relations(metadata)
|
114
|
+
metadata.resources.each do |resource|
|
115
|
+
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)))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_parsed_response(path)
|
124
|
+
result = @web_client.get(path)
|
125
|
+
check_status(result,path)
|
126
|
+
@parser.parse(result.body,symbolize_names: true)
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_status(response,path)
|
130
|
+
case response.status
|
131
|
+
when 200
|
132
|
+
return
|
133
|
+
when 404
|
134
|
+
raise MissingResource.new(path)
|
135
|
+
else
|
136
|
+
raise APIError.new(path)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def check_stub(object_stub)
|
141
|
+
unless object_stub.has_key?(:rod_id) && object_stub.has_key?(:type)
|
142
|
+
raise APIError.new(invalid_stub_error(object_stub))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
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])))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
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)))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def resource_path(resource)
|
159
|
+
"/#{plural_resource_name(resource)}"
|
160
|
+
end
|
161
|
+
|
162
|
+
def primary_resource_finder_path(resource,id)
|
163
|
+
"/#{plural_resource_name(resource)}/#{id}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def resource_finder_path(resource,property_name,value)
|
167
|
+
"/#{plural_resource_name(resource)}#{finder_query(property_name,value)}"
|
168
|
+
end
|
169
|
+
|
170
|
+
def association_count_path(resource,id,association_name)
|
171
|
+
"/#{plural_resource_name(resource)}/#{id}/#{association_name}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def association_path(resource,association_name,id,index)
|
175
|
+
"/#{plural_resource_name(resource)}/#{id}/#{association_name}/#{index}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def metadata_path
|
179
|
+
"/metadata"
|
180
|
+
end
|
181
|
+
|
182
|
+
def primary_finder_method_name(resource)
|
183
|
+
"find_#{singular_resource_name(resource)}"
|
184
|
+
end
|
185
|
+
|
186
|
+
def finder_method_name(resource,property_name)
|
187
|
+
"find_#{plural_resource_name(resource)}_by_#{property_name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
def association_count_method_name(resource,association_name)
|
191
|
+
"#{singular_resource_name(resource)}_#{association_name}_count"
|
192
|
+
end
|
193
|
+
|
194
|
+
def association_method_name(resource,association_name)
|
195
|
+
"#{singular_resource_name(resource)}_#{association_name.to_s.singularize}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def finder_query(property_name,value)
|
199
|
+
"?#{@url_encoder.escape(property_name)}=#{@url_encoder.escape(value)}"
|
200
|
+
end
|
201
|
+
|
202
|
+
def invalid_stub_error(object_stub)
|
203
|
+
"The object stub is invalid: #{object_stub}"
|
204
|
+
end
|
205
|
+
|
206
|
+
def invalid_method_error(plural_name)
|
207
|
+
"The API doesn't have the method '#{plural_name}'"
|
208
|
+
end
|
209
|
+
|
210
|
+
def no_metadata_error
|
211
|
+
"The API doesn't provide metadata."
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rod/rest/exception'
|
2
|
+
|
3
|
+
module Rod
|
4
|
+
module Rest
|
5
|
+
class CollectionProxy
|
6
|
+
include Enumerable
|
7
|
+
attr_reader :size
|
8
|
+
|
9
|
+
# Initializes a CollectionPorxy.
|
10
|
+
# * +:proxy+ - the object this collection belongs to
|
11
|
+
# * +:association_name+ - the name of proxie's plural association this collection is returned for
|
12
|
+
# * +:size+ - the size of the collection
|
13
|
+
# * +:client+ - the REST API client
|
14
|
+
def initialize(proxy,association_name,size,client)
|
15
|
+
@proxy = proxy
|
16
|
+
@association_name = association_name
|
17
|
+
@size = size
|
18
|
+
@client = client
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns true if the collection is empty (i.e. its size == 0).
|
22
|
+
def empty?
|
23
|
+
self.size == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the index-th element of the collection.
|
27
|
+
def [](index)
|
28
|
+
begin
|
29
|
+
@client.fetch_related_object(@proxy,@association_name,index)
|
30
|
+
rescue MissingResource
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the last element of the collection.
|
36
|
+
def last
|
37
|
+
size > 0 ? self[size - 1] : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterates over the elements of the collection.
|
41
|
+
def each
|
42
|
+
if block_given?
|
43
|
+
@size.times do |index|
|
44
|
+
yield self[index]
|
45
|
+
end
|
46
|
+
else
|
47
|
+
enum_for(:each)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|