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