shutl_resource 0.8.0
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/.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
|