dm-rest-adapter 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/History.txt +7 -1
  2. data/Manifest.txt +9 -87
  3. data/README.markdown +47 -0
  4. data/README.txt +44 -0
  5. data/Rakefile +5 -5
  6. data/config/database.rb.example +8 -0
  7. data/dm-rest-adapter.gemspec +34 -0
  8. data/lib/rest_adapter.rb +10 -246
  9. data/lib/rest_adapter/adapter.rb +291 -0
  10. data/lib/rest_adapter/connection.rb +76 -0
  11. data/lib/rest_adapter/exceptions.rb +66 -0
  12. data/lib/rest_adapter/formats.rb +16 -0
  13. data/lib/rest_adapter/version.rb +2 -4
  14. data/spec/connection_spec.rb +130 -0
  15. data/spec/crud_spec.rb +250 -0
  16. data/spec/spec_helper.rb +1 -6
  17. data/tasks/spec.rb +1 -1
  18. metadata +16 -94
  19. data/fixtures/book_service/README +0 -256
  20. data/fixtures/book_service/Rakefile +0 -10
  21. data/fixtures/book_service/app/controllers/application.rb +0 -15
  22. data/fixtures/book_service/app/controllers/books_controller.rb +0 -85
  23. data/fixtures/book_service/app/controllers/shelves_controller.rb +0 -89
  24. data/fixtures/book_service/app/helpers/application_helper.rb +0 -3
  25. data/fixtures/book_service/app/helpers/books_helper.rb +0 -2
  26. data/fixtures/book_service/app/helpers/shelf_helper.rb +0 -2
  27. data/fixtures/book_service/app/models/book.rb +0 -6
  28. data/fixtures/book_service/app/models/shelf.rb +0 -5
  29. data/fixtures/book_service/app/views/books/edit.html.erb +0 -20
  30. data/fixtures/book_service/app/views/books/index.html.erb +0 -22
  31. data/fixtures/book_service/app/views/books/new.html.erb +0 -19
  32. data/fixtures/book_service/app/views/books/show.html.erb +0 -13
  33. data/fixtures/book_service/app/views/layouts/books.html.erb +0 -17
  34. data/fixtures/book_service/app/views/shelves/edit.html.erb +0 -16
  35. data/fixtures/book_service/app/views/shelves/index.html.erb +0 -20
  36. data/fixtures/book_service/app/views/shelves/new.html.erb +0 -15
  37. data/fixtures/book_service/app/views/shelves/show.html.erb +0 -7
  38. data/fixtures/book_service/config/boot.rb +0 -109
  39. data/fixtures/book_service/config/database.yml +0 -19
  40. data/fixtures/book_service/config/environment.rb +0 -67
  41. data/fixtures/book_service/config/environments/development.rb +0 -17
  42. data/fixtures/book_service/config/environments/production.rb +0 -22
  43. data/fixtures/book_service/config/environments/test.rb +0 -22
  44. data/fixtures/book_service/config/initializers/inflections.rb +0 -10
  45. data/fixtures/book_service/config/initializers/mime_types.rb +0 -5
  46. data/fixtures/book_service/config/initializers/new_rails_defaults.rb +0 -15
  47. data/fixtures/book_service/config/routes.rb +0 -44
  48. data/fixtures/book_service/db/development.sqlite3 +0 -0
  49. data/fixtures/book_service/db/migrate/20080608165526_create_books.rb +0 -15
  50. data/fixtures/book_service/db/migrate/20080621171551_create_shelves.rb +0 -13
  51. data/fixtures/book_service/db/migrate/20080629143033_create_fake_books_and_shelves.rb +0 -20
  52. data/fixtures/book_service/db/schema.rb +0 -28
  53. data/fixtures/book_service/public/404.html +0 -30
  54. data/fixtures/book_service/public/422.html +0 -30
  55. data/fixtures/book_service/public/500.html +0 -30
  56. data/fixtures/book_service/public/dispatch.cgi +0 -10
  57. data/fixtures/book_service/public/dispatch.fcgi +0 -24
  58. data/fixtures/book_service/public/dispatch.rb +0 -10
  59. data/fixtures/book_service/public/favicon.ico +0 -0
  60. data/fixtures/book_service/public/images/rails.png +0 -0
  61. data/fixtures/book_service/public/index.html +0 -274
  62. data/fixtures/book_service/public/javascripts/application.js +0 -2
  63. data/fixtures/book_service/public/javascripts/controls.js +0 -963
  64. data/fixtures/book_service/public/javascripts/dragdrop.js +0 -972
  65. data/fixtures/book_service/public/javascripts/effects.js +0 -1120
  66. data/fixtures/book_service/public/javascripts/prototype.js +0 -4225
  67. data/fixtures/book_service/public/robots.txt +0 -5
  68. data/fixtures/book_service/public/stylesheets/scaffold.css +0 -53
  69. data/fixtures/book_service/script/about +0 -3
  70. data/fixtures/book_service/script/console +0 -3
  71. data/fixtures/book_service/script/dbconsole +0 -3
  72. data/fixtures/book_service/script/destroy +0 -3
  73. data/fixtures/book_service/script/generate +0 -3
  74. data/fixtures/book_service/script/performance/benchmarker +0 -3
  75. data/fixtures/book_service/script/performance/profiler +0 -3
  76. data/fixtures/book_service/script/performance/request +0 -3
  77. data/fixtures/book_service/script/plugin +0 -3
  78. data/fixtures/book_service/script/process/inspector +0 -3
  79. data/fixtures/book_service/script/process/reaper +0 -3
  80. data/fixtures/book_service/script/process/spawner +0 -3
  81. data/fixtures/book_service/script/runner +0 -3
  82. data/fixtures/book_service/script/server +0 -3
  83. data/fixtures/book_service/test/fixtures/books.yml +0 -9
  84. data/fixtures/book_service/test/fixtures/shelves.yml +0 -7
  85. data/fixtures/book_service/test/functional/books_controller_test.rb +0 -45
  86. data/fixtures/book_service/test/functional/shelf_controller_test.rb +0 -8
  87. data/fixtures/book_service/test/test_helper.rb +0 -38
  88. data/fixtures/book_service/test/unit/book_test.rb +0 -8
  89. data/fixtures/book_service/test/unit/shelf_test.rb +0 -8
  90. data/spec/create_spec.rb +0 -21
  91. data/spec/delete_spec.rb +0 -22
  92. data/spec/read_spec.rb +0 -101
  93. data/spec/update_spec.rb +0 -36
  94. data/stories/all.rb +0 -5
  95. data/stories/crud/create +0 -39
  96. data/stories/crud/delete +0 -9
  97. data/stories/crud/read +0 -36
  98. data/stories/crud/stories.rb +0 -18
  99. data/stories/crud/update +0 -44
  100. data/stories/helper.rb +0 -19
  101. data/stories/resources/helpers/book.rb +0 -7
  102. data/stories/resources/helpers/story_helper.rb +0 -2
  103. data/stories/resources/steps/read.rb +0 -35
  104. data/stories/resources/steps/using_rest_adapter.rb +0 -99
  105. data/tasks/stories.rb +0 -5
@@ -0,0 +1,291 @@
1
+ module DataMapperRest
2
+ # TODO: Abstract XML support out from the protocol
3
+ # TODO: Build JSON support
4
+
5
+ # All http_"verb" (http_post) method calls use method missing in connection class which uses run_verb
6
+ class Adapter < DataMapper::Adapters::AbstractAdapter
7
+ include Extlib
8
+
9
+ def connection
10
+ @connection ||= Connection.new(@uri, @format)
11
+ end
12
+
13
+ # Creates a new resource in the specified repository.
14
+ # TODO: map all remote resource attributes to this resource
15
+ def create(resources)
16
+ created = 0
17
+ resources.each do |resource|
18
+ response = connection.http_post(resource_name(resource), resource.to_xml)
19
+ populate_resource_from_xml(response.body, resource)
20
+
21
+ created += 1
22
+ end
23
+
24
+ created
25
+ end
26
+
27
+ # read_set
28
+ #
29
+ # Examples of query string:
30
+ # A. []
31
+ # GET /books/
32
+ #
33
+ # B. [[:eql, #<Property:Book:id>, 4200]]
34
+ # GET /books/4200
35
+ #
36
+ # IN PROGRESS
37
+ # TODO: Need to account for query.conditions (i.e., [[:eql, #<Property:Book:id>, 1]] for books/1)
38
+ def read_many(query)
39
+ resource_name = Inflection.underscore(query.model.name)
40
+ ::DataMapper::Collection.new(query) do |collection|
41
+ case query.conditions
42
+ when []
43
+ resources_meta = read_set_all(repository, query, resource_name)
44
+ else
45
+ resources_meta = read_set_for_condition(repository, query, resource_name)
46
+ end
47
+ resources_meta.each do |resource_meta|
48
+ if resource_meta.has_key?(:associations)
49
+ load_nested_resources_from resource_meta[:associations], query
50
+ end
51
+ collection.load(resource_meta[:values])
52
+ end
53
+ end
54
+ end
55
+
56
+ def read_one(query)
57
+ resource = nil
58
+ resource_name = resource_name_from_query(query)
59
+ resources_meta = nil
60
+ if query.conditions.empty? && query.limit == 1
61
+ results = read_set_all(repository, query, resource_name)
62
+ resource_meta = results.first unless results.empty?
63
+ else
64
+ id = query.conditions.first[2]
65
+ # KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
66
+
67
+ response = connection.http_get("#{resource_name.pluralize}/#{id}")
68
+
69
+ data = response.body
70
+ resource_meta = parse_resource(data, query.model, query)
71
+ end
72
+ if resource_meta
73
+ if resource_meta.has_key?(:associations)
74
+ load_nested_resources_from resource_meta[:associations], query
75
+ end
76
+ resource = query.model.load(resource_meta[:values], query)
77
+ end
78
+ resource
79
+ end
80
+
81
+ def update(attributes, query)
82
+ # TODO What if we have a compound key?
83
+ raise NotImplementedError.new unless is_single_resource_query? query
84
+ id = query.conditions.first[2]
85
+ resource = nil
86
+ query.repository.scope do
87
+ resource = query.model.get(id)
88
+ end
89
+ attributes.each do |attr, val|
90
+ resource.send("#{attr.name}=", val)
91
+ end
92
+ # KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
93
+ res = connection.http_put("#{resource_name_from_query(query).pluralize}/#{id}", resource.to_xml)
94
+ # TODO: Raise error if cannot reach server
95
+ res.kind_of?(Net::HTTPSuccess) ? 1 : 0
96
+ end
97
+
98
+ def delete(query)
99
+ raise NotImplementedError.new unless is_single_resource_query? query
100
+ id = query.conditions.first[2]
101
+ res = connection.http_delete("#{resource_name_from_query(query).pluralize}/#{id}")
102
+ res.kind_of?(Net::HTTPSuccess) ? 1 : 0
103
+ end
104
+
105
+ protected
106
+
107
+ def normalize_uri(uri_or_options)
108
+ @format = uri_or_options[:format].nil? ? "xml" : uri_or_options[:format]
109
+
110
+ if uri_or_options.kind_of?(String) || uri_or_options.kind_of?(Addressable::URI)
111
+ uri_or_options = DataObjects::URI.parse(uri_or_options)
112
+ end
113
+
114
+ if uri_or_options.kind_of?(DataObjects::URI)
115
+ return uri_or_options
116
+ end
117
+
118
+ query = uri_or_options.except(:adapter, :username, :password, :host, :port, :format, :login).map { |pair| pair.join('=') }.join('&')
119
+ query = nil if query.blank? # not sure if the query is usable
120
+
121
+ return DataObjects::URI.parse(Addressable::URI.new(
122
+ :scheme => "http",
123
+ :adapter => uri_or_options[:adapter].to_s,
124
+ :user => uri_or_options[:login],
125
+ :password => uri_or_options[:password],
126
+ :host => uri_or_options[:host],
127
+ :port => uri_or_options[:port]
128
+ ))
129
+ end
130
+
131
+ def load_nested_resources_from(nested_resources, query)
132
+ nested_resources.each do |resource_meta|
133
+ # TODO: Houston, we have a problem. Model#load expects a Query. When we're nested, we don't have a query yet...
134
+ #resource_meta[:model].load(resource_meta[:values])
135
+ #if resource_meta.has_key? :associations
136
+ # load_nested_resources_from resource_meta, query
137
+ #end
138
+ end
139
+ end
140
+
141
+ def read_set_all(repository, query, resource_name)
142
+ # TODO: how do we know whether the resource we're talking to is singular or plural?
143
+ res = connection.http_get("#{resource_name.pluralize}")
144
+ data = res.body
145
+ parse_resources(data, query.model, query)
146
+ # TODO: Raise error if cannot reach server
147
+ end
148
+
149
+ # GET /books/4200
150
+ def read_set_for_condition(repository, query, resource_name)
151
+ # More complex conditions
152
+ raise NotImplementedError.new
153
+ end
154
+
155
+ # query.conditions like [[:eql, #<Property:Book:id>, 4200]]
156
+ def is_single_resource_query?(query)
157
+ query.conditions.length == 1 && query.conditions.first.first == :eql && query.conditions.first[1].name == :id
158
+ end
159
+
160
+ def values_from_rexml(entity_element, dm_model_class)
161
+ resource = {}
162
+ resource[:values] = []
163
+ entity_element.elements.each do |field_element|
164
+ attribute = dm_model_class.properties(repository.name).find do |property|
165
+ property.name.to_s == field_element.name.to_s.tr('-', '_')
166
+ end
167
+ if attribute
168
+ resource[:values] << field_element.text
169
+ next
170
+ end
171
+ association = dm_model_class.relationships.find do |name, dm_relationship|
172
+ field_element.name.to_s == Inflection.pluralize(Inflection.underscore(dm_relationship.child_model.to_s))
173
+ end
174
+ if association
175
+ field_element.each_element do |associated_element|
176
+ model = association[1].child_model
177
+ (resource[:associations] ||= []) << {
178
+ :model => model,
179
+ :value => values_from_rexml(associated_element, association[1].child_model)
180
+ }
181
+ end
182
+ end
183
+ end
184
+ resource
185
+ end
186
+
187
+ def parse_resource(xml, dm_model_class, query = nil)
188
+ doc = REXML::Document::new(xml)
189
+ # TODO: handle singular resource case as well....
190
+ entity_element = REXML::XPath.first(doc, "/#{resource_name_from_model(dm_model_class)}")
191
+ return nil unless entity_element
192
+ values_from_rexml(entity_element, dm_model_class)
193
+ end
194
+
195
+ def parse_resources(xml, dm_model_class, query = nil)
196
+ doc = REXML::Document::new(xml)
197
+ # # TODO: handle singular resource case as well....
198
+ # array = XPath(doc, "/*[@type='array']")
199
+ # if array
200
+ # parse_resources()
201
+ # else
202
+ resource_name = resource_name_from_model dm_model_class
203
+ doc.elements.collect("#{resource_name.pluralize}/#{resource_name}") do |entity_element|
204
+ values_from_rexml(entity_element, dm_model_class)
205
+ end
206
+ end
207
+
208
+ def resource_name_from_model(model)
209
+ Inflection.underscore(model.name)
210
+ end
211
+
212
+ def resource_name(resource)
213
+ Inflection.underscore(resource.class.name).pluralize
214
+ end
215
+
216
+ def resource_name_from_query(query)
217
+ resource_name_from_model(query.model)
218
+ end
219
+
220
+ def populate_resource_from_xml(xml, resource)
221
+ doc = REXML::Document::new(xml)
222
+ entity_element = REXML::XPath.first(doc, "/#{resource_name_from_model(resource.class)}")
223
+ raise "No root element matching #{resource_name_from_model(resource.class)} in xml" unless entity_element
224
+
225
+ entity_element.elements.each do |field_element|
226
+ attribute = resource.class.properties(repository.name).find { |property| property.name.to_s == field_element.name.to_s.tr('-', '_') }
227
+ resource.send("#{attribute.name.to_s}=", field_element.text) if attribute && !field_element.text.nil?
228
+ # TODO: add association saving
229
+ end
230
+ resource
231
+ end
232
+
233
+ # TODO: this is a temporary hack to allow applications using models with dm-rest-adapter
234
+ # together with models using other adapters
235
+ module Migration
236
+ #
237
+ # Returns whether the storage_name exists.
238
+ #
239
+ # @param storage_name<String> a String defining the name of a storage,
240
+ # for example a table name.
241
+ #
242
+ # @return <Boolean> true if the storage exists
243
+ #
244
+ def storage_exists?(storage_name)
245
+ true
246
+ end
247
+
248
+ #
249
+ # Returns whether the field exists.
250
+ #
251
+ # @param storage_name<String> a String defining the name of a storage, for example a table name.
252
+ # @param field_name<String> a String defining the name of a field, for example a column name.
253
+ #
254
+ # @return <Boolean> true if the field exists.
255
+ #
256
+ def field_exists?(storage_name, field_name)
257
+ true
258
+ end
259
+
260
+ def upgrade_model_storage(repository, model)
261
+ true
262
+ end
263
+
264
+ def create_model_storage(repository, model)
265
+ true
266
+ end
267
+
268
+ def destroy_model_storage(repository, model)
269
+ true
270
+ end
271
+
272
+ def alter_model_storage(repository, *args)
273
+ true
274
+ end
275
+
276
+ def create_property_storage(repository, property)
277
+ true
278
+ end
279
+
280
+ def destroy_property_storage(repository, property)
281
+ true
282
+ end
283
+
284
+ def alter_property_storage(repository, *args)
285
+ true
286
+ end
287
+
288
+ end
289
+ include Migration
290
+ end
291
+ end
@@ -0,0 +1,76 @@
1
+ require 'net/http'
2
+
3
+ module DataMapperRest
4
+ # Somewhat stolen from ActiveResource
5
+ # TODO: Support https?
6
+ class Connection
7
+ include Extlib
8
+ attr_accessor :uri, :format
9
+
10
+ def initialize(uri, format)
11
+ @uri = uri
12
+ @format = Format.new(format)
13
+ end
14
+
15
+ # this is used to run the http verbs like http_post, http_put, http_delete etc.
16
+ # TODO: handle nested resources, see prefix in ActiveResource
17
+ def method_missing(method, *args)
18
+ @uri.path = "/#{args[0]}.#{@format.extension}" # Should be the form of /resources
19
+ if verb = method.to_s.match(/^http_(get|post|put|delete|head)$/)
20
+ run_verb(verb.to_s.split("_").last, args[1])
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ def run_verb(verb, data = nil)
27
+ request do |http|
28
+ mod = Net::HTTP::module_eval(Inflection.camelize(verb))
29
+ request = mod.new(@uri.to_s, @format.header)
30
+ request.basic_auth(@uri.user, @uri.password) if @uri.user && @uri.password
31
+ result = http.request(request, data)
32
+
33
+ handle_response(result)
34
+ end
35
+ end
36
+
37
+ def request(&block)
38
+ res = nil
39
+ Net::HTTP.start(@uri.host, @uri.port) do |http|
40
+ res = yield(http)
41
+ end
42
+ res
43
+ end
44
+
45
+ # Handles response and error codes from remote service.
46
+ def handle_response(response)
47
+ case response.code.to_i
48
+ when 301,302
49
+ raise(Redirection.new(response))
50
+ when 200...400
51
+ response
52
+ when 400
53
+ raise(BadRequest.new(response))
54
+ when 401
55
+ raise(UnauthorizedAccess.new(response))
56
+ when 403
57
+ raise(ForbiddenAccess.new(response))
58
+ when 404
59
+ raise(ResourceNotFound.new(response))
60
+ when 405
61
+ raise(MethodNotAllowed.new(response))
62
+ when 409
63
+ raise(ResourceConflict.new(response))
64
+ when 422
65
+ raise(ResourceInvalid.new(response))
66
+ when 401...500
67
+ raise(ClientError.new(response))
68
+ when 500...600
69
+ raise(ServerError.new(response))
70
+ else
71
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,66 @@
1
+ module DataMapperRest
2
+ # Snagged from Active Resource, it is clean and does what needs to be done
3
+ class ConnectionError < StandardError # :nodoc:
4
+ attr_reader :response
5
+
6
+ def initialize(response, message = nil)
7
+ @response = response
8
+ @message = message
9
+ end
10
+
11
+ def to_s
12
+ "Resource action failed with code: #{response.code}, message: #{response.message if response.respond_to?(:message)}"
13
+ end
14
+ end
15
+
16
+ # Raised when a Timeout::Error occurs.
17
+ class TimeoutError < ConnectionError
18
+ def initialize(message)
19
+ @message = message
20
+ end
21
+ def to_s; @message ;end
22
+ end
23
+
24
+ # 3xx Redirection
25
+ class Redirection < ConnectionError # :nodoc:
26
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
27
+ end
28
+
29
+ # 4xx Client Error
30
+ class ClientError < ConnectionError; end # :nodoc:
31
+
32
+ # 400 Bad Request
33
+ class BadRequest < ClientError; end # :nodoc
34
+
35
+ # 401 Unauthorized
36
+ class UnauthorizedAccess < ClientError; end # :nodoc
37
+
38
+ # 403 Forbidden
39
+ class ForbiddenAccess < ClientError; end # :nodoc
40
+
41
+ # 404 Not Found
42
+ class ResourceNotFound < ClientError; end # :nodoc:
43
+
44
+ # 409 Conflict
45
+ class ResourceConflict < ClientError; end # :nodoc:
46
+
47
+ # 422
48
+ class ResourceInvalid < ClientError; # :nodoc:
49
+ # On this case, we could try to retrieve the validation_errors from message body:
50
+ attr_reader :body
51
+ def initialize(response, message = nil)
52
+ super(response, message)
53
+ @body = response.body unless response.body.nil?
54
+ end
55
+ end
56
+
57
+ # 5xx Server Error
58
+ class ServerError < ConnectionError; end # :nodoc:
59
+
60
+ # 405 Method Not Allowed
61
+ class MethodNotAllowed < ClientError # :nodoc:
62
+ def allowed_methods
63
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,16 @@
1
+ module DataMapperRest
2
+ # Absolutely simple format class, extend later if needed
3
+ class Format
4
+ attr_accessor :extension, :mime
5
+
6
+ def initialize(type)
7
+ @extension = type
8
+ @mime = "application/#{type}"
9
+ end
10
+
11
+ def header
12
+ {'Content-Type' => @mime}
13
+ end
14
+
15
+ end
16
+ end