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.
- data/{History.txt → History.rdoc} +5 -3
- data/Manifest.txt +5 -8
- data/{README.markdown → README.rdoc} +0 -0
- data/Rakefile +2 -3
- data/lib/rest_adapter.rb +4 -6
- data/lib/rest_adapter/adapter.rb +109 -232
- data/lib/rest_adapter/connection.rb +12 -5
- data/lib/rest_adapter/exceptions.rb +2 -2
- data/lib/rest_adapter/formats.rb +1 -1
- data/lib/rest_adapter/version.rb +1 -1
- data/spec/fixtures/book.rb +8 -0
- data/spec/{connection_spec.rb → semipublic/connection_spec.rb} +8 -10
- data/spec/semipublic/rest_adapter_spec.rb +224 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -14
- data/tasks/install.rb +1 -1
- data/tasks/spec.rb +4 -4
- metadata +16 -26
- data/README.txt +0 -47
- data/config/database.rb.example +0 -8
- data/dm-rest-adapter.gemspec +0 -34
- data/spec/crud_spec.rb +0 -250
- data/spec/ruby_forker.rb +0 -13
data/Manifest.txt
CHANGED
@@ -1,21 +1,18 @@
|
|
1
|
-
History.
|
1
|
+
History.rdoc
|
2
2
|
LICENSE
|
3
3
|
Manifest.txt
|
4
|
-
README.
|
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/
|
17
|
-
spec/
|
18
|
-
spec/
|
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.
|
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}"
|
data/lib/rest_adapter.rb
CHANGED
@@ -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
|
-
|
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'
|
data/lib/rest_adapter/adapter.rb
CHANGED
@@ -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
|
-
|
19
|
-
populate_resource_from_xml(response.body, resource)
|
9
|
+
model = resource.model
|
20
10
|
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
created
|
25
|
-
end
|
11
|
+
response = connection.http_post("#{resource_name(model)}", resource.to_xml)
|
26
12
|
|
27
|
-
|
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
|
57
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
if
|
74
|
-
|
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
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
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
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
62
|
+
def initialize(*)
|
63
|
+
super
|
64
|
+
@format = @options.fetch(:format, 'xml')
|
158
65
|
end
|
159
66
|
|
160
|
-
def
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
188
|
-
|
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
|
-
|
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
|
-
|
209
|
-
|
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
|
-
|
213
|
-
Inflection.underscore(resource.class.name).pluralize
|
98
|
+
key_condition.first.value
|
214
99
|
end
|
215
100
|
|
216
|
-
def
|
217
|
-
|
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
|
221
|
-
|
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.
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
119
|
+
|
120
|
+
record
|
231
121
|
end
|
232
122
|
|
233
|
-
|
234
|
-
|
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
|
-
|
261
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
269
|
-
|
270
|
-
end
|
136
|
+
def parse_resources(xml, model)
|
137
|
+
doc = REXML::Document::new(xml)
|
271
138
|
|
272
|
-
|
273
|
-
|
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
|
-
|
277
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
147
|
+
def element_name(model)
|
148
|
+
Extlib::Inflection.underscore(model.name)
|
149
|
+
end
|
283
150
|
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|