dm-rest-adapter 0.9.11 → 0.10.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.
@@ -1,8 +1,10 @@
1
- === 0.9.11 / 2009-03-29
1
+ === 0.10.0 / 2009-10-15
2
+
3
+ * Updated to work with dm-core 0.10.0
2
4
 
3
- * 1 major enhancement:
5
+ === 0.9.11 / 2009-03-29
4
6
 
5
- * Updated and refactored entire adapter
7
+ * No changes this version
6
8
 
7
9
  === 0.9.10 / 2009-01-19
8
10
 
@@ -1,21 +1,18 @@
1
- History.txt
1
+ History.rdoc
2
2
  LICENSE
3
3
  Manifest.txt
4
- README.markdown
5
- README.txt
4
+ README.rdoc
6
5
  Rakefile
7
6
  TODO
8
- config/database.rb.example
9
- dm-rest-adapter.gemspec
10
7
  lib/rest_adapter.rb
11
8
  lib/rest_adapter/adapter.rb
12
9
  lib/rest_adapter/connection.rb
13
10
  lib/rest_adapter/exceptions.rb
14
11
  lib/rest_adapter/formats.rb
15
12
  lib/rest_adapter/version.rb
16
- spec/connection_spec.rb
17
- spec/crud_spec.rb
18
- spec/ruby_forker.rb
13
+ spec/fixtures/book.rb
14
+ spec/semipublic/connection_spec.rb
15
+ spec/semipublic/rest_adapter_spec.rb
19
16
  spec/spec.opts
20
17
  spec/spec_helper.rb
21
18
  tasks/install.rb
File without changes
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'pathname'
2
- require 'rubygems'
3
2
 
4
3
  ROOT = Pathname(__FILE__).dirname.expand_path
5
4
  JRUBY = RUBY_PLATFORM =~ /java/
@@ -12,9 +11,9 @@ AUTHOR = 'Scott Burton @ Joyent Inc'
12
11
  EMAIL = 'scott.burton [a] joyent [d] com'
13
12
  GEM_NAME = 'dm-rest-adapter'
14
13
  GEM_VERSION = DataMapperRest::VERSION
15
- GEM_DEPENDENCIES = [['dm-core', GEM_VERSION]]
14
+ GEM_DEPENDENCIES = [['dm-core', GEM_VERSION], ['dm-serializer', GEM_VERSION]]
16
15
  GEM_CLEAN = %w[ log pkg coverage ]
17
- GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO History.txt ] }
16
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.rdoc LICENSE TODO History.rdoc ] }
18
17
 
19
18
  PROJECT_NAME = 'datamapper'
20
19
  PROJECT_URL = "http://github.com/datamapper/dm-more/tree/master/adapters/#{GEM_NAME}"
@@ -1,11 +1,9 @@
1
- $:.push File.expand_path(File.dirname(__FILE__))
2
-
3
- require 'dm-core'
4
- require 'extlib'
5
- require 'pathname'
6
1
  require 'rexml/document'
7
- require 'rubygems'
2
+
3
+ require 'cgi' # for CGI.escape
4
+ require 'addressable/uri'
8
5
  require 'dm-serializer'
6
+
9
7
  require 'rest_adapter/version'
10
8
  require 'rest_adapter/adapter'
11
9
  require 'rest_adapter/connection'
@@ -4,288 +4,165 @@ module DataMapperRest
4
4
 
5
5
  # All http_"verb" (http_post) method calls use method missing in connection class which uses run_verb
6
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
7
  def create(resources)
16
- created = 0
17
8
  resources.each do |resource|
18
- response = connection.http_post(resource_name(resource), resource.to_xml)
19
- populate_resource_from_xml(response.body, resource)
9
+ model = resource.model
20
10
 
21
- created += 1
22
- end
23
-
24
- created
25
- end
11
+ response = connection.http_post("#{resource_name(model)}", resource.to_xml)
26
12
 
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
13
+ update_with_response(resource, response)
53
14
  end
54
15
  end
55
16
 
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}")
17
+ def read(query)
18
+ model = query.model
68
19
 
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
20
+ records = if id = extract_id_from_query(query)
21
+ response = connection.http_get("#{resource_name(model)}/#{id}")
22
+ [ parse_resource(response.body, model) ]
23
+ else
24
+ query_string = if (params = extract_params_from_query(query)).any?
25
+ params.map { |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
75
26
  end
76
- resource = query.model.load(resource_meta[:values], query)
77
- end
78
- resource
79
- end
80
27
 
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)
28
+ response = connection.http_get("#{resource_name(model)}#{'?' << query_string if query_string}")
29
+ parse_resources(response.body, model)
91
30
  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
31
 
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
32
+ query.filter_records(records)
103
33
  end
104
34
 
105
- protected
35
+ def update(dirty_attributes, collection)
36
+ collection.select do |resource|
37
+ model = resource.model
38
+ key = model.key
39
+ id = key.get(resource).join
106
40
 
107
- def normalize_uri(uri_or_options)
108
- @format = uri_or_options[:format].nil? ? "xml" : uri_or_options[:format]
41
+ dirty_attributes.each { |p, v| p.set!(resource, v) }
109
42
 
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
43
+ response = connection.http_put("#{resource_name(model)}/#{id}", resource.to_xml)
113
44
 
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
- ))
45
+ update_with_response(resource, response)
46
+ end.size
129
47
  end
130
48
 
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
49
+ def delete(collection)
50
+ collection.select do |resource|
51
+ model = resource.model
52
+ key = model.key
53
+ id = key.get(resource).join
140
54
 
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
55
+ response = connection.http_delete("#{resource_name(model)}/#{id}")
56
+ response.kind_of?(Net::HTTPSuccess)
57
+ end.size
147
58
  end
148
59
 
149
- # GET /books/4200
150
- def read_set_for_condition(repository, query, resource_name)
151
- # More complex conditions
152
- raise NotImplementedError.new
153
- end
60
+ private
154
61
 
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
62
+ def initialize(*)
63
+ super
64
+ @format = @options.fetch(:format, 'xml')
158
65
  end
159
66
 
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
67
+ def connection
68
+ @connection ||= Connection.new(normalized_uri, @format)
69
+ end
70
+
71
+ def normalized_uri
72
+ @normalized_uri ||=
73
+ begin
74
+ query = @options.except(:adapter, :user, :password, :host, :port, :path, :fragment)
75
+ query = nil if query.empty?
76
+
77
+ Addressable::URI.new(
78
+ :scheme => 'http',
79
+ :user => @options[:user],
80
+ :password => @options[:password],
81
+ :host => @options[:host],
82
+ :port => @options[:port],
83
+ :path => @options[:path],
84
+ :query_values => query,
85
+ :fragment => @options[:fragment]
86
+ ).freeze
182
87
  end
183
- end
184
- resource
185
88
  end
186
89
 
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
90
+ def extract_id_from_query(query)
91
+ return nil unless query.limit == 1
194
92
 
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
93
+ conditions = query.conditions
207
94
 
208
- def resource_name_from_model(model)
209
- Inflection.underscore(model.name)
210
- end
95
+ return nil unless conditions.kind_of?(DataMapper::Query::Conditions::AndOperation)
96
+ return nil unless (key_condition = conditions.select { |o| o.subject.key? }).size == 1
211
97
 
212
- def resource_name(resource)
213
- Inflection.underscore(resource.class.name).pluralize
98
+ key_condition.first.value
214
99
  end
215
100
 
216
- def resource_name_from_query(query)
217
- resource_name_from_model(query.model)
101
+ def extract_params_from_query(query)
102
+ conditions = query.conditions
103
+
104
+ return {} unless conditions.kind_of?(DataMapper::Query::Conditions::AndOperation)
105
+ return {} if conditions.any? { |o| o.subject.key? }
106
+
107
+ query.options
218
108
  end
219
109
 
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
110
+ def record_from_rexml(entity_element, field_to_property)
111
+ record = {}
224
112
 
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
113
+ entity_element.elements.map do |element|
114
+ # TODO: push this to the per-property mix-in for this adapter
115
+ field = element.name.to_s.tr('-', '_')
116
+ next unless property = field_to_property[field]
117
+ record[field] = property.typecast(element.text)
229
118
  end
230
- resource
119
+
120
+ record
231
121
  end
232
122
 
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
123
+ def parse_resource(xml, model)
124
+ doc = REXML::Document::new(xml)
247
125
 
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
126
+ element_name = element_name(model)
259
127
 
260
- def upgrade_model_storage(repository, model)
261
- true
128
+ unless entity_element = REXML::XPath.first(doc, "/#{element_name}")
129
+ raise "No root element matching #{element_name} in xml"
262
130
  end
263
131
 
264
- def create_model_storage(repository, model)
265
- true
266
- end
132
+ field_to_property = model.properties(name).map { |p| [ p.field, p ] }.to_hash
133
+ record_from_rexml(entity_element, field_to_property)
134
+ end
267
135
 
268
- def destroy_model_storage(repository, model)
269
- true
270
- end
136
+ def parse_resources(xml, model)
137
+ doc = REXML::Document::new(xml)
271
138
 
272
- def alter_model_storage(repository, *args)
273
- true
274
- end
139
+ field_to_property = model.properties(name).map { |p| [ p.field, p ] }.to_hash
140
+ element_name = element_name(model)
275
141
 
276
- def create_property_storage(repository, property)
277
- true
142
+ doc.elements.collect("/#{element_name.pluralize}/#{element_name}") do |entity_element|
143
+ record_from_rexml(entity_element, field_to_property)
278
144
  end
145
+ end
279
146
 
280
- def destroy_property_storage(repository, property)
281
- true
282
- end
147
+ def element_name(model)
148
+ Extlib::Inflection.underscore(model.name)
149
+ end
283
150
 
284
- def alter_property_storage(repository, *args)
285
- true
286
- end
151
+ def resource_name(model)
152
+ Extlib::Inflection.underscore(model.name).pluralize
153
+ end
154
+
155
+ def update_with_response(resource, response)
156
+ return unless response.kind_of?(Net::HTTPSuccess) && !response.body.blank?
287
157
 
158
+ model = resource.model
159
+ properties = model.properties(name)
160
+
161
+ parse_resource(response.body, model).each do |key, value|
162
+ if property = properties[key.to_sym]
163
+ property.set!(resource, value)
164
+ end
165
+ end
288
166
  end
289
- include Migration
290
167
  end
291
168
  end