dm-rest-adapter 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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