Roman2K-web-service 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +7 -0
- data/README.mdown +147 -0
- data/Rakefile +39 -0
- data/lib/web_service.rb +36 -0
- data/lib/web_service/attribute_accessors.rb +147 -0
- data/lib/web_service/core_ext.rb +36 -0
- data/lib/web_service/crud_operations.rb +114 -0
- data/lib/web_service/named_request_methods.rb +19 -0
- data/lib/web_service/remote_collection.rb +155 -0
- data/lib/web_service/resource.rb +177 -0
- data/lib/web_service/response_handling.rb +148 -0
- data/lib/web_service/site.rb +40 -0
- data/test/test_helper.rb +59 -0
- data/test/web_service/attribute_accessors_test.rb +236 -0
- data/test/web_service/core_ext_test.rb +13 -0
- data/test/web_service/crud_operations_test.rb +61 -0
- data/test/web_service/named_request_methods_test.rb +34 -0
- data/test/web_service/remote_collection_test.rb +114 -0
- data/test/web_service/resource_test.rb +200 -0
- data/test/web_service/response_handling_test.rb +66 -0
- data/test/web_service/site_test.rb +62 -0
- data/web-service.gemspec +44 -0
- metadata +137 -0
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2008-2009 Roman Le Négrate
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.mdown
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# WebService
|
2
|
+
|
3
|
+
REST client. Alternative to [`ActiveResource`](http://api.rubyonrails.org/classes/ActiveResource/Base.html).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Since `WebService` depends on the gem `class-inheritable-attributes` which cannot be installed directly by RubyGems because the account name has to be prepended to the actual gem name, please refer to [`README`](http://github.com/Roman2K/class-inheritable-attributes/tree) for instructions on how to install that dependency manually.
|
8
|
+
|
9
|
+
Then, install the `web-service` gem itself:
|
10
|
+
|
11
|
+
$ gem install Roman2K-web-service -s http://gems.github.com/
|
12
|
+
|
13
|
+
## Usage example
|
14
|
+
|
15
|
+
Let's say you have a Rails app exposing a RESTful API for articles and their comments:
|
16
|
+
|
17
|
+
map.resources :articles, :has_many => :comments
|
18
|
+
|
19
|
+
When building the corresponding client for this API to consume these resources, the code could look like:
|
20
|
+
|
21
|
+
require 'web_service'
|
22
|
+
|
23
|
+
class BasicResource < WebService::Resource
|
24
|
+
# Mandatory:
|
25
|
+
self.site = WebService::Site.new("http://login:password@host:port")
|
26
|
+
|
27
|
+
# Optional (and possibly in subclasses):
|
28
|
+
self.site = WebService::Site.new("https://secure.site")
|
29
|
+
self.credentials = ["login", "password"]
|
30
|
+
self.element_name = "custom_element_name"
|
31
|
+
self.singleton = true # /foo instead of /foos/1
|
32
|
+
end
|
33
|
+
|
34
|
+
class Article < BasicResource
|
35
|
+
has_many :comments
|
36
|
+
end
|
37
|
+
|
38
|
+
class Comment < BasicResource
|
39
|
+
belongs_to :article
|
40
|
+
end
|
41
|
+
|
42
|
+
### Attributes
|
43
|
+
|
44
|
+
article = Article.new(:title => "First article")
|
45
|
+
article.title # => "First article"
|
46
|
+
article.title? # => true
|
47
|
+
article.title = " "
|
48
|
+
article.title? # => false
|
49
|
+
article.body # raises NoMethodError
|
50
|
+
article.body? # raises NoMethodError
|
51
|
+
|
52
|
+
### CRUD
|
53
|
+
|
54
|
+
article = Article.new(:title => "First article")
|
55
|
+
begin
|
56
|
+
article.save
|
57
|
+
# => POST /articles
|
58
|
+
rescue WebService::ResourceInvalid
|
59
|
+
article.body = "Like the title says."
|
60
|
+
article.save
|
61
|
+
# => POST /articles
|
62
|
+
end
|
63
|
+
|
64
|
+
article.body += " Updated."
|
65
|
+
article.save
|
66
|
+
# => PUT /articles/1
|
67
|
+
|
68
|
+
article.destroy
|
69
|
+
# => DELETE /articles/1
|
70
|
+
|
71
|
+
### Fetching
|
72
|
+
|
73
|
+
article.id # => 1
|
74
|
+
Article[1] == article # => true
|
75
|
+
Article[99] # => nil
|
76
|
+
Article.find(99) # raises WebService::ResourceNotFound
|
77
|
+
|
78
|
+
Article.all # => [Article[1], ..., Article[42]]
|
79
|
+
Article.first # => Article[1]
|
80
|
+
Article.last # => Article[42]
|
81
|
+
|
82
|
+
comment = Comment.new(:article_id => 1)
|
83
|
+
comment.article # => Article[1]
|
84
|
+
# => GET /articles/1
|
85
|
+
comment.article # => Article[1]
|
86
|
+
# => (cache hit)
|
87
|
+
|
88
|
+
### Nested resources (associations)
|
89
|
+
|
90
|
+
Use `article.comments` like you would `Comment`, only scoped to `article`.
|
91
|
+
|
92
|
+
article.comments.all
|
93
|
+
# => GET /articles/1/comments
|
94
|
+
|
95
|
+
Associating a comment with a article:
|
96
|
+
|
97
|
+
article.comments.create(:body => "First comment.")
|
98
|
+
# => POST /articles/1/comments
|
99
|
+
|
100
|
+
The same in a more formal way:
|
101
|
+
|
102
|
+
comment = Comment.new(:body => "First comment.")
|
103
|
+
comment.article = article
|
104
|
+
comment.article == article # => true
|
105
|
+
comment.article_id # => 1
|
106
|
+
comment.save
|
107
|
+
|
108
|
+
### Arbitrary actions
|
109
|
+
|
110
|
+
All of resource classes, resource instances and association collections respond to all four HTTP verbs (`GET`, `POST`, `PUT` and `DELETE`) to issue requests to arbitrary actions.
|
111
|
+
|
112
|
+
**Note:** these methods do not return resource instances, but rather plain unserialized objects, as returned by the server:
|
113
|
+
|
114
|
+
Article.delete(:unpopular) # => [{'id' => 5, ...}, ...]
|
115
|
+
# => DELETE /articles/unpopular
|
116
|
+
|
117
|
+
Article.get(:popular, :page => 2) # => [{'id' => 1, ...}, ...]
|
118
|
+
# => GET /articles/popular?page=2
|
119
|
+
|
120
|
+
article.put(:publish, :at => 1.hour.from_now)
|
121
|
+
# => PUT /articles/1/publish {:at => 1.hour.from_now}
|
122
|
+
|
123
|
+
article.comments.put(:spam, 5)
|
124
|
+
# => PUT /articles/1/comments/5/spam
|
125
|
+
|
126
|
+
article.comments.get("/spam")
|
127
|
+
# => PUT /articles/1/comments/spam
|
128
|
+
|
129
|
+
## Formats
|
130
|
+
|
131
|
+
* Requests are sent **JSON**-encoded.
|
132
|
+
* Responses can be returned either **JSON**- or **XML**-encoded.
|
133
|
+
|
134
|
+
## Status
|
135
|
+
|
136
|
+
* Fetched resources should be cached in memory, so that the following returns true:
|
137
|
+
|
138
|
+
comments = article.comments
|
139
|
+
comments.all.object_id == comments.all.object_id
|
140
|
+
|
141
|
+
* There are **tests** left to be written. Run `rake coverage` to find out what still has to be tested. Note that for RCov to work with Ruby 1.8.7, you might need to install the latest `rcov` gem from [GitHub](http://github.com/spicycode/rcov):
|
142
|
+
|
143
|
+
gem install spicycode-rcov -s http://gems.github.com
|
144
|
+
|
145
|
+
## Credits
|
146
|
+
|
147
|
+
Written by [Roman Le Négrate](http://roman.flucti.com) ([contact](mailto:roman.lenegrate@gmail.com)). Released under the MIT license: see the `LICENSE` file.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "echoe"
|
2
|
+
|
3
|
+
Echoe.new('web-service', '0.1.1') do |p|
|
4
|
+
p.description = "REST client; an alternative to ActiveResource"
|
5
|
+
p.url = "https://github.com/Roman2K/web-service"
|
6
|
+
p.author = "Roman Le Négrate"
|
7
|
+
p.email = "roman.lenegrate@gmail.com"
|
8
|
+
p.ignore_pattern = "*.gemspec"
|
9
|
+
p.dependencies = ["activesupport >=2.2.2", "class-inheritable-attributes"]
|
10
|
+
p.development_dependencies = ["mocha", "test-unit-ext"]
|
11
|
+
p.rdoc_options = %w(--main README.mdown --inline-source --line-numbers --charset UTF-8)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Weirdly enough, Echoe's default `test' task doesn't get overridden by the one
|
15
|
+
# defined below. Even weirder, `rake test' runs both tasks! The same applies to
|
16
|
+
# `coverage'. Dirty workaround:
|
17
|
+
%w(test coverage).each do |name|
|
18
|
+
Rake.application.instance_eval("@tasks").delete(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
task :default => :test
|
22
|
+
|
23
|
+
desc "Run the test suite"
|
24
|
+
task :test do
|
25
|
+
all_test_files.each { |test| require test }
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Measure test coverage"
|
29
|
+
COVERAGE_OUT = "doc/coverage"
|
30
|
+
COVERAGE_CODE = %w(lib)
|
31
|
+
task :coverage do
|
32
|
+
rm_rf COVERAGE_OUT; mkdir_p COVERAGE_OUT
|
33
|
+
sh %(rcov -I.:lib:test -x '^(?!#{COVERAGE_CODE * '|'})/' --text-summary --sort coverage --no-validator-links -o #{COVERAGE_OUT} #{all_test_files * ' '})
|
34
|
+
system %(open #{COVERAGE_OUT}/index.html)
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_test_files
|
38
|
+
Dir['test/**/*_test.rb'].sort
|
39
|
+
end
|
data/lib/web_service.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'class_inheritable_attributes'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'cgi'
|
5
|
+
require 'uri'
|
6
|
+
require 'net/https'
|
7
|
+
require 'web_service/core_ext'
|
8
|
+
|
9
|
+
module WebService
|
10
|
+
autoload :Site, 'web_service/site'
|
11
|
+
autoload :Resource, 'web_service/resource'
|
12
|
+
autoload :AttributeAccessors, 'web_service/attribute_accessors'
|
13
|
+
autoload :RemoteCollection, 'web_service/remote_collection'
|
14
|
+
autoload :ResponseHandling, 'web_service/response_handling'
|
15
|
+
autoload :NamedRequestMethods, 'web_service/named_request_methods'
|
16
|
+
autoload :CRUDOperations, 'web_service/crud_operations'
|
17
|
+
|
18
|
+
class Error < StandardError
|
19
|
+
end
|
20
|
+
class ResourceNotSaved < Error
|
21
|
+
end
|
22
|
+
class NotResourceClass < Error
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def logger
|
27
|
+
@logger ||= begin
|
28
|
+
require 'logger'
|
29
|
+
Logger.new(STDOUT)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
attr_writer :logger
|
33
|
+
end
|
34
|
+
|
35
|
+
include ResponseHandling::Exceptions
|
36
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module WebService
|
2
|
+
module AttributeAccessors
|
3
|
+
def initialize(attributes={})
|
4
|
+
self.attributes = attributes
|
5
|
+
end
|
6
|
+
|
7
|
+
def attributes
|
8
|
+
attribute_registry.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes=(attributes)
|
12
|
+
# Resolve single-key hash
|
13
|
+
attributes = attributes[self.class.element_name] if attributes.size == 1 && attributes[self.class.element_name]
|
14
|
+
|
15
|
+
# Clear previous attributes
|
16
|
+
attribute_registry.clear
|
17
|
+
|
18
|
+
# Assign the ID first, so that it can be used while assigning the rest of the attributes
|
19
|
+
if id_key = [:id, "id"].find { |key| attributes.include? key }
|
20
|
+
self.id = attributes.delete(id_key)
|
21
|
+
end
|
22
|
+
attributes.each { |name, value| send("#{name}=", value) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO handle the argument for whether to include singleton methods or not
|
26
|
+
def methods
|
27
|
+
super | attribute_accessor_methods.to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def respond_to?(method, include_private=false)
|
31
|
+
super || attribute_accessor_methods.include?(MethodList::TO_METHOD_NAME[method])
|
32
|
+
end
|
33
|
+
|
34
|
+
def read_attribute(attr_name)
|
35
|
+
if (id = attribute_registry["#{attr_name}_id"])
|
36
|
+
association_registry[attr_name.to_s] ||= resource_class_for(attr_name).find(id)
|
37
|
+
else
|
38
|
+
attribute_registry[attr_name.to_s] || association_registry[attr_name.to_s]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute_readable?(attr_name)
|
43
|
+
attribute_registry.key?(attr_name) || association_registry.key?(attr_name) || attribute_registry.key?("#{attr_name}_id")
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_attribute(attr_name, value)
|
47
|
+
attr_name = attr_name.to_s
|
48
|
+
resolve_record_descriptor = lambda { |descriptor|
|
49
|
+
if Hash === descriptor && descriptor.size == 1
|
50
|
+
if attributes = descriptor[attr_name]
|
51
|
+
attributes
|
52
|
+
elsif klass = resource_class_for?(descriptor.keys.first)
|
53
|
+
klass.new(descriptor.values.first)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
}
|
57
|
+
value = resolve_record_descriptor[value] || (value.kind_of?(Array) and value.map { |elt| resolve_record_descriptor[elt] || elt }) || value
|
58
|
+
association_registry[$`] &&= nil if attr_name =~ /_id$/
|
59
|
+
if value.nil?
|
60
|
+
attribute_registry[attr_name] = nil
|
61
|
+
attribute_registry["#{attr_name}_id"] &&= nil unless attr_name =~ /_id$/
|
62
|
+
association_registry[attr_name] &&= nil
|
63
|
+
elsif resource_class?(value.class)
|
64
|
+
value.saved? or raise ResourceNotSaved, "resource must have an ID in order to be associated to another resource"
|
65
|
+
attribute_registry["#{attr_name}_id"], association_registry[attr_name] = value.id, value
|
66
|
+
elsif Hash === value and id = value["id"] and klass = resource_class_for?(attr_name)
|
67
|
+
attribute_registry["#{attr_name}_id"], association_registry[attr_name] = id, klass.new(value)
|
68
|
+
else
|
69
|
+
attribute_registry[attr_name] = value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def attribute_set?(attr_name)
|
74
|
+
!attribute_registry[attr_name.to_s].blank? || !attribute_registry["#{attr_name}_id"].blank?
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def attribute_registry
|
80
|
+
@attributes ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def association_registry
|
84
|
+
@associations ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def method_missing(method_id, *args)
|
88
|
+
method_name = method_id.to_s
|
89
|
+
case
|
90
|
+
when method_name =~ /=$/
|
91
|
+
write_attribute($`, *args)
|
92
|
+
when attribute_readable?(method_name)
|
93
|
+
read_attribute(method_name, *args)
|
94
|
+
when method_name =~ /\?$/
|
95
|
+
super unless respond_to?($`)
|
96
|
+
attribute_set?($`, *args)
|
97
|
+
else
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def attribute_accessor_methods
|
103
|
+
@attribute_accessor_methods ||= MethodList.new([attribute_registry, association_registry])
|
104
|
+
end
|
105
|
+
|
106
|
+
def resource_class_for(association_name)
|
107
|
+
klass = association_name.to_s.camelize.constantize
|
108
|
+
unless resource_class?(klass)
|
109
|
+
raise NotResourceClass, "class #{klass} found for association `#{association_name}' is not a resource class"
|
110
|
+
end
|
111
|
+
klass
|
112
|
+
end
|
113
|
+
|
114
|
+
def resource_class_for?(association_name)
|
115
|
+
resource_class_for(association_name)
|
116
|
+
rescue NameError
|
117
|
+
raise unless /uninitialized constant/ =~ $!
|
118
|
+
rescue NotResourceClass
|
119
|
+
raise unless /for association `#{association_name}'/ =~ $!
|
120
|
+
end
|
121
|
+
|
122
|
+
def resource_class?(klass)
|
123
|
+
Class === klass && klass.respond_to?(:find) && klass.public_method_defined?(:saved?)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class MethodList
|
128
|
+
TO_METHOD_NAME = (Kernel.methods.first.kind_of?(String) ? :to_s : :to_sym).to_proc
|
129
|
+
METHOD_SUFFIXES = ["", "=", "?"]
|
130
|
+
|
131
|
+
include Enumerable
|
132
|
+
|
133
|
+
def initialize(registries)
|
134
|
+
@registries = registries
|
135
|
+
end
|
136
|
+
|
137
|
+
def each
|
138
|
+
METHOD_SUFFIXES.each do |suffix|
|
139
|
+
@registries.each do |registry|
|
140
|
+
registry.each_key do |attr_name|
|
141
|
+
yield TO_METHOD_NAME["#{attr_name}#{suffix}"]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Object.class_eval do
|
2
|
+
undef :id if method_defined?(:id)
|
3
|
+
end
|
4
|
+
|
5
|
+
Array.class_eval do
|
6
|
+
alias_method :index_without_block_form, :index
|
7
|
+
def index(*args, &block)
|
8
|
+
if args.empty? && block
|
9
|
+
each_with_index { |obj, idx| return idx if block.call(obj) }; nil
|
10
|
+
elsif args.size == 1
|
11
|
+
index_without_block_form(*args, &block)
|
12
|
+
else
|
13
|
+
raise ArgumentError, "wrong number of arguments (%d for 1)" % args.size
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end if Array.instance_method(:index).arity == 1
|
17
|
+
|
18
|
+
def URI(object)
|
19
|
+
URI === object ? object : URI.parse(object.to_s)
|
20
|
+
end
|
21
|
+
|
22
|
+
URI.class_eval do
|
23
|
+
def obfuscate
|
24
|
+
returning(dup) do |obfuscated|
|
25
|
+
obfuscated.user &&= '***'
|
26
|
+
obfuscated.password &&= '***'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << CGI
|
32
|
+
alias_method :old_escape, :escape
|
33
|
+
def escape(string)
|
34
|
+
old_escape(string).gsub(/\./, '%' + '.'.unpack('H2')[0])
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module WebService
|
2
|
+
module CRUDOperations
|
3
|
+
include ResponseHandling::Exceptions
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
delegate :each, :to => :all
|
7
|
+
|
8
|
+
def all(*args)
|
9
|
+
return @cache if @cache
|
10
|
+
expect(request(:get, *args), Net::HTTPOK) do |data|
|
11
|
+
instantiate_several_from_http_response_data(data)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache=(collection)
|
16
|
+
@cache = collection.map { |res| instantiate_resource(res) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def flush_cache
|
20
|
+
@cache = nil
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def first(*args)
|
25
|
+
all(*args).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def last(*args)
|
29
|
+
all(*args).last
|
30
|
+
end
|
31
|
+
|
32
|
+
def find(id, *args)
|
33
|
+
expect(request(:get, id, *args), Net::HTTPOK) do |data|
|
34
|
+
instantiate_single_from_http_response_data(data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](id)
|
39
|
+
find(id)
|
40
|
+
rescue ResourceNotFound
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def build(attributes={})
|
45
|
+
default_attributes = respond_to?(:implicit_attributes) ? implicit_attributes : {}
|
46
|
+
instantiate_resource(default_attributes.stringify_keys.merge(attributes.stringify_keys))
|
47
|
+
end
|
48
|
+
|
49
|
+
def create(attributes={})
|
50
|
+
expect(request(:post, body_for_create_or_update(attributes)), Net::HTTPCreated, Net::HTTPAccepted) do |data|
|
51
|
+
instantiate_single_from_http_response_data(data)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update(id, attributes={})
|
56
|
+
expect(request(:put, id, body_for_create_or_update(attributes)), Net::HTTPOK, Net::HTTPAccepted) do |data|
|
57
|
+
instantiate_single_from_http_response_data(data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def element_name
|
64
|
+
@element_name ||= respond_to?(:resource_class) ? resource_class.element_name : super
|
65
|
+
end
|
66
|
+
|
67
|
+
def expect(response, *types)
|
68
|
+
case response
|
69
|
+
when *types
|
70
|
+
yield response.data
|
71
|
+
else
|
72
|
+
raise ConnectionError.new(response, "unexpected response")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def body_for_create_or_update(attributes)
|
77
|
+
{element_name => build(attributes).attributes}
|
78
|
+
end
|
79
|
+
|
80
|
+
def instantiate_single_from_http_response_data(data)
|
81
|
+
return nil if data.to_s.blank?
|
82
|
+
|
83
|
+
unless data.respond_to?(:key?) && data.respond_to?(:[])
|
84
|
+
raise ArgumentError, "wrong data type for marshalled resource: #{data.class} (expected a Hash)"
|
85
|
+
end
|
86
|
+
unless data.key?(element_name)
|
87
|
+
raise ArgumentError, "wrong format for marshalled resource: expected a Hash with #{element_name.inspect} as sole key"
|
88
|
+
end
|
89
|
+
instantiate_resource(data[element_name])
|
90
|
+
end
|
91
|
+
|
92
|
+
def instantiate_several_from_http_response_data(data)
|
93
|
+
[data].flatten.map { |entry| instantiate_single_from_http_response_data(entry) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def instantiate_resource(*args, &block)
|
97
|
+
klass = respond_to?(:new) ? self : resource_class
|
98
|
+
|
99
|
+
# TODO Fix the need that hack:
|
100
|
+
#
|
101
|
+
# It makes has_one saves work, as foo.bar builds a subclass of Bar
|
102
|
+
# with singleton=true so that the RemoteCollection of foo.bar will
|
103
|
+
# make POST requests to /foos/1/bar instead of /foos/1/bars.
|
104
|
+
#
|
105
|
+
# See Resource#remote_collection
|
106
|
+
#
|
107
|
+
basic_collection = self if respond_to?(:resource_class) && resource_class.name.to_s.empty?
|
108
|
+
|
109
|
+
resource = args.size == 1 && !block && klass === args.first ? args.first : klass.new(*args, &block)
|
110
|
+
resource.instance_variable_set(:@basic_remote_collection, basic_collection)
|
111
|
+
return resource
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|