dm-rest-adapter 0.9.2

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Potomac Ruby Hackers
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ dm-rest-adapter
2
+ ==================
3
+
4
+ A DataMapper adapter for REST Web Services
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'rake/clean'
4
+ require 'rake/gempackagetask'
5
+ require 'spec/rake/spectask'
6
+ require 'pathname'
7
+
8
+ CLEAN.include '{log,pkg}/'
9
+
10
+ spec = Gem::Specification.new do |s|
11
+ s.name = 'dm-rest-adapter'
12
+ s.version = '0.9.2'
13
+ s.platform = Gem::Platform::RUBY
14
+ s.has_rdoc = true
15
+ s.extra_rdoc_files = %w[ README LICENSE TODO ]
16
+ s.summary = 'REST Adapter for DataMapper'
17
+ s.description = s.summary
18
+ s.author = 'Potomac Ruby Hackers'
19
+ s.email = 'potomac-ruby-hackers@googlegroups.com'
20
+ s.homepage = 'http://github.com/pjb3/dm-more/tree/master/adapters/dm-rest-adapter'
21
+ s.require_path = 'lib'
22
+ s.files = FileList[ '{lib,spec}/**/*.rb', 'spec/spec.opts', 'Rakefile', *s.extra_rdoc_files ]
23
+ s.add_dependency('dm-core', "=#{s.version}")
24
+ end
25
+
26
+ task :default => [ :spec ]
27
+
28
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
29
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
30
+
31
+ Rake::GemPackageTask.new(spec) do |pkg|
32
+ pkg.gem_spec = spec
33
+ end
34
+
35
+ desc "Install #{spec.name} #{spec.version}"
36
+ task :install => [ :package ] do
37
+ sh "#{SUDO} gem install pkg/#{spec.name}-#{spec.version} --no-update-sources", :verbose => false
38
+ end
39
+
40
+ desc 'Run specifications'
41
+ Spec::Rake::SpecTask.new(:spec) do |t|
42
+ if File.exists?('spec/spec.opts')
43
+ t.spec_opts << '--options' << 'spec/spec.opts'
44
+ end
45
+ t.spec_files =
46
+ Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
47
+ end
48
+
49
+ desc "Run all stories"
50
+ task :stories do
51
+ ruby "stories/all.rb --colour --format plain"
52
+ end
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ TODO
2
+ ====
@@ -0,0 +1,190 @@
1
+ require 'rubygems'
2
+ gem 'dm-core', '=0.9.2'
3
+ require 'dm-core'
4
+ require 'extlib'
5
+ require 'dm-serializer'
6
+ require 'pathname'
7
+ require 'net/http'
8
+ require 'rexml/document'
9
+
10
+ # TODO: Abstract XML support out from the protocol
11
+ # TODO: Build JSON support
12
+ module DataMapper
13
+ module Adapters
14
+ class RestAdapter < AbstractAdapter
15
+ 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
+
29
+ # Creates a new resource in the specified repository.
30
+ def create(resources)
31
+ success = true
32
+ resources.each do |resource|
33
+ resource_name = Inflection.underscore(resource.class.name.downcase)
34
+ result = http_post("/#{resource_name.pluralize}.xml", resource.to_xml)
35
+ # TODO: Raise error if cannot reach server
36
+ success = success && result.instance_of?(Net::HTTPCreated)
37
+ if success
38
+ updated_resource = parse_resource(result.body, resource.class)
39
+ resource.id = updated_resource.id
40
+ end
41
+ # TODO: We're not using the response to update the DataMapper::Resource with the newly acquired ID!!!
42
+ end
43
+ success
44
+ end
45
+
46
+ # read_set
47
+ #
48
+ # Examples of query string:
49
+ # A. []
50
+ # GET /books/
51
+ #
52
+ # B. [[:eql, #<Property:Book:id>, 4200]]
53
+ # GET /books/4200
54
+ #
55
+ # IN PROGRESS
56
+ # TODO: Need to account for query.conditions (i.e., [[:eql, #<Property:Book:id>, 1]] for books/1)
57
+ def read_many(query)
58
+ resource_name = Inflection.underscore(query.model.name.downcase)
59
+ case query.conditions
60
+ when []
61
+ read_set_all(repository, query, resource_name)
62
+ else
63
+ read_set_for_condition(repository, query, resource)
64
+ end
65
+ end
66
+
67
+ def read_one(query)
68
+ # puts "---------------- QUERY: #{query} #{query.inspect}"
69
+ id = query.conditions.first[2]
70
+ # KLUGE: Again, we're assuming below that we're dealing with a pluralized resource mapping
71
+ resource_name = resource_name_from_query(query)
72
+ response = http_get("/#{resource_name.pluralize}/#{id}.xml")
73
+
74
+ # KLUGE: Rails returns HTML if it can't find a resource. A properly RESTful app would return a 404, right?
75
+ return nil if response.is_a? Net::HTTPNotFound || response.content_type == "text/html"
76
+
77
+ data = response.body
78
+ res = parse_resource(data, query.model)
79
+ res
80
+ end
81
+
82
+ def update(attributes, query)
83
+ # TODO update for v0.9.2
84
+ raise NotImplementedError.new unless is_single_resource_query? query
85
+ id = query.conditions.first[2]
86
+ resource = query.model.new
87
+ attributes.each do |attr, val|
88
+ resource.send("#{attr.name}=", val)
89
+ end
90
+ # 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)
92
+ # TODO: Raise error if cannot reach server
93
+ end
94
+
95
+ def delete(query)
96
+ #puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>> QUERY: #{query} #{query.inspect}"
97
+ # TODO update for v0.9.2
98
+ raise NotImplementedError.new unless is_single_resource_query? query
99
+ id = query.conditions.first[2]
100
+ http_delete("/#{resource_name_from_query(query).pluralize}/#{id}.xml")
101
+ end
102
+
103
+ protected
104
+ def read_set_all(repository, query, resource_name)
105
+ # TODO: how do we know whether the resource we're talking to is singular or plural?
106
+ res = http_get("/#{resource_name.pluralize}.xml")
107
+ data = res.body
108
+ parse_resources(data, query.model)
109
+ # TODO: Raise error if cannot reach server
110
+ end
111
+
112
+ # GET /books/4200
113
+ def read_set_for_condition(repository, query, resource_name)
114
+ # More complex conditions
115
+ raise NotImplementedError.new
116
+ end
117
+
118
+ # query.conditions like [[:eql, #<Property:Book:id>, 4200]]
119
+ def is_single_resource_query?(query)
120
+ query.conditions.length == 1 && query.conditions.first.first == :eql && query.conditions.first[1].name == :id
121
+ end
122
+
123
+ def http_put(uri, data = nil)
124
+ request { |http| http.put(uri, data) }
125
+ end
126
+
127
+ def http_post(uri, data)
128
+ request { |http| http.post(uri, data, {"Content-Type", "application/xml"}) }
129
+ end
130
+
131
+ def http_get(uri)
132
+ request { |http| http.get(uri) }
133
+ end
134
+
135
+ def http_delete(uri)
136
+ request { |http| http.delete(uri) }
137
+ end
138
+
139
+ def request(&block)
140
+ res = nil
141
+ Net::HTTP.start(@uri[:host], @uri[:port].to_i) do |http|
142
+ res = yield(http)
143
+ end
144
+ res
145
+ end
146
+
147
+ def resource_from_rexml(entity_element, dm_model_class)
148
+ resource = dm_model_class.new
149
+ entity_element.elements.each do |field_element|
150
+ attribute = resource.attributes.find do |name, val|
151
+ # *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)
153
+ end
154
+ resource.send("#{Inflection.underscore(attribute[0])}=", field_element.text) if attribute
155
+ end
156
+ resource.instance_eval { @new_record= false }
157
+ resource
158
+ end
159
+
160
+ def parse_resource(xml, dm_model_class)
161
+ doc = REXML::Document::new(xml)
162
+ # TODO: handle singular resource case as well....
163
+ entity_element = REXML::XPath.first(doc, "/#{resource_name_from_model(dm_model_class)}")
164
+ return nil unless entity_element
165
+ resource_from_rexml(entity_element, dm_model_class)
166
+ end
167
+
168
+ def parse_resources(xml, dm_model_class)
169
+ doc = REXML::Document::new(xml)
170
+ # # TODO: handle singular resource case as well....
171
+ # array = XPath(doc, "/*[@type='array']")
172
+ # if array
173
+ # parse_resources()
174
+ # else
175
+ resource_name = resource_name_from_model dm_model_class
176
+ doc.elements.collect("#{resource_name.pluralize}/#{resource_name}") do |entity_element|
177
+ resource_from_rexml(entity_element, dm_model_class)
178
+ end
179
+ end
180
+
181
+ def resource_name_from_model(model)
182
+ Inflection.underscore(model.name.downcase)
183
+ end
184
+
185
+ def resource_name_from_query(query)
186
+ resource_name_from_model(query.model)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,153 @@
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 :id, Integer, :serial => true
14
+ property :title, String
15
+ property :author, String
16
+ property :created_at, DateTime
17
+ end
18
+
19
+ describe 'A REST adapter' do
20
+
21
+ before do
22
+ @adapter = DataMapper::Repository.adapters[:default]
23
+ 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
+
37
+ describe 'when getting one resource' do
38
+
39
+ describe 'if the resource exists' do
40
+
41
+ before do
42
+ book_xml = <<-BOOK
43
+ <?xml version='1.0' encoding='UTF-8'?>
44
+ <book>
45
+ <author>Stephen King</author>
46
+ <created-at type='datetime'>2008-06-08T17:03:07Z</created-at>
47
+ <id type='integer'>1</id>
48
+ <title>The Shining</title>
49
+ <updated-at type='datetime'>2008-06-08T17:03:07Z</updated-at>
50
+ </book>
51
+ BOOK
52
+ @id = 1
53
+ @response = mock(Net::HTTPResponse)
54
+ @response.stub!(:body).and_return(book_xml)
55
+ @adapter.stub!(:http_get).and_return(@response)
56
+ end
57
+
58
+ it 'should return the resource' do
59
+ book = Book.get(@id)
60
+ book.should_not be_nil
61
+ book.id.should be_an_instance_of(Fixnum)
62
+ book.id.should == 1
63
+ end
64
+
65
+ it 'should do an HTTP GET' do
66
+ @adapter.should_receive(:http_get).with('/books/1.xml').and_return(@response)
67
+ Book.get(@id)
68
+ end
69
+ end
70
+
71
+ describe 'if the resource does not exist' do
72
+ it 'should return nil' do
73
+ @id = 1
74
+ @response = mock(Net::HTTPNotFound)
75
+ @response.stub!(:content_type).and_return('text/html')
76
+ @response.stub!(:body).and_return('<html></html>')
77
+ @adapter.stub!(:http_get).and_return(@response)
78
+ id = 4200
79
+ Book.get(id).should be_nil
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'when getting all resource of a particular type' do
85
+ before do
86
+ books_xml = <<-BOOK
87
+ <?xml version='1.0' encoding='UTF-8'?>
88
+ <books type='array'>
89
+ <book>
90
+ <author>Ursula K LeGuin</author>
91
+ <created-at type='datetime'>2008-06-08T17:02:28Z</created-at>
92
+ <id type='integer'>1</id>
93
+ <title>The Dispossed</title>
94
+ <updated-at type='datetime'>2008-06-08T17:02:28Z</updated-at>
95
+ </book>
96
+ <book>
97
+ <author>Stephen King</author>
98
+ <created-at type='datetime'>2008-06-08T17:03:07Z</created-at>
99
+ <id type='integer'>2</id>
100
+ <title>The Shining</title>
101
+ <updated-at type='datetime'>2008-06-08T17:03:07Z</updated-at>
102
+ </book>
103
+ </books>
104
+ BOOK
105
+ @response = mock(Net::HTTPResponse)
106
+ @response.stub!(:body).and_return(books_xml)
107
+ @adapter.stub!(:http_get).and_return(@response)
108
+ end
109
+
110
+ it 'should get a non-empty list' do
111
+ Book.all.should_not be_empty
112
+ end
113
+
114
+ it 'should receive one Resource for each entity in the XML' do
115
+ Book.all.size.should == 2
116
+ end
117
+
118
+ it 'should do an HTTP GET' do
119
+ @adapter.should_receive(:http_get).and_return(@response)
120
+ Book.all
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
+
152
+ end
153
+ end
@@ -0,0 +1,13 @@
1
+ require 'rbconfig'
2
+
3
+ module RubyForker
4
+ # Forks a ruby interpreter with same type as ourself.
5
+ # juby will fork jruby, ruby will fork ruby etc.
6
+ def ruby(args, stderr=nil)
7
+ config = ::Config::CONFIG
8
+ interpreter = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT']
9
+ cmd = "#{interpreter} #{args}"
10
+ cmd << " 2> #{stderr}" unless stderr.nil?
11
+ `#{cmd}`
12
+ end
13
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-rest-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.2
5
+ platform: ruby
6
+ authors:
7
+ - Potomac Ruby Hackers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-25 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - "="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.2
23
+ version:
24
+ description: REST Adapter for DataMapper
25
+ email: potomac-ruby-hackers@googlegroups.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ - LICENSE
33
+ - TODO
34
+ files:
35
+ - lib/rest_adapter.rb
36
+ - spec/rest_adapter_spec.rb
37
+ - spec/ruby_forker.rb
38
+ - spec/spec.opts
39
+ - Rakefile
40
+ - README
41
+ - LICENSE
42
+ - TODO
43
+ has_rdoc: true
44
+ homepage: http://github.com/pjb3/dm-more/tree/master/adapters/dm-rest-adapter
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.0.1
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: REST Adapter for DataMapper
69
+ test_files: []
70
+