rod-rest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ .bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format doc
@@ -0,0 +1 @@
1
+ 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rod', :git => 'git://github.com/apohllo/rod.git', :tag => 'v0.7.2.4'
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ # rod-rest
2
+
3
+ REST API for [Ruby Object Database](https://github.com/apohllo/rod)
@@ -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'
@@ -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