shutl_resource 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Shutl
2
+ module Resource
3
+ VERSION = '0.8.0'
4
+ end
5
+ 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,2 @@
1
+ bundle install --path vendor/bundle
2
+ bundle exec rspec spec
@@ -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