dm-rest-adapter 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|