dm-rest-adapter 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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
+