dm-rest-adapter 0.9.2 → 0.9.3
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 +1 -0
- data/Manifest.txt +26 -0
- data/{README → README.txt} +1 -2
- data/Rakefile +19 -25
- data/TODO +0 -1
- data/lib/rest_adapter.rb +115 -75
- data/lib/rest_adapter/version.rb +7 -0
- data/spec/create_spec.rb +21 -0
- data/spec/delete_spec.rb +22 -0
- data/spec/{rest_adapter_spec.rb → read_spec.rb} +22 -74
- data/spec/ruby_forker.rb +1 -1
- data/spec/spec_helper.rb +17 -0
- data/spec/update_spec.rb +36 -0
- data/stories/all.rb +5 -0
- data/stories/crud/create +39 -0
- data/stories/crud/delete +9 -0
- data/stories/crud/read +36 -0
- data/stories/crud/stories.rb +18 -0
- data/stories/crud/update +44 -0
- data/stories/helper.rb +9 -0
- data/stories/resources/helpers/book.rb +7 -0
- data/stories/resources/helpers/story_helper.rb +2 -0
- data/stories/resources/steps/read.rb +35 -0
- data/stories/resources/steps/using_rest_adapter.rb +99 -0
- metadata +45 -14
data/History.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/Manifest.txt
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
History.txt
|
2
|
+
LICENSE
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
TODO
|
7
|
+
lib/rest_adapter.rb
|
8
|
+
lib/rest_adapter/version.rb
|
9
|
+
spec/create_spec.rb
|
10
|
+
spec/delete_spec.rb
|
11
|
+
spec/read_spec.rb
|
12
|
+
spec/ruby_forker.rb
|
13
|
+
spec/spec.opts
|
14
|
+
spec/spec_helper.rb
|
15
|
+
spec/update_spec.rb
|
16
|
+
stories/all.rb
|
17
|
+
stories/crud/create
|
18
|
+
stories/crud/delete
|
19
|
+
stories/crud/read
|
20
|
+
stories/crud/stories.rb
|
21
|
+
stories/crud/update
|
22
|
+
stories/helper.rb
|
23
|
+
stories/resources/helpers/book.rb
|
24
|
+
stories/resources/helpers/story_helper.rb
|
25
|
+
stories/resources/steps/read.rb
|
26
|
+
stories/resources/steps/using_rest_adapter.rb
|
data/{README → README.txt}
RENAMED
data/Rakefile
CHANGED
@@ -1,40 +1,33 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'spec'
|
3
|
-
require 'rake/clean'
|
4
|
-
require 'rake/gempackagetask'
|
5
3
|
require 'spec/rake/spectask'
|
6
4
|
require 'pathname'
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
6
|
+
ROOT = Pathname(__FILE__).dirname.expand_path
|
7
|
+
require ROOT + 'lib/rest_adapter/version'
|
8
|
+
|
9
|
+
AUTHOR = "Potomac Ruby Hackers"
|
10
|
+
EMAIL = "potomac-ruby-hackers@googlegroups.com"
|
11
|
+
GEM_NAME = "dm-rest-adapter"
|
12
|
+
GEM_VERSION = DataMapper::More::RestAdapter::VERSION
|
13
|
+
GEM_DEPENDENCIES = [["dm-core", GEM_VERSION]]
|
14
|
+
GEM_CLEAN = ["log", "pkg"]
|
15
|
+
GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO ] }
|
16
|
+
|
17
|
+
PROJECT_NAME = "datamapper"
|
18
|
+
PROJECT_URL = "http://github.com/pjb3/dm-more/tree/master/adapters/dm-rest-adapter"
|
19
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY = "REST Adapter for DataMapper"
|
20
|
+
|
21
|
+
require ROOT.parent.parent + 'tasks/hoe'
|
25
22
|
|
26
23
|
task :default => [ :spec ]
|
27
24
|
|
28
25
|
WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
|
29
26
|
SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
|
30
27
|
|
31
|
-
|
32
|
-
pkg.gem_spec = spec
|
33
|
-
end
|
34
|
-
|
35
|
-
desc "Install #{spec.name} #{spec.version}"
|
28
|
+
desc "Install #{GEM_NAME} #{GEM_VERSION}"
|
36
29
|
task :install => [ :package ] do
|
37
|
-
sh "#{SUDO} gem install pkg/#{
|
30
|
+
sh "#{SUDO} gem install pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
|
38
31
|
end
|
39
32
|
|
40
33
|
desc 'Run specifications'
|
@@ -48,5 +41,6 @@ end
|
|
48
41
|
|
49
42
|
desc "Run all stories"
|
50
43
|
task :stories do
|
44
|
+
# TODO Re-migrate the book service or else you won't have test data!
|
51
45
|
ruby "stories/all.rb --colour --format plain"
|
52
46
|
end
|
data/lib/rest_adapter.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
|
2
|
+
require 'pathname'
|
3
|
+
require Pathname(__FILE__).dirname + 'rest_adapter/version'
|
4
|
+
gem 'dm-core', DataMapper::More::RestAdapter::VERSION
|
3
5
|
require 'dm-core'
|
4
6
|
require 'extlib'
|
5
7
|
require 'dm-serializer'
|
6
|
-
require 'pathname'
|
7
8
|
require 'net/http'
|
8
9
|
require 'rexml/document'
|
9
10
|
|
@@ -13,36 +14,26 @@ module DataMapper
|
|
13
14
|
module Adapters
|
14
15
|
class RestAdapter < AbstractAdapter
|
15
16
|
include Extlib
|
16
|
-
|
17
|
-
# def read_one(query)
|
18
|
-
# raise NotImplementedError
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# def update(attributes, query)
|
22
|
-
# raise NotImplementedError
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# def delete(query)
|
26
|
-
# raise NotImplementedError
|
27
|
-
# end
|
28
|
-
|
17
|
+
|
29
18
|
# Creates a new resource in the specified repository.
|
30
19
|
def create(resources)
|
31
|
-
|
20
|
+
count = 0
|
32
21
|
resources.each do |resource|
|
33
|
-
resource_name = Inflection.underscore(resource.class.name
|
22
|
+
resource_name = Inflection.underscore(resource.class.name)
|
34
23
|
result = http_post("/#{resource_name.pluralize}.xml", resource.to_xml)
|
35
24
|
# TODO: Raise error if cannot reach server
|
36
|
-
success =
|
25
|
+
success = result.instance_of?(Net::HTTPCreated)
|
37
26
|
if success
|
38
|
-
|
39
|
-
|
27
|
+
count += 1
|
28
|
+
# TODO: Fix commented out code below to work through the identity_map of the repository
|
29
|
+
# values = parse_resource(result.body, resource.class)
|
30
|
+
# resource.id = updated_resource.id
|
40
31
|
end
|
41
32
|
# TODO: We're not using the response to update the DataMapper::Resource with the newly acquired ID!!!
|
42
33
|
end
|
43
|
-
|
34
|
+
count
|
44
35
|
end
|
45
|
-
|
36
|
+
|
46
37
|
# read_set
|
47
38
|
#
|
48
39
|
# Examples of query string:
|
@@ -55,73 +46,107 @@ module DataMapper
|
|
55
46
|
# IN PROGRESS
|
56
47
|
# TODO: Need to account for query.conditions (i.e., [[:eql, #<Property:Book:id>, 1]] for books/1)
|
57
48
|
def read_many(query)
|
58
|
-
resource_name = Inflection.underscore(query.model.name
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
49
|
+
resource_name = Inflection.underscore(query.model.name)
|
50
|
+
Collection.new(query) do |collection|
|
51
|
+
case query.conditions
|
52
|
+
when []
|
53
|
+
resources_meta = read_set_all(repository, query, resource_name)
|
54
|
+
else
|
55
|
+
resources_meta = read_set_for_condition(repository, query, resource_name)
|
56
|
+
end
|
57
|
+
resources_meta.each do |resource_meta|
|
58
|
+
if resource_meta.has_key?(:associations)
|
59
|
+
load_nested_resources_from resource_meta[:associations], query
|
60
|
+
end
|
61
|
+
collection.load(resource_meta[:values])
|
62
|
+
end
|
64
63
|
end
|
65
64
|
end
|
66
|
-
|
65
|
+
|
67
66
|
def read_one(query)
|
68
|
-
|
69
|
-
id = query.conditions.first[2]
|
70
|
-
# KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
|
67
|
+
resource = nil
|
71
68
|
resource_name = resource_name_from_query(query)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
69
|
+
resources_meta = nil
|
70
|
+
if query.conditions.empty? && query.limit == 1
|
71
|
+
results = read_set_all(repository, query, resource_name)
|
72
|
+
resource_meta = results.first unless results.empty?
|
73
|
+
else
|
74
|
+
id = query.conditions.first[2]
|
75
|
+
# KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
|
76
|
+
|
77
|
+
response = http_get("/#{resource_name.pluralize}/#{id}.xml")
|
78
|
+
|
79
|
+
# KLUGE: Rails returns HTML if it can't find a resource. A properly RESTful app would return a 404, right?
|
80
|
+
return nil if response.is_a? Net::HTTPNotFound || response.content_type == "text/html"
|
81
|
+
|
82
|
+
data = response.body
|
83
|
+
resource_meta = parse_resource(data, query.model, query)
|
84
|
+
end
|
85
|
+
if resource_meta
|
86
|
+
if resource_meta.has_key?(:associations)
|
87
|
+
load_nested_resources_from resource_meta[:associations], query
|
88
|
+
end
|
89
|
+
resource = query.model.load(resource_meta[:values], query)
|
90
|
+
end
|
91
|
+
resource
|
80
92
|
end
|
81
|
-
|
93
|
+
|
82
94
|
def update(attributes, query)
|
83
|
-
# TODO
|
95
|
+
# TODO What if we have a compound key?
|
84
96
|
raise NotImplementedError.new unless is_single_resource_query? query
|
85
97
|
id = query.conditions.first[2]
|
86
|
-
resource =
|
98
|
+
resource = nil
|
99
|
+
query.repository.scope do
|
100
|
+
resource = query.model.get(id)
|
101
|
+
end
|
87
102
|
attributes.each do |attr, val|
|
88
103
|
resource.send("#{attr.name}=", val)
|
89
104
|
end
|
90
105
|
# KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
|
91
|
-
http_put("/#{resource_name_from_query(query).pluralize}/#{id}.xml", resource.to_xml)
|
106
|
+
res = http_put("/#{resource_name_from_query(query).pluralize}/#{id}.xml", resource.to_xml)
|
92
107
|
# TODO: Raise error if cannot reach server
|
108
|
+
res.kind_of?(Net::HTTPSuccess) ? 1 : 0
|
93
109
|
end
|
94
|
-
|
110
|
+
|
95
111
|
def delete(query)
|
96
|
-
#puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>> QUERY: #{query} #{query.inspect}"
|
97
|
-
# TODO update for v0.9.2
|
98
112
|
raise NotImplementedError.new unless is_single_resource_query? query
|
99
113
|
id = query.conditions.first[2]
|
100
|
-
http_delete("/#{resource_name_from_query(query).pluralize}/#{id}.xml")
|
114
|
+
res = http_delete("/#{resource_name_from_query(query).pluralize}/#{id}.xml")
|
115
|
+
res.kind_of?(Net::HTTPSuccess) ? 1 : 0
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
def load_nested_resources_from(nested_resources, query)
|
120
|
+
nested_resources.each do |resource_meta|
|
121
|
+
# TODO: Houston, we have a problem. Model#load expects a Query. When we're nested, we don't have a query yet...
|
122
|
+
#resource_meta[:model].load(resource_meta[:values])
|
123
|
+
#if resource_meta.has_key? :associations
|
124
|
+
# load_nested_resources_from resource_meta, query
|
125
|
+
#end
|
126
|
+
end
|
101
127
|
end
|
102
|
-
|
103
|
-
protected
|
128
|
+
|
104
129
|
def read_set_all(repository, query, resource_name)
|
105
130
|
# TODO: how do we know whether the resource we're talking to is singular or plural?
|
106
131
|
res = http_get("/#{resource_name.pluralize}.xml")
|
107
132
|
data = res.body
|
108
|
-
parse_resources(data, query.model)
|
133
|
+
parse_resources(data, query.model, query)
|
109
134
|
# TODO: Raise error if cannot reach server
|
110
135
|
end
|
111
|
-
|
136
|
+
|
112
137
|
# GET /books/4200
|
113
138
|
def read_set_for_condition(repository, query, resource_name)
|
114
139
|
# More complex conditions
|
115
140
|
raise NotImplementedError.new
|
116
|
-
end
|
117
|
-
|
141
|
+
end
|
142
|
+
|
118
143
|
# query.conditions like [[:eql, #<Property:Book:id>, 4200]]
|
119
144
|
def is_single_resource_query?(query)
|
120
145
|
query.conditions.length == 1 && query.conditions.first.first == :eql && query.conditions.first[1].name == :id
|
121
146
|
end
|
122
|
-
|
147
|
+
|
123
148
|
def http_put(uri, data = nil)
|
124
|
-
request { |http| http.put(uri, data) }
|
149
|
+
request { |http| http.put(uri, data, {"Content-Type", "application/xml"}) }
|
125
150
|
end
|
126
151
|
|
127
152
|
def http_post(uri, data)
|
@@ -129,11 +154,11 @@ module DataMapper
|
|
129
154
|
end
|
130
155
|
|
131
156
|
def http_get(uri)
|
132
|
-
request { |http| http.get(uri) }
|
157
|
+
request { |http| http.get(uri, {"Content-Type", "application/xml"}) }
|
133
158
|
end
|
134
159
|
|
135
160
|
def http_delete(uri)
|
136
|
-
request { |http| http.delete(uri) }
|
161
|
+
request { |http| http.delete(uri, {"Content-Type", "application/xml"}) }
|
137
162
|
end
|
138
163
|
|
139
164
|
def request(&block)
|
@@ -142,30 +167,45 @@ module DataMapper
|
|
142
167
|
res = yield(http)
|
143
168
|
end
|
144
169
|
res
|
145
|
-
end
|
170
|
+
end
|
146
171
|
|
147
|
-
def
|
148
|
-
resource =
|
172
|
+
def values_from_rexml(entity_element, dm_model_class)
|
173
|
+
resource = {}
|
174
|
+
resource[:values] = []
|
149
175
|
entity_element.elements.each do |field_element|
|
150
|
-
attribute =
|
176
|
+
attribute = dm_model_class.properties(repository.name).find do |property|
|
151
177
|
# *MUST* use Inflection.underscore on the XML as Rails converts '_' to '-' in the XML
|
152
|
-
name.to_s == Inflection.underscore(field_element.name.to_s)
|
178
|
+
property.name.to_s == Inflection.underscore(field_element.name.to_s)
|
179
|
+
end
|
180
|
+
if attribute
|
181
|
+
resource[:values] << field_element.text
|
182
|
+
next
|
183
|
+
end
|
184
|
+
association = dm_model_class.relationships.find do |name, dm_relationship|
|
185
|
+
field_element.name.to_s == Inflection.pluralize(Inflection.underscore(dm_relationship.child_model.to_s))
|
186
|
+
end
|
187
|
+
if association
|
188
|
+
field_element.each_element do |associated_element|
|
189
|
+
model = association[1].child_model
|
190
|
+
(resource[:associations] ||= []) << {
|
191
|
+
:model => model,
|
192
|
+
:value => values_from_rexml(associated_element, association[1].child_model)
|
193
|
+
}
|
194
|
+
end
|
153
195
|
end
|
154
|
-
resource.send("#{Inflection.underscore(attribute[0])}=", field_element.text) if attribute
|
155
196
|
end
|
156
|
-
resource.instance_eval { @new_record= false }
|
157
197
|
resource
|
158
198
|
end
|
159
199
|
|
160
|
-
def parse_resource(xml, dm_model_class)
|
200
|
+
def parse_resource(xml, dm_model_class, query = nil)
|
161
201
|
doc = REXML::Document::new(xml)
|
162
202
|
# TODO: handle singular resource case as well....
|
163
203
|
entity_element = REXML::XPath.first(doc, "/#{resource_name_from_model(dm_model_class)}")
|
164
204
|
return nil unless entity_element
|
165
|
-
|
205
|
+
values_from_rexml(entity_element, dm_model_class)
|
166
206
|
end
|
167
|
-
|
168
|
-
def parse_resources(xml, dm_model_class)
|
207
|
+
|
208
|
+
def parse_resources(xml, dm_model_class, query = nil)
|
169
209
|
doc = REXML::Document::new(xml)
|
170
210
|
# # TODO: handle singular resource case as well....
|
171
211
|
# array = XPath(doc, "/*[@type='array']")
|
@@ -174,17 +214,17 @@ module DataMapper
|
|
174
214
|
# else
|
175
215
|
resource_name = resource_name_from_model dm_model_class
|
176
216
|
doc.elements.collect("#{resource_name.pluralize}/#{resource_name}") do |entity_element|
|
177
|
-
|
217
|
+
values_from_rexml(entity_element, dm_model_class)
|
178
218
|
end
|
179
|
-
end
|
180
|
-
|
219
|
+
end
|
220
|
+
|
181
221
|
def resource_name_from_model(model)
|
182
|
-
Inflection.underscore(model.name
|
222
|
+
Inflection.underscore(model.name)
|
183
223
|
end
|
184
|
-
|
224
|
+
|
185
225
|
def resource_name_from_query(query)
|
186
226
|
resource_name_from_model(query.model)
|
187
227
|
end
|
188
228
|
end
|
189
229
|
end
|
190
|
-
end
|
230
|
+
end
|
data/spec/create_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'A REST adapter' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@adapter = DataMapper::Repository.adapters[:default]
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'when saving a resource' do
|
11
|
+
|
12
|
+
before do
|
13
|
+
@book = Book.new(:title => 'Hello, World!', :author => 'Anonymous')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should make an HTTP Post' do
|
17
|
+
@adapter.should_receive(:http_post).with('/books.xml', @book.to_xml)
|
18
|
+
@book.save
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/delete_spec.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'A REST adapter' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@adapter = DataMapper::Repository.adapters[:default]
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'when deleting an existing resource' do
|
11
|
+
before do
|
12
|
+
@book = Book.new(:title => 'Hello, World!', :author => 'Anonymous')
|
13
|
+
@book.stub!(:new_record?).and_return(false)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should do an HTTP DELETE' do
|
17
|
+
@adapter.should_receive(:http_delete)
|
18
|
+
@book.destroy
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -1,43 +1,16 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
|
4
|
-
DataMapper.setup(:default, {
|
5
|
-
:adapter => 'rest',
|
6
|
-
:format => 'xml',
|
7
|
-
:host => 'localhost',
|
8
|
-
:port => '3001'
|
9
|
-
})
|
10
|
-
|
11
|
-
class Book
|
12
|
-
include DataMapper::Resource
|
13
|
-
property :id, Integer, :serial => true
|
14
|
-
property :title, String
|
15
|
-
property :author, String
|
16
|
-
property :created_at, DateTime
|
17
|
-
end
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
18
3
|
|
19
4
|
describe 'A REST adapter' do
|
20
|
-
|
5
|
+
|
21
6
|
before do
|
22
7
|
@adapter = DataMapper::Repository.adapters[:default]
|
23
8
|
end
|
24
|
-
|
25
|
-
describe 'when saving a resource' do
|
26
|
-
|
27
|
-
before do
|
28
|
-
@book = Book.new(:title => 'Hello, World!', :author => 'Anonymous')
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'should make an HTTP Post' do
|
32
|
-
@adapter.should_receive(:http_post).with('/books.xml', @book.to_xml)
|
33
|
-
@book.save
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
9
|
+
|
37
10
|
describe 'when getting one resource' do
|
38
11
|
|
39
12
|
describe 'if the resource exists' do
|
40
|
-
|
13
|
+
|
41
14
|
before do
|
42
15
|
book_xml = <<-BOOK
|
43
16
|
<?xml version='1.0' encoding='UTF-8'?>
|
@@ -54,20 +27,25 @@ describe 'A REST adapter' do
|
|
54
27
|
@response.stub!(:body).and_return(book_xml)
|
55
28
|
@adapter.stub!(:http_get).and_return(@response)
|
56
29
|
end
|
57
|
-
|
30
|
+
|
58
31
|
it 'should return the resource' do
|
59
32
|
book = Book.get(@id)
|
60
33
|
book.should_not be_nil
|
61
34
|
book.id.should be_an_instance_of(Fixnum)
|
62
35
|
book.id.should == 1
|
63
36
|
end
|
64
|
-
|
37
|
+
|
65
38
|
it 'should do an HTTP GET' do
|
66
39
|
@adapter.should_receive(:http_get).with('/books/1.xml').and_return(@response)
|
67
40
|
Book.get(@id)
|
68
41
|
end
|
42
|
+
|
43
|
+
it "it be equal to itself" do
|
44
|
+
Book.get(@id).should == Book.get(@id)
|
45
|
+
end
|
69
46
|
end
|
70
|
-
|
47
|
+
|
48
|
+
|
71
49
|
describe 'if the resource does not exist' do
|
72
50
|
it 'should return nil' do
|
73
51
|
@id = 1
|
@@ -80,9 +58,9 @@ describe 'A REST adapter' do
|
|
80
58
|
end
|
81
59
|
end
|
82
60
|
end
|
83
|
-
|
61
|
+
|
84
62
|
describe 'when getting all resource of a particular type' do
|
85
|
-
before do
|
63
|
+
before do
|
86
64
|
books_xml = <<-BOOK
|
87
65
|
<?xml version='1.0' encoding='UTF-8'?>
|
88
66
|
<books type='array'>
|
@@ -92,7 +70,7 @@ describe 'A REST adapter' do
|
|
92
70
|
<id type='integer'>1</id>
|
93
71
|
<title>The Dispossed</title>
|
94
72
|
<updated-at type='datetime'>2008-06-08T17:02:28Z</updated-at>
|
95
|
-
</book>
|
73
|
+
</book>
|
96
74
|
<book>
|
97
75
|
<author>Stephen King</author>
|
98
76
|
<created-at type='datetime'>2008-06-08T17:03:07Z</created-at>
|
@@ -106,48 +84,18 @@ describe 'A REST adapter' do
|
|
106
84
|
@response.stub!(:body).and_return(books_xml)
|
107
85
|
@adapter.stub!(:http_get).and_return(@response)
|
108
86
|
end
|
109
|
-
|
87
|
+
|
110
88
|
it 'should get a non-empty list' do
|
111
89
|
Book.all.should_not be_empty
|
112
90
|
end
|
113
|
-
|
91
|
+
|
114
92
|
it 'should receive one Resource for each entity in the XML' do
|
115
93
|
Book.all.size.should == 2
|
116
94
|
end
|
117
|
-
|
95
|
+
|
118
96
|
it 'should do an HTTP GET' do
|
119
97
|
@adapter.should_receive(:http_get).and_return(@response)
|
120
|
-
Book.
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
describe 'when updating an existing resource' do
|
125
|
-
before do
|
126
|
-
@books_xml = "<book><id type='integer'>42</id><title>The Dispossed</title><author>Ursula K LeGuin</author><created-at type='datetime'>2008-06-08T17:02:28Z</created-at></book>"
|
127
|
-
@book = Book.new(:id => 42,
|
128
|
-
:title => 'The Dispossed',
|
129
|
-
:author => 'Ursula K LeGuin',
|
130
|
-
:created_at => DateTime.parse('2008-06-08T17:02:28Z'))
|
131
|
-
@book.stub!(:new_record?).and_return(false)
|
132
|
-
@book.stub!(:dirty?).and_return(true)
|
133
|
-
end
|
134
|
-
|
135
|
-
it 'should do an HTTP PUT' do
|
136
|
-
@adapter.should_receive(:http_put).with('/books/42.xml', @book.to_xml)
|
137
|
-
@book.save
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
describe 'when deleting an existing resource' do
|
142
|
-
before do
|
143
|
-
@book = Book.new(:title => 'Hello, World!', :author => 'Anonymous')
|
144
|
-
@book.stub!(:new_record?).and_return(false)
|
145
|
-
end
|
146
|
-
|
147
|
-
it 'should do an HTTP DELETE' do
|
148
|
-
@adapter.should_receive(:http_delete)
|
149
|
-
@book.destroy
|
150
|
-
end
|
151
|
-
|
98
|
+
Book.first
|
99
|
+
end
|
152
100
|
end
|
153
|
-
end
|
101
|
+
end
|
data/spec/ruby_forker.rb
CHANGED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname.parent.expand_path + 'lib/rest_adapter'
|
3
|
+
|
4
|
+
DataMapper.setup(:default, {
|
5
|
+
:adapter => 'rest',
|
6
|
+
:format => 'xml',
|
7
|
+
:host => 'localhost',
|
8
|
+
:port => '3001'
|
9
|
+
})
|
10
|
+
|
11
|
+
class Book
|
12
|
+
include DataMapper::Resource
|
13
|
+
property :author, String
|
14
|
+
property :created_at, DateTime
|
15
|
+
property :id, Integer, :serial => true
|
16
|
+
property :title, String
|
17
|
+
end
|
data/spec/update_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'A REST adapter' do
|
5
|
+
|
6
|
+
describe 'when updating an existing resource' do
|
7
|
+
before do
|
8
|
+
@books_xml = <<-XML
|
9
|
+
<book>
|
10
|
+
<id type='integer'>42</id>
|
11
|
+
<title>Starship Troopers</title>
|
12
|
+
<author>Robert Heinlein</author>
|
13
|
+
<created-at type='datetime'>2008-06-08T17:02:28Z</created-at>
|
14
|
+
</book>
|
15
|
+
XML
|
16
|
+
repository do |repo|
|
17
|
+
@repository = repo
|
18
|
+
@book = Book.new(:id => 42,
|
19
|
+
:title => 'Starship Troopers',
|
20
|
+
:author => 'Robert Heinlein',
|
21
|
+
:created_at => DateTime.parse('2008-06-08T17:02:28Z'))
|
22
|
+
@book.instance_eval { @new_record = false }
|
23
|
+
@repository.identity_map(Book).set(@book.key, @book)
|
24
|
+
@book.title = "Mary Had a Little Lamb"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should do an HTTP PUT' do
|
29
|
+
adapter = @repository.adapter #DataMapper::Repository.adapters[:default]
|
30
|
+
adapter.should_receive(:http_put).with('/books/42.xml', @book.to_xml)
|
31
|
+
@repository.scope do
|
32
|
+
@book.save
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/stories/all.rb
ADDED
data/stories/crud/create
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
Story: remote app wants to create a resource
|
2
|
+
As a remote app
|
3
|
+
I want to create a resource
|
4
|
+
So that I can store some data about a particular item
|
5
|
+
|
6
|
+
Scenario: remote app supplies a new valid Resource
|
7
|
+
Given a valid DataMapper::Resource
|
8
|
+
When I try to save the Resource
|
9
|
+
Then the Resource should save
|
10
|
+
|
11
|
+
Scenario: remote app supplies a new invalid Resource
|
12
|
+
Given a valid DataMapper::Resource
|
13
|
+
When I make invalid changes to that Resource
|
14
|
+
When I try to save the Resource
|
15
|
+
Then the Resource should not save
|
16
|
+
|
17
|
+
Scenario: remote app supplies a new valid Resource associated with another new valid Resource
|
18
|
+
Given a new Resource
|
19
|
+
And another new Resource associated with the first
|
20
|
+
When I try to save the first Resource
|
21
|
+
Then both Resources should save
|
22
|
+
|
23
|
+
Scenario: remote app supplies a new invalid Resource associated with another new valid Resource
|
24
|
+
Given a new invalid Resouce
|
25
|
+
And another new Resource associated with the first
|
26
|
+
When I try to save the first Resource
|
27
|
+
Neither Resource should save
|
28
|
+
|
29
|
+
Scenario: remote app supplies a new valid Resource associated with another new invalid Resource
|
30
|
+
Given a new Resource
|
31
|
+
And another new invalid Resource associted with the first
|
32
|
+
When I try to save the first Resource
|
33
|
+
Neither Resource should save
|
34
|
+
|
35
|
+
Scenario: remote app supplies a new invalid Resource associated with another new invalid Resource
|
36
|
+
Given a new invalid Resource
|
37
|
+
And another new invalid Resource associted with the first
|
38
|
+
When I try to save the first Resource
|
39
|
+
Neither Resource should save
|
data/stories/crud/delete
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Story: remote app wants to delete a Resource
|
2
|
+
As a remote app
|
3
|
+
I want to delete a Resource
|
4
|
+
So that the information will no longer be available
|
5
|
+
|
6
|
+
Scenario: remote app attempts to delete a Resource by ID
|
7
|
+
Given a local representation of a remote Resource
|
8
|
+
When I destroy the Resource
|
9
|
+
Then the Resource will no longer be available
|
data/stories/crud/read
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Story: remote app wants to access one or more Resources
|
2
|
+
As a remote app
|
3
|
+
I want to get one or more Resources
|
4
|
+
So that I can take an action based upon the information
|
5
|
+
|
6
|
+
Scenario: GET <resource>
|
7
|
+
Given a type of Resource
|
8
|
+
When I request all of the Resources of that type
|
9
|
+
Then I should not receive an empty list
|
10
|
+
|
11
|
+
Scenario: GET <resource>/<id>
|
12
|
+
Given a type of Resource
|
13
|
+
And the ID of an existing Resource
|
14
|
+
When I request the Resource
|
15
|
+
Then I should receive that Resource
|
16
|
+
|
17
|
+
Scenario: GET <resource>/<invalid id>
|
18
|
+
Given a type of Resource
|
19
|
+
And the ID of a nonexistent Resource
|
20
|
+
When I request the Resource
|
21
|
+
Then I should get nothing in return
|
22
|
+
|
23
|
+
Scenario: GET <nested resource>/<id>
|
24
|
+
Given a Resource that returns associated resources
|
25
|
+
And the ID of an existing Resource that has associated Resources
|
26
|
+
And I have all of the necessary class definitions
|
27
|
+
When I GET <nested resource>/<id>
|
28
|
+
Then I should get the Resource
|
29
|
+
And the Resource will have associated Resources
|
30
|
+
|
31
|
+
Scenario: GET <nested resource>/<id> but we are missing some class definitions
|
32
|
+
Given a Resource that returns associated resources
|
33
|
+
And the ID of an existing Resource that has associated Resources
|
34
|
+
And I do not have all of the necessary class definitions
|
35
|
+
When I GET <nested resource>/<id>
|
36
|
+
Then I should get an Exception
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. helper])
|
2
|
+
|
3
|
+
Dir["#{File.dirname(__FILE__)}/*"].each do |file|
|
4
|
+
unless (file =~ /\.rb$/)
|
5
|
+
file_name = file.split('/').last
|
6
|
+
begin
|
7
|
+
require File.join(File.dirname(__FILE__), "..", "resources", "steps", file_name)
|
8
|
+
rescue LoadError
|
9
|
+
puts "Can't find #{file_name}.rb to define story steps; assuming this is intentional"
|
10
|
+
rescue Exception => e
|
11
|
+
puts e.backtrace
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
with_steps_for :using_rest_adapter, file_name.to_sym do
|
15
|
+
run file if File.file?(file)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/stories/crud/update
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
Story: remote app wants to update a resource
|
2
|
+
As a remote app
|
3
|
+
I want to modify a resource
|
4
|
+
So that it's information represents the correct state
|
5
|
+
|
6
|
+
Scenario: remote app updates with changes made to a resource's state
|
7
|
+
Given a local representation of a remote Resource
|
8
|
+
When I make valid changes to that Resource
|
9
|
+
And I try to save the Resource
|
10
|
+
Then the Resource should save
|
11
|
+
|
12
|
+
Scenario: remote app updates with invalid changes to a resource's state
|
13
|
+
Given a local representation of a remote Resource
|
14
|
+
When I make invalid changes to that Resource
|
15
|
+
And I try to save the Resource
|
16
|
+
Then the Resource should not save
|
17
|
+
|
18
|
+
Scenario: remote app updates with changes to an object and one of its children
|
19
|
+
Given a local representation of a remote Resource
|
20
|
+
And a local representation of one of that Resource's child Resources
|
21
|
+
When I make valid changes to the parent Resource
|
22
|
+
And I make valid changes to the child Resource
|
23
|
+
Then both Resources should save
|
24
|
+
|
25
|
+
Scenario: remote app updates with valid changes to an object and invalid changes to one of its children
|
26
|
+
Given a local representation of a remote Resource
|
27
|
+
And a local representation of one of that Resource's child Resources
|
28
|
+
When I make valid changes to the parent Resource
|
29
|
+
And I make invalid changes to the child Resource
|
30
|
+
Then neither Resource should save
|
31
|
+
|
32
|
+
Scenario: remote app updates with invalid changes to an object and valid changes to one of its children
|
33
|
+
Given a local representation of a remote Resource
|
34
|
+
And a local representation of one of that Resource's child Resources
|
35
|
+
When I make invalid changes to the parent Resource
|
36
|
+
And I make valid changes to the child Resource
|
37
|
+
Then neither Resource should save
|
38
|
+
|
39
|
+
Scenario: remote app updates with invalid changes to an object and invalid changes to one of its children
|
40
|
+
Given a local representation of a remote Resource
|
41
|
+
And a local representation of one of that Resource's child Resources
|
42
|
+
When I make invalid changes to the parent Resource
|
43
|
+
And I make invalid changes to the child Resource
|
44
|
+
Then neither Resource should save
|
data/stories/helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("#{File.dirname(__FILE__)}/../lib/rest_adapter")
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'dm-core'
|
6
|
+
#require 'pathname'
|
7
|
+
#require Pathname(__FILE__).dirname.parent.expand_path + '../../lib/rest_adapter'
|
8
|
+
require File.join(File.dirname(__FILE__), *%w[resources helpers story_helper])
|
9
|
+
require File.join(File.dirname(__FILE__), *%w[resources steps using_rest_adapter])
|
@@ -0,0 +1,35 @@
|
|
1
|
+
steps_for :read do
|
2
|
+
Given "a Resource that returns associated resources" do
|
3
|
+
class Shelf
|
4
|
+
include DataMapper::Resource
|
5
|
+
property :id, Integer, :serial => true
|
6
|
+
property :name, String
|
7
|
+
has n, :books
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Given "the ID of an existing Resource that has associated Resources" do
|
12
|
+
# Assuming that resource 1 is there.
|
13
|
+
# @type.first would do a GET; that's what we're testing
|
14
|
+
@resource_id = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
Given "I have all of the necessary class definitions" do
|
18
|
+
# NO-OP because defined above
|
19
|
+
end
|
20
|
+
|
21
|
+
When "I GET <nested resource>/<id>" do
|
22
|
+
@resource = Shelf.get(@resource_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
Then "I should get the Resource" do
|
26
|
+
@resource.should_not be_nil
|
27
|
+
@resource.should be_an_instance_of(Shelf)
|
28
|
+
@resource.id.should == 1
|
29
|
+
end
|
30
|
+
|
31
|
+
Then "the Resource will have associated Resources" do
|
32
|
+
@resource.books.should_not be_empty
|
33
|
+
@resource.books.first.should be_an_instance_of(Book)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
|
3
|
+
steps_for :using_rest_adapter do
|
4
|
+
#To run these specs, you must have a REST Application running on port 3001 on your localhost
|
5
|
+
#The app is expected to implement the REST API for CRUD operations on a Book
|
6
|
+
|
7
|
+
DataMapper.setup(:default, {
|
8
|
+
:adapter => 'rest',
|
9
|
+
:format => 'xml',
|
10
|
+
:host => 'localhost',
|
11
|
+
:port => '3001'
|
12
|
+
})
|
13
|
+
|
14
|
+
Given("a valid DataMapper::Resource") do
|
15
|
+
# TODO refactor
|
16
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
17
|
+
@resource = new_book
|
18
|
+
end
|
19
|
+
|
20
|
+
Given("a type of Resource") do
|
21
|
+
# TODO refactor
|
22
|
+
# require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
23
|
+
# Book = Book
|
24
|
+
end
|
25
|
+
|
26
|
+
Given("the ID of an existing Resource") do
|
27
|
+
# TODO refactor
|
28
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
29
|
+
@resource_id = Book.first.id
|
30
|
+
end
|
31
|
+
|
32
|
+
Given("the ID of a nonexistent Resource") do
|
33
|
+
@resource_id = 42 ** 3
|
34
|
+
end
|
35
|
+
|
36
|
+
Given("a local representation of a remote Resource") do
|
37
|
+
# TODO refactor
|
38
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
39
|
+
@resource = Book.first
|
40
|
+
@resource_id = @resource.id
|
41
|
+
end
|
42
|
+
|
43
|
+
When("I try to save the Resource") do
|
44
|
+
@result = @resource.save
|
45
|
+
end
|
46
|
+
|
47
|
+
When("I request all of the Resources of that type") do
|
48
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
49
|
+
@resources = Book.all
|
50
|
+
end
|
51
|
+
|
52
|
+
When("I request the Resource") do
|
53
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
54
|
+
@resource = Book.get(@resource_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
When("I make valid changes to that Resource") do
|
58
|
+
@resource.title = "Mary had a little lamb"
|
59
|
+
end
|
60
|
+
|
61
|
+
When("I make invalid changes to that Resource") do
|
62
|
+
@resource.title = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
When("I destroy the Resource") do
|
66
|
+
@resource.destroy
|
67
|
+
end
|
68
|
+
|
69
|
+
Then("the Resource should save") do
|
70
|
+
@result.should be_true
|
71
|
+
end
|
72
|
+
|
73
|
+
Then("the Resource should not save") do
|
74
|
+
@result.should be_false
|
75
|
+
end
|
76
|
+
|
77
|
+
Then("I should not receive an empty list") do
|
78
|
+
@resources.should_not be_empty
|
79
|
+
end
|
80
|
+
|
81
|
+
Then("I should receive that Resource") do
|
82
|
+
@resource.should_not be_nil
|
83
|
+
@resource.id.should == @resource_id
|
84
|
+
end
|
85
|
+
|
86
|
+
Then("I should get nothing in return") do
|
87
|
+
@resource.should be_nil
|
88
|
+
end
|
89
|
+
|
90
|
+
Then("the Resource will no longer be available") do
|
91
|
+
# TODO refactor
|
92
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'book')
|
93
|
+
Book.get(@resource_id).should be_nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def new_book(options={})
|
98
|
+
Book.new(options.merge({:title => "Hello, World!", :author => "Some dude"}))
|
99
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-rest-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Potomac Ruby Hackers
|
@@ -9,42 +9,73 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-07-24 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: dm-core
|
17
|
+
type: :runtime
|
17
18
|
version_requirement:
|
18
19
|
version_requirements: !ruby/object:Gem::Requirement
|
19
20
|
requirements:
|
20
21
|
- - "="
|
21
22
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.9.
|
23
|
+
version: 0.9.3
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.7.0
|
23
34
|
version:
|
24
35
|
description: REST Adapter for DataMapper
|
25
|
-
email:
|
36
|
+
email:
|
37
|
+
- potomac-ruby-hackers@googlegroups.com
|
26
38
|
executables: []
|
27
39
|
|
28
40
|
extensions: []
|
29
41
|
|
30
42
|
extra_rdoc_files:
|
31
|
-
- README
|
43
|
+
- README.txt
|
32
44
|
- LICENSE
|
33
45
|
- TODO
|
34
46
|
files:
|
47
|
+
- History.txt
|
48
|
+
- LICENSE
|
49
|
+
- Manifest.txt
|
50
|
+
- README.txt
|
51
|
+
- Rakefile
|
52
|
+
- TODO
|
35
53
|
- lib/rest_adapter.rb
|
36
|
-
-
|
54
|
+
- lib/rest_adapter/version.rb
|
55
|
+
- spec/create_spec.rb
|
56
|
+
- spec/delete_spec.rb
|
57
|
+
- spec/read_spec.rb
|
37
58
|
- spec/ruby_forker.rb
|
38
59
|
- spec/spec.opts
|
39
|
-
-
|
40
|
-
-
|
41
|
-
-
|
42
|
-
-
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
- spec/update_spec.rb
|
62
|
+
- stories/all.rb
|
63
|
+
- stories/crud/create
|
64
|
+
- stories/crud/delete
|
65
|
+
- stories/crud/read
|
66
|
+
- stories/crud/stories.rb
|
67
|
+
- stories/crud/update
|
68
|
+
- stories/helper.rb
|
69
|
+
- stories/resources/helpers/book.rb
|
70
|
+
- stories/resources/helpers/story_helper.rb
|
71
|
+
- stories/resources/steps/read.rb
|
72
|
+
- stories/resources/steps/using_rest_adapter.rb
|
43
73
|
has_rdoc: true
|
44
74
|
homepage: http://github.com/pjb3/dm-more/tree/master/adapters/dm-rest-adapter
|
45
75
|
post_install_message:
|
46
|
-
rdoc_options:
|
47
|
-
|
76
|
+
rdoc_options:
|
77
|
+
- --main
|
78
|
+
- README.txt
|
48
79
|
require_paths:
|
49
80
|
- lib
|
50
81
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -61,8 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
92
|
version:
|
62
93
|
requirements: []
|
63
94
|
|
64
|
-
rubyforge_project:
|
65
|
-
rubygems_version: 1.0
|
95
|
+
rubyforge_project: datamapper
|
96
|
+
rubygems_version: 1.2.0
|
66
97
|
signing_key:
|
67
98
|
specification_version: 2
|
68
99
|
summary: REST Adapter for DataMapper
|