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