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.
@@ -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