shutl_resource 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +13 -0
- data/.rbenv-version +1 -0
- data/.rspec +4 -0
- data/.rvmrc +48 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +61 -0
- data/Rakefile +10 -0
- data/bin/autospec +16 -0
- data/bin/erubis +16 -0
- data/bin/htmldiff +16 -0
- data/bin/httparty +16 -0
- data/bin/httpclient +16 -0
- data/bin/ldiff +16 -0
- data/bin/rackup +16 -0
- data/bin/rails +16 -0
- data/bin/rake +16 -0
- data/bin/rake2thor +16 -0
- data/bin/rdebug +16 -0
- data/bin/ri +16 -0
- data/bin/rspec +16 -0
- data/bin/sprockets +16 -0
- data/bin/thor +16 -0
- data/bin/tilt +16 -0
- data/bin/tt +16 -0
- data/lib/shutl/resource/configuration.rb +36 -0
- data/lib/shutl/resource/errors.rb +28 -0
- data/lib/shutl/resource/rest.rb +101 -0
- data/lib/shutl/resource/rest_class_methods.rb +225 -0
- data/lib/shutl/resource/version.rb +5 -0
- data/lib/shutl_resource.rb +20 -0
- data/script/ci +2 -0
- data/shutl_resource.gemspec +32 -0
- data/spec/configuration_spec.rb +17 -0
- data/spec/dynamic_resource_spec.rb +65 -0
- data/spec/remote_url_spec.rb +149 -0
- data/spec/rest_resource_spec.rb +329 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/double_logger.rb +14 -0
- data/spec/support/test_resource.rb +6 -0
- metadata +248 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
module Shutl::Resource
|
2
|
+
module RestClassMethods
|
3
|
+
def find(args, params = {})
|
4
|
+
unless args.kind_of?(Hash)
|
5
|
+
id = args
|
6
|
+
args = { resource_id_name => id }
|
7
|
+
end
|
8
|
+
token = params.delete :auth
|
9
|
+
url = member_url args.dup, params
|
10
|
+
response = get url, headers_with_auth(token)
|
11
|
+
|
12
|
+
check_fail response, "Failed to find #{name} with the id #{id}"
|
13
|
+
|
14
|
+
parsed = response.parsed_response
|
15
|
+
|
16
|
+
including_parent_attributes = parsed[@resource_name].merge args
|
17
|
+
|
18
|
+
new_object including_parent_attributes, response
|
19
|
+
end
|
20
|
+
|
21
|
+
def create attributes = {}, options = {}
|
22
|
+
url = generate_collection_url attributes
|
23
|
+
attributes.delete "response"
|
24
|
+
|
25
|
+
response = post(url,
|
26
|
+
{body: {@resource_name => attributes}.to_json}.
|
27
|
+
merge(headers_with_auth options[:auth]))
|
28
|
+
|
29
|
+
check_fail response, "Create failed"
|
30
|
+
|
31
|
+
parsed = response.parsed_response || {}
|
32
|
+
|
33
|
+
attributes = parsed[@resource_name] || {}
|
34
|
+
new_object attributes, response
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy instance, options = {}
|
38
|
+
message = "Failed to destroy #{name.downcase.pluralize}"
|
39
|
+
|
40
|
+
perform_action(
|
41
|
+
instance,
|
42
|
+
:delete,
|
43
|
+
headers_with_auth(options[:auth]),
|
44
|
+
message
|
45
|
+
).success?
|
46
|
+
end
|
47
|
+
|
48
|
+
def save instance, options = {}
|
49
|
+
#TODO: this is sometimes a hash and sometimes a Rest - need to rethink this
|
50
|
+
attributes = instance.attributes rescue instance
|
51
|
+
|
52
|
+
response = perform_action instance, :put,
|
53
|
+
{body: {@resource_name => convert_new_id(attributes)}.to_json}.merge(headers_with_auth options[:auth]),
|
54
|
+
"Save failed"
|
55
|
+
|
56
|
+
response.success?
|
57
|
+
end
|
58
|
+
|
59
|
+
def update args, options = {}
|
60
|
+
save args, options
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def all(args = {})
|
65
|
+
token = args.delete :auth
|
66
|
+
partition = args.partition {|key,value| !remote_collection_url.index(":#{key}").nil? }
|
67
|
+
|
68
|
+
url_args = partition.first.inject({}) { |h,pair| h[pair.first] = pair.last ; h }
|
69
|
+
params = partition.last. inject({}) { |h,pair| h[pair.first] = pair.last ; h }
|
70
|
+
|
71
|
+
url = generate_collection_url url_args, params
|
72
|
+
response = get url, headers_with_auth(token)
|
73
|
+
|
74
|
+
check_fail response, "Failed to find all #{name.downcase.pluralize}"
|
75
|
+
|
76
|
+
response.parsed_response[@resource_name.pluralize].map do |h|
|
77
|
+
new_object(args.merge(h), response)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def resource_name(name)
|
82
|
+
instance_variable_set :@resource_name, name
|
83
|
+
end
|
84
|
+
|
85
|
+
def resource_id(variable_name)
|
86
|
+
instance_variable_set :@resource_id, variable_name
|
87
|
+
end
|
88
|
+
|
89
|
+
def resource_id_name
|
90
|
+
instance_variable_get(:@resource_id).to_sym
|
91
|
+
end
|
92
|
+
|
93
|
+
def remote_collection_url
|
94
|
+
@remote_collection_url ||= "/#{@resource_name.pluralize}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def remote_resource_url
|
98
|
+
@remote_resource_url ||= "#{remote_collection_url}/:#{resource_id_name}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def collection_url(url)
|
102
|
+
@remote_collection_url = url
|
103
|
+
end
|
104
|
+
|
105
|
+
def resource_url(url)
|
106
|
+
@remote_resource_url = url
|
107
|
+
end
|
108
|
+
|
109
|
+
def convert_new_id attributes
|
110
|
+
if attributes[:new_id]
|
111
|
+
attributes = attributes.clone.tap {|h| h[:id] = h[:new_id]; h.delete(:new_id)}
|
112
|
+
end
|
113
|
+
|
114
|
+
attributes
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_resource_id_to args={}
|
118
|
+
args = args.dup.with_indifferent_access
|
119
|
+
unless args.has_key? "id"
|
120
|
+
args.merge!({"id" => args[resource_id_name]})
|
121
|
+
end
|
122
|
+
args
|
123
|
+
end
|
124
|
+
|
125
|
+
def member_url *args
|
126
|
+
attributes = args.first.with_indifferent_access
|
127
|
+
unless attributes[resource_id_name] ||= attributes[:id]
|
128
|
+
raise ArgumentError, "Missing resource id with name: `#{resource_id_name}' for #{self}"
|
129
|
+
end
|
130
|
+
|
131
|
+
args[0] = attributes
|
132
|
+
|
133
|
+
generate_url! remote_resource_url, *(args.dup)
|
134
|
+
end
|
135
|
+
|
136
|
+
def generate_collection_url *args
|
137
|
+
generate_url! remote_collection_url, *args
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def headers_with_auth token
|
144
|
+
{ headers: headers.merge('Authorization' => "Bearer #{token}") }
|
145
|
+
end
|
146
|
+
|
147
|
+
def perform_action instance, verb, args, failure_message
|
148
|
+
attributes = instance.is_a?(Hash) ? instance : instance.attributes
|
149
|
+
attributes.delete "response" #used in debugging requests/responses
|
150
|
+
|
151
|
+
url = member_url attributes
|
152
|
+
response = send verb, url, args
|
153
|
+
|
154
|
+
check_fail response, failure_message
|
155
|
+
|
156
|
+
response
|
157
|
+
end
|
158
|
+
|
159
|
+
def new_object(args={}, response=nil)
|
160
|
+
instance = new add_resource_id_to(args), response
|
161
|
+
|
162
|
+
instance.tap do |i|
|
163
|
+
parsed_response = response.parsed_response
|
164
|
+
|
165
|
+
if errors = (parsed_response and parsed_response["errors"])
|
166
|
+
i.errors = errors
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def check_fail response, message
|
172
|
+
c = response.code
|
173
|
+
failure_klass = case c
|
174
|
+
when 299 then Shutl::NoQuotesGenerated
|
175
|
+
when 400 then Shutl::BadRequest
|
176
|
+
when 401 then Shutl::UnauthorizedAccess
|
177
|
+
when 403 then Shutl::ForbiddenAccess
|
178
|
+
when 404 then Shutl::ResourceNotFound
|
179
|
+
when 409 then Shutl::ResourceConflict
|
180
|
+
when 410 then Shutl::ResourceGone
|
181
|
+
when 422
|
182
|
+
if Shutl::Resource.raise_exceptions_on_validation
|
183
|
+
Shutl::ResourceInvalid
|
184
|
+
else
|
185
|
+
nil #handled as validation failure
|
186
|
+
end
|
187
|
+
|
188
|
+
when 411..499
|
189
|
+
Shutl::BadRequest
|
190
|
+
when 500 then Shutl::ServerError
|
191
|
+
when 503 then Shutl::ServiceUnavailable
|
192
|
+
when 501..Float::INFINITY
|
193
|
+
Shutl::ServerError
|
194
|
+
end
|
195
|
+
|
196
|
+
raise failure_klass.new message, response if failure_klass
|
197
|
+
end
|
198
|
+
|
199
|
+
protected
|
200
|
+
|
201
|
+
def generate_url!(url_pattern, args, params = {})
|
202
|
+
url = url_pattern.dup
|
203
|
+
|
204
|
+
args, url = replace_args_from_pattern! args, url
|
205
|
+
|
206
|
+
url = URI.escape url
|
207
|
+
unless params.empty?
|
208
|
+
url += '?'
|
209
|
+
params.each { |key, value| url += "#{key}=#{value}&" }
|
210
|
+
end
|
211
|
+
url
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
def replace_args_from_pattern! args, url
|
216
|
+
args = args.reject! do |key,value|
|
217
|
+
if s = url[":#{key}"]
|
218
|
+
url.gsub!(s, value.to_s)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
return args, url
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Shutl
|
2
|
+
end
|
3
|
+
|
4
|
+
require 'shutl_auth'
|
5
|
+
require 'shutl/resource/configuration'
|
6
|
+
require 'shutl/resource/rest'
|
7
|
+
require 'shutl/resource/rest_class_methods'
|
8
|
+
require 'shutl/resource/errors'
|
9
|
+
|
10
|
+
module Shutl::Resource
|
11
|
+
extend self
|
12
|
+
|
13
|
+
delegate :logger, :logger=, to: Configuration
|
14
|
+
|
15
|
+
def configure(*args, &block)
|
16
|
+
Configuration.configure(*args, &block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
data/script/ci
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/shutl/resource/version', __FILE__)
|
3
|
+
|
4
|
+
$platform ||= RUBY_PLATFORM[/java/] || 'ruby'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.authors = ["David Rouchy", "Volker Pacher", "Mark Burns"]
|
8
|
+
gem.email = ["davidr@shutl.co.uk", "volker@shutl.com", "mark@shutl.com"]
|
9
|
+
gem.description = %q{Shutl Rest resource}
|
10
|
+
gem.summary = %q{Manage Shutl Rest resource. Parse/Serialize JSON}
|
11
|
+
gem.homepage = ""
|
12
|
+
gem.platform = $platform
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.name = "shutl_resource"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = Shutl::Resource::VERSION
|
20
|
+
|
21
|
+
gem.add_dependency 'httparty', '~> 0.10.0'
|
22
|
+
gem.add_dependency 'shutl_auth', '0.8.0'
|
23
|
+
gem.add_dependency 'activemodel'
|
24
|
+
|
25
|
+
gem.add_development_dependency 'rake'
|
26
|
+
gem.add_development_dependency 'rspec', '~> 2.11.0'
|
27
|
+
gem.add_development_dependency 'debugger' if $platform.to_s == 'ruby'
|
28
|
+
gem.add_development_dependency 'ruby-debug' if $platform.to_s == 'java'
|
29
|
+
gem.add_development_dependency 'vcr'
|
30
|
+
|
31
|
+
gem.add_development_dependency 'webmock', '~> 1.8.7'
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Shutl::Resource do
|
4
|
+
describe '#configure' do
|
5
|
+
let(:logger) { Shutl::Resource.logger }
|
6
|
+
|
7
|
+
it 'should configure the logger' do
|
8
|
+
logger = stub('logger')
|
9
|
+
|
10
|
+
Shutl::Resource.configure do |config|
|
11
|
+
config.logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
Shutl::Resource.logger.should == logger
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Shutl::Resource::Rest do
|
4
|
+
|
5
|
+
class TestResource
|
6
|
+
include Shutl::Resource::Rest
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestOverride
|
10
|
+
include Shutl::Resource::Rest
|
11
|
+
resource_name 'a_other_prefix'
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:resource) { TestResource.new({ a: 'a', b: 'b' }) }
|
15
|
+
|
16
|
+
describe '#initalize' do
|
17
|
+
it 'should assign the instance variable value for the entries in the hash' do
|
18
|
+
resource.instance_variable_get(:@a).should == 'a'
|
19
|
+
resource.instance_variable_get(:@b).should == 'b'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should create a attribute reader' do
|
23
|
+
resource.a().should == 'a'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should keep the method not found behaviour' do
|
27
|
+
lambda { resource.notfound() }.should raise_error(NameError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#to_json' do
|
32
|
+
|
33
|
+
it 'should "prefix" the json with the name of the class' do
|
34
|
+
json = resource.to_json
|
35
|
+
|
36
|
+
JSON.parse(json, symbolize_names: true)[:test_resource].should_not be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should serialize in json based on the instance variables' do
|
40
|
+
json = resource.to_json
|
41
|
+
|
42
|
+
JSON.parse(json, symbolize_names: true).should == { test_resource: { a: 'a', b: 'b' } }
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should be able to overribe the prefix' do
|
46
|
+
json = TestOverride.new({ a: 'a'}).to_json
|
47
|
+
|
48
|
+
JSON.parse(json, symbolize_names: true)[:a_other_prefix].should_not be_empty
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'udpate_attributes' do
|
54
|
+
|
55
|
+
it 'should replace the attributes' do
|
56
|
+
resource.update_attributes(a: 'c')
|
57
|
+
|
58
|
+
resource.instance_variable_get(:@a).should == 'c'
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should use a white list permission'
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Shutl::Resource::Rest do
|
4
|
+
let(:headers) do
|
5
|
+
{'Accept'=>'application/json', 'Authorization'=>'Bearer', 'Content-Type'=>'application/json'}
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
class OverrideUrlResource
|
10
|
+
include Shutl::Resource::Rest
|
11
|
+
collection_url '/api/resources'
|
12
|
+
resource_url '/api/resources/:name'
|
13
|
+
end
|
14
|
+
|
15
|
+
module Namespace
|
16
|
+
class Resource
|
17
|
+
include Shutl::Resource::Rest
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#remote_collection_url' do
|
22
|
+
it 'should be based on the resource name by default' do
|
23
|
+
TestRest.remote_collection_url.should == '/test_rests'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should use the override if defined' do
|
27
|
+
OverrideUrlResource.remote_collection_url.should == '/api/resources'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'only uses the specific object to infer the resource name' do
|
31
|
+
Namespace::Resource.remote_collection_url.should == '/resources'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#remote_resource_url' do
|
36
|
+
it 'should be base on the resource name and the resource id name by default' do
|
37
|
+
TestRest.remote_resource_url.should == '/test_rests/:a'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should use the override if defined' do
|
41
|
+
OverrideUrlResource.remote_resource_url.should == '/api/resources/:name'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'nested resource' do
|
46
|
+
class NestedResource
|
47
|
+
include Shutl::Resource::Rest
|
48
|
+
base_uri 'http://host'
|
49
|
+
collection_url '/nested/:parent_id/resources'
|
50
|
+
resource_url '/nested/:parent_id/resources/:id'
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:resource) { NestedResource.new(id: 2, parent_id: 10) }
|
54
|
+
|
55
|
+
describe '#all' do
|
56
|
+
it 'should query the correct endpoint' do
|
57
|
+
request = stub_request(:get, 'http://host/nested/10/resources').
|
58
|
+
to_return(body: '{"nested_resources": []}', headers: headers)
|
59
|
+
|
60
|
+
NestedResource.all(parent_id: 10)
|
61
|
+
|
62
|
+
request.should have_been_requested
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should add the nested params to the attributes' do
|
66
|
+
stub_request(:get, 'http://host/nested/10/resources').
|
67
|
+
to_return(body: '{"nested_resources": [{}, {}]}', headers: headers)
|
68
|
+
|
69
|
+
resources = NestedResource.all(parent_id: 10)
|
70
|
+
|
71
|
+
resources.each { |r| r.parent_id.should == 10 }
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should support the params' do
|
75
|
+
request = stub_request(:get, 'http://host/nested/10/resources?arg1=val1&arg2=val2').
|
76
|
+
to_return(body: '{"nested_resources": []}', headers: headers)
|
77
|
+
|
78
|
+
NestedResource.all(parent_id: 10, arg1: 'val1', arg2: 'val2')
|
79
|
+
|
80
|
+
request.should have_been_requested
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#find' do
|
85
|
+
it 'should query the correct endpoint' do
|
86
|
+
request = stub_request(:get, 'http://host/nested/10/resources/2').
|
87
|
+
to_return(body: '{"nested_resource": {}}', headers: headers)
|
88
|
+
|
89
|
+
NestedResource.find(id: 2, parent_id: 10)
|
90
|
+
|
91
|
+
request.should have_been_requested
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should add the nested params to the attributes' do
|
95
|
+
stub_request(:get, 'http://host/nested/10/resources/2').
|
96
|
+
to_return(body: '{"nested_resource": {}}', headers: headers)
|
97
|
+
|
98
|
+
resource = NestedResource.find(id: 2, parent_id: 10)
|
99
|
+
|
100
|
+
resource.parent_id.should == 10
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'update' do
|
105
|
+
it 'should query the correct endpoint' do
|
106
|
+
request = stub_request(:put, 'http://host/nested/10/resources/2').
|
107
|
+
to_return(body: '{"nested_resource": {}}', headers: headers)
|
108
|
+
|
109
|
+
resource.save
|
110
|
+
|
111
|
+
request.should have_been_requested
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#create' do
|
116
|
+
it 'should query the correct endpoint' do
|
117
|
+
request = stub_request(:post, 'http://host/nested/10/resources').
|
118
|
+
to_return(body: '{"nested_resource": {}}', headers: headers)
|
119
|
+
|
120
|
+
NestedResource.create(parent_id: 10)
|
121
|
+
|
122
|
+
request.should have_been_requested
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '#delete' do
|
127
|
+
it 'should query the correct endpoint' do
|
128
|
+
request = stub_request(:delete, 'http://host/nested/10/resources/2').
|
129
|
+
to_return(body: '{"nested_resource": {}}', headers: headers)
|
130
|
+
|
131
|
+
NestedResource.destroy(parent_id: 10, id: 2)
|
132
|
+
|
133
|
+
request.should have_been_requested
|
134
|
+
end
|
135
|
+
|
136
|
+
specify do
|
137
|
+
request = stub_request(:delete, 'http://host/nested/10/resources/2').
|
138
|
+
to_return(body: '{"nested_resource": {}}',
|
139
|
+
headers: {"Content-Type" => "application/json"})
|
140
|
+
|
141
|
+
resource.destroy auth: 'TOKEN'
|
142
|
+
|
143
|
+
request.should have_been_requested
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|