aptible-resource 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e0a4813794c67cf121e6e99aacdeb6d4658de77
4
- data.tar.gz: 8a30600a2fe9e4b9aa5bcf1ef616af537d30f833
3
+ metadata.gz: f8992244b07c1cd292461871658dd5ba0b400f9a
4
+ data.tar.gz: 2b16f2fb43d9f60cd0993d9ee98f9d2528a7f249
5
5
  SHA512:
6
- metadata.gz: c72824d53f2a8dc4933a824c733ac82858364d349ee223f1d5c46e491b759a2550c50cce746a5e49e8e5f512ae1eda1c8733ebfb82c7a0d5f655f7511b0624ec
7
- data.tar.gz: b1570c06b61d5222484c0c2198ffb5f14be4bd599e95a3f534094de0fdce252e4a7bef172fc5f6e3b0f8fde9427e2ce2deab4146bd7443372c5dc6a27ec7f181
6
+ metadata.gz: 28bd9df8faea54c8e263e8e85da8f3543b397c438937ddcd9383db4b346bf445cc0ddbbbf6c9919c769a6c6d04b8da4e8f65b24dc3fad323576dbca25238bdbc
7
+ data.tar.gz: 81df2fcbaf1c1e2b98e5b8f99ac41e59fdec11e503c90ba9e6fd8f0fa2349505a2d5554c0adb3114f1b6a12a0cbdc9233fd6e6dded91d15b1181f9c8be1362e1
@@ -1,18 +1,18 @@
1
1
  require 'fridge'
2
+ require 'active_support/inflector'
2
3
 
3
4
  # Require vendored HyperResource
4
5
  $LOAD_PATH.unshift File.expand_path('../..', __FILE__)
5
6
  require 'hyper_resource'
6
7
 
7
8
  require 'aptible/resource/adapter'
8
- require 'aptible/resource/model'
9
+ require 'aptible/resource/errors'
9
10
 
10
11
  module Aptible
11
12
  module Resource
13
+ # rubocop:disable ClassLength
12
14
  class Base < HyperResource
13
- include Model
14
-
15
- attr_accessor :token
15
+ attr_accessor :token, :errors
16
16
 
17
17
  def self.get_data_type_from_response(response)
18
18
  return nil unless response && response.body
@@ -23,6 +23,125 @@ module Aptible
23
23
  Aptible::Resource::Adapter
24
24
  end
25
25
 
26
+ def self.collection_href
27
+ "/#{basename}"
28
+ end
29
+
30
+ def self.basename
31
+ name.split('::').last.downcase.pluralize
32
+ end
33
+
34
+ def self.all(options = {})
35
+ resource = find_by_url(collection_href, options)
36
+ return [] unless resource
37
+ resource.send(basename).entries
38
+ end
39
+
40
+ def self.find(id, options = {})
41
+ find_by_url("#{collection_href}/#{id}", options)
42
+ end
43
+
44
+ def self.find_by_url(url, options = {})
45
+ # REVIEW: Should exception be raised if return type mismatch?
46
+ new(options).find_by_url(url)
47
+ rescue HyperResource::ClientError => e
48
+ if e.response.status == 404
49
+ return nil
50
+ else
51
+ raise e
52
+ end
53
+ end
54
+
55
+ def self.create!(params = {})
56
+ token = params.delete(:token)
57
+ resource = new(token: token)
58
+ resource.send(basename).create(normalize_params(params))
59
+ end
60
+
61
+ def self.create(params = {})
62
+ create!(params)
63
+ rescue HyperResource::ResponseError => e
64
+ new.tap { |resource| resource.errors = Errors.from_exception(e) }
65
+ end
66
+
67
+ # rubocop:disable PredicateName
68
+ def self.has_many(relation)
69
+ define_has_many_getter(relation)
70
+ define_has_many_setter(relation)
71
+ end
72
+ # rubocop:enable PredicateName
73
+
74
+ def self.field(name, options = {})
75
+ define_method name do
76
+ self.class.cast_field(attributes[name], options[:type])
77
+ end
78
+ end
79
+
80
+ def self.belongs_to(relation)
81
+ define_method relation do
82
+ get unless loaded
83
+ if (memoized = instance_variable_get("@#{relation}"))
84
+ memoized
85
+ elsif links[relation]
86
+ instance_variable_set("@#{relation}", links[relation].get)
87
+ end
88
+ end
89
+ end
90
+
91
+ # rubocop:disable PredicateName
92
+ def self.has_one(relation)
93
+ # Better than class << self + alias_method?
94
+ belongs_to(relation)
95
+ end
96
+ # rubocop:enable PredicateName
97
+
98
+ def self.define_has_many_getter(relation)
99
+ define_method relation do
100
+ get unless loaded
101
+ if (memoized = instance_variable_get("@#{relation}"))
102
+ memoized
103
+ elsif links[relation]
104
+ instance_variable_set("@#{relation}", links[relation].entries)
105
+ end
106
+ end
107
+ end
108
+
109
+ # rubocop:disable MethodLength
110
+ def self.define_has_many_setter(relation)
111
+ define_method "create_#{relation.to_s.singularize}!" do |params = {}|
112
+ get unless loaded
113
+ links[relation].create(self.class.normalize_params(params))
114
+ end
115
+
116
+ define_method "create_#{relation.to_s.singularize}" do |params = {}|
117
+ begin
118
+ send "create_#{relation.to_s.singularize}!", params
119
+ rescue HyperResource::ResponseError => e
120
+ Base.new(root: root_url, namespace: namespace).tap do |base|
121
+ base.errors = Errors.from_exception(e)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ # rubocop:enable MethodLength
127
+
128
+ def self.normalize_params(params = {})
129
+ params_array = params.map do |key, value|
130
+ value.is_a?(HyperResource) ? [key, value.href] : [key, value]
131
+ end
132
+ Hash[params_array]
133
+ end
134
+
135
+ def self.cast_field(value, type)
136
+ if type == Time
137
+ Time.parse(value) if value
138
+ elsif type == DateTime
139
+ DateTime.parse(value) if value
140
+ else
141
+ value
142
+ end
143
+ end
144
+
26
145
  def initialize(options = {})
27
146
  if options.is_a?(Hash)
28
147
  self.token = options[:token]
@@ -63,8 +182,38 @@ module Aptible
63
182
  when String then token
64
183
  end
65
184
  end
185
+
186
+ alias_method :_hyperresource_update, :update
187
+ def update!(params)
188
+ _hyperresource_update(self.class.normalize_params(params))
189
+ rescue HyperResource::ResponseError => e
190
+ self.errors = Errors.from_exception(e)
191
+ raise e
192
+ end
193
+
194
+ def update(params)
195
+ update!(params)
196
+ rescue HyperResource::ResponseError
197
+ false
198
+ end
199
+
200
+ def delete
201
+ super
202
+ rescue HyperResource::ResponseError
203
+ # HyperResource/Faraday choke on empty response bodies
204
+ nil
205
+ end
206
+ alias_method :destroy, :delete
207
+
208
+ # NOTE: The following does not update the object in-place
209
+ def reload
210
+ self.class.find_by_url(href, headers: headers)
211
+ end
212
+
213
+ def errors
214
+ @errors ||= Aptible::Resource::Errors.new
215
+ end
66
216
  end
217
+ # rubocop:enable ClassLength
67
218
  end
68
219
  end
69
-
70
- require 'aptible/resource/token'
@@ -0,0 +1,24 @@
1
+ module Aptible
2
+ module Resource
3
+ class Errors
4
+ attr_accessor :status_code, :messages, :full_messages
5
+
6
+ def self.from_exception(exception)
7
+ new.tap do |errors|
8
+ response_json = JSON.parse(exception.response.body)
9
+ errors.messages = { base: response_json['message'] }
10
+ errors.full_messages = [response_json['message']]
11
+ errors.status_code = exception.response.status
12
+ end
13
+ end
14
+
15
+ def messages
16
+ @messages ||= {}
17
+ end
18
+
19
+ def full_messages
20
+ @full_messages ||= []
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module Resource
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -1,8 +1,107 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Aptible::Resource::Base do
4
+ let(:hyperresource_exception) { HyperResource::ResponseError.new('403') }
5
+ let(:error_response) { double 'Faraday::Response' }
6
+ before { hyperresource_exception.stub(:response) { error_response } }
7
+ before do
8
+ error_response.stub(:body) { { message: 'Forbidden' }.to_json }
9
+ error_response.stub(:status) { 403 }
10
+ end
11
+
4
12
  subject { Api.new }
5
13
 
14
+ describe '.collection_href' do
15
+ it 'should use the pluralized resource name' do
16
+ url = Api::Mainframe.collection_href
17
+ expect(url).to eq '/mainframes'
18
+ end
19
+ end
20
+
21
+ describe '.find' do
22
+ it 'should call find_by_url' do
23
+ url = '/mainframes/42'
24
+ expect(Api::Mainframe).to receive(:find_by_url).with url, {}
25
+ Api::Mainframe.find(42)
26
+ end
27
+ end
28
+
29
+ describe '.all' do
30
+ let(:mainframe) { double 'Mainframe' }
31
+ let(:collection) { double 'Api' }
32
+
33
+ before do
34
+ collection.stub(:mainframes) { [mainframe] }
35
+ Api::Mainframe.any_instance.stub(:find_by_url) { collection }
36
+ end
37
+
38
+ it 'should be an array' do
39
+ expect(Api::Mainframe.all).to be_a Array
40
+ end
41
+
42
+ it 'should return the root collection' do
43
+ expect(Api::Mainframe.all).to eq [mainframe]
44
+ end
45
+
46
+ it 'should pass options to the HyperResource initializer' do
47
+ klass = Api::Mainframe
48
+ options = { token: 'token' }
49
+ expect(klass).to receive(:new).with(options).and_return klass.new
50
+ Api::Mainframe.all(options)
51
+ end
52
+ end
53
+
54
+ describe '.create' do
55
+ let(:mainframe) { Api::Mainframe.new }
56
+ let(:mainframes_link) { HyperResource::Link.new(href: '/mainframes') }
57
+
58
+ before { Api.any_instance.stub(:mainframes) { mainframes_link } }
59
+ before { mainframes_link.stub(:create) { mainframe } }
60
+
61
+ it 'should create a new top-level resource' do
62
+ mainframes_link.stub(:create) { mainframe }
63
+ expect(mainframes_link).to receive(:create).with(foo: 'bar')
64
+ Api::Mainframe.create(foo: 'bar')
65
+ end
66
+
67
+ it 'should populate #errors in the event of an error' do
68
+ mainframes_link.stub(:create) { fail hyperresource_exception }
69
+ mainframe = Api::Mainframe.create
70
+ expect(mainframe.errors.messages).to eq(base: 'Forbidden')
71
+ expect(mainframe.errors.full_messages).to eq(['Forbidden'])
72
+ end
73
+
74
+ it 'should return a Base-classed resource on error' do
75
+ mainframes_link.stub(:create) { fail hyperresource_exception }
76
+ expect(Api::Mainframe.create).to be_a Api::Mainframe
77
+ end
78
+
79
+ it 'should return the object in the event of successful creation' do
80
+ mainframes_link.stub(:create) { mainframe }
81
+ expect(Api::Mainframe.create).to eq mainframe
82
+ end
83
+ end
84
+
85
+ describe '.create!' do
86
+ let(:mainframe) { Api::Mainframe.new }
87
+ let(:mainframes_link) { HyperResource::Link.new(href: '/mainframes') }
88
+
89
+ before { Api.any_instance.stub(:mainframes) { mainframes_link } }
90
+ before { mainframes_link.stub(:create) { mainframe } }
91
+
92
+ it 'should pass through any exceptions' do
93
+ mainframes_link.stub(:create) { fail hyperresource_exception }
94
+ expect do
95
+ Api::Mainframe.create!
96
+ end.to raise_error HyperResource::ResponseError
97
+ end
98
+
99
+ it 'should return the object in the event of successful creation' do
100
+ mainframes_link.stub(:create) { mainframe }
101
+ expect(Api::Mainframe.create!).to eq mainframe
102
+ end
103
+ end
104
+
6
105
  describe '#initialize' do
7
106
  it 'should be a HyperResource instance' do
8
107
  expect(subject).to be_a HyperResource
@@ -33,4 +132,124 @@ describe Aptible::Resource::Base do
33
132
  expect(subject.bearer_token).to eq 'token'
34
133
  end
35
134
  end
135
+
136
+ describe '#errors' do
137
+ it 'should default to an empty error' do
138
+ expect(subject.errors).to be_a Aptible::Resource::Errors
139
+ expect(subject.errors.messages).to eq({})
140
+ expect(subject.errors.full_messages).to eq([])
141
+ end
142
+ end
143
+
144
+ describe '#update' do
145
+ it 'should populate #errors in the event of an error' do
146
+ HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
147
+ subject.update({})
148
+ expect(subject.errors.messages).to eq(base: 'Forbidden')
149
+ expect(subject.errors.full_messages).to eq(['Forbidden'])
150
+ end
151
+
152
+ it 'should return false in the event of an error' do
153
+ HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
154
+ expect(subject.update({})).to eq false
155
+ end
156
+
157
+ it 'should return the object in the event of a successful update' do
158
+ HyperResource.any_instance.stub(:put) { subject }
159
+ expect(subject.update({})).to eq subject
160
+ end
161
+ end
162
+
163
+ describe '#update!' do
164
+ it 'should populate #errors in the event of an error' do
165
+ HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
166
+ begin
167
+ subject.update!({})
168
+ rescue
169
+ # Allow errors to be populated and tested
170
+ nil
171
+ end
172
+ expect(subject.errors.messages).to eq(base: 'Forbidden')
173
+ expect(subject.errors.full_messages).to eq(['Forbidden'])
174
+ end
175
+
176
+ it 'should pass through any exceptions' do
177
+ HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
178
+ expect do
179
+ subject.update!({})
180
+ end.to raise_error HyperResource::ResponseError
181
+ end
182
+
183
+ it 'should return the object in the event of a successful update' do
184
+ HyperResource.any_instance.stub(:put) { subject }
185
+ expect(subject.update!({})).to eq subject
186
+ end
187
+ end
188
+
189
+ context '.has_many' do
190
+ let(:mainframe) { Api::Mainframe.new }
191
+ let(:mainframes_link) { HyperResource::Link.new(href: '/mainframes') }
192
+
193
+ before { Api.has_many :mainframes }
194
+ before { subject.stub(:loaded) { true } }
195
+ before { subject.stub(:links) { { mainframes: mainframes_link } } }
196
+
197
+ describe '#create_#{relation}' do
198
+ it 'should populate #errors in the event of an error' do
199
+ mainframes_link.stub(:create) { fail hyperresource_exception }
200
+ mainframe = subject.create_mainframe({})
201
+ expect(mainframe.errors.messages).to eq(base: 'Forbidden')
202
+ expect(mainframe.errors.full_messages).to eq(['Forbidden'])
203
+ end
204
+
205
+ it 'should return a Base-classed resource on error' do
206
+ mainframes_link.stub(:create) { fail hyperresource_exception }
207
+ expect(subject.create_mainframe.class).to eq Aptible::Resource::Base
208
+ end
209
+
210
+ it 'should return the object in the event of successful creation' do
211
+ mainframes_link.stub(:create) { mainframe }
212
+ expect(subject.create_mainframe({})).to eq mainframe
213
+ end
214
+ end
215
+
216
+ describe '#create_#{relation}!' do
217
+ it 'should pass through any exceptions' do
218
+ mainframes_link.stub(:create) { fail hyperresource_exception }
219
+ expect do
220
+ subject.create_mainframe!({})
221
+ end.to raise_error HyperResource::ResponseError
222
+ end
223
+
224
+ it 'should return the object in the event of successful creation' do
225
+ mainframes_link.stub(:create) { mainframe }
226
+ expect(subject.create_mainframe!({})).to eq mainframe
227
+ end
228
+ end
229
+ end
230
+
231
+ context '.field' do
232
+ it 'should define a method for the field' do
233
+ Api.field :foo, type: String
234
+ expect(subject.respond_to?(:foo)).to be_true
235
+ end
236
+
237
+ it 'should return the raw attribute' do
238
+ Api.field :foo, type: String
239
+ subject.stub(:attributes) { { foo: 'bar' } }
240
+ expect(subject.foo).to eq 'bar'
241
+ end
242
+
243
+ it 'should parse the attribute if DateTime' do
244
+ Api.field :created_at, type: DateTime
245
+ subject.stub(:attributes) { { created_at: Time.now.to_json } }
246
+ expect(subject.created_at).to be_a DateTime
247
+ end
248
+
249
+ it 'should parse the attribute if Time' do
250
+ Api.field :created_at, type: Time
251
+ subject.stub(:attributes) { { created_at: Time.now.to_json } }
252
+ expect(subject.created_at).to be_a Time
253
+ end
254
+ end
36
255
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-18 00:00:00.000000000 Z
11
+ date: 2014-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uri_template
@@ -169,8 +169,7 @@ files:
169
169
  - lib/aptible/resource.rb
170
170
  - lib/aptible/resource/adapter.rb
171
171
  - lib/aptible/resource/base.rb
172
- - lib/aptible/resource/model.rb
173
- - lib/aptible/resource/token.rb
172
+ - lib/aptible/resource/errors.rb
174
173
  - lib/aptible/resource/version.rb
175
174
  - lib/hyper_resource.rb
176
175
  - lib/hyper_resource/adapter.rb
@@ -185,7 +184,6 @@ files:
185
184
  - lib/hyper_resource/response.rb
186
185
  - lib/hyper_resource/version.rb
187
186
  - spec/aptible/resource/base_spec.rb
188
- - spec/aptible/resource/model_spec.rb
189
187
  - spec/fixtures/api.rb
190
188
  - spec/fixtures/mainframe.rb
191
189
  - spec/fixtures/token.rb
@@ -217,7 +215,6 @@ specification_version: 4
217
215
  summary: Foundation classes for Aptible resource server gems
218
216
  test_files:
219
217
  - spec/aptible/resource/base_spec.rb
220
- - spec/aptible/resource/model_spec.rb
221
218
  - spec/fixtures/api.rb
222
219
  - spec/fixtures/mainframe.rb
223
220
  - spec/fixtures/token.rb
@@ -1,115 +0,0 @@
1
- require 'active_support/inflector'
2
-
3
- module Aptible
4
- module Resource
5
- module Model
6
- def self.included(base)
7
- base.extend ClassMethods
8
- end
9
-
10
- module ClassMethods
11
- def collection_href
12
- "/#{basename}"
13
- end
14
-
15
- def basename
16
- name.split('::').last.downcase.pluralize
17
- end
18
-
19
- def all(options = {})
20
- resource = find_by_url(collection_href, options)
21
- return [] unless resource
22
- resource.send(basename).entries
23
- end
24
-
25
- def find(id, options = {})
26
- find_by_url("#{collection_href}/#{id}", options)
27
- end
28
-
29
- def find_by_url(url, options = {})
30
- # REVIEW: Should exception be raised if return type mismatch?
31
- new(options).find_by_url(url)
32
- rescue HyperResource::ClientError => e
33
- if e.response.status == 404
34
- return nil
35
- else
36
- raise e
37
- end
38
- end
39
-
40
- def create(params)
41
- token = params.delete(:token)
42
- resource = new(token: token)
43
- resource.send(basename).create(normalize_params(params))
44
- end
45
-
46
- # rubocop:disable PredicateName
47
- def has_many(relation)
48
- define_has_many_getter(relation)
49
- define_has_many_setter(relation)
50
- end
51
- # rubocop:enable PredicateName
52
-
53
- def belongs_to(relation)
54
- define_method relation do
55
- get unless loaded
56
- if (memoized = instance_variable_get("@#{relation}"))
57
- memoized
58
- elsif links[relation]
59
- instance_variable_set("@#{relation}", links[relation].get)
60
- end
61
- end
62
- end
63
-
64
- # rubocop:disable PredicateName
65
- def has_one(relation)
66
- # Better than class << self + alias_method?
67
- belongs_to(relation)
68
- end
69
- # rubocop:enable PredicateName
70
-
71
- def define_has_many_getter(relation)
72
- define_method relation do
73
- get unless loaded
74
- if (memoized = instance_variable_get("@#{relation}"))
75
- memoized
76
- elsif links[relation]
77
- instance_variable_set("@#{relation}", links[relation].entries)
78
- end
79
- end
80
- end
81
-
82
- def define_has_many_setter(relation)
83
- define_method "create_#{relation.to_s.singularize}" do |params = {}|
84
- get unless loaded
85
- links[relation].create(self.class.normalize_params(params))
86
- end
87
- end
88
-
89
- def normalize_params(params = {})
90
- params_array = params.map do |key, value|
91
- value.is_a?(HyperResource) ? [key, value.href] : [key, value]
92
- end
93
- Hash[params_array]
94
- end
95
- end
96
-
97
- def delete
98
- # HyperResource/Faraday choke on empty response bodies
99
- super
100
- rescue HyperResource::ResponseError
101
- nil
102
- end
103
- alias_method :destroy, :delete
104
-
105
- def update(params)
106
- super(self.class.normalize_params(params))
107
- end
108
-
109
- # NOTE: The following does not update the object in-place
110
- def reload
111
- self.class.find_by_url(href, headers: headers)
112
- end
113
- end
114
- end
115
- end
@@ -1,10 +0,0 @@
1
- require 'aptible/resource/base'
2
-
3
- # Skeleton class for token implementations to inherit from
4
- module Aptible
5
- module Resource
6
- class Token < Base
7
- attr_accessor :access_token
8
- end
9
- end
10
- end
@@ -1,54 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Aptible::Resource::Model do
4
- subject { Api.new }
5
-
6
- describe '.collection_href' do
7
- it 'should use the pluralized resource name' do
8
- url = Api::Mainframe.collection_href
9
- expect(url).to eq '/mainframes'
10
- end
11
- end
12
-
13
- describe '.find' do
14
- it 'should call find_by_url' do
15
- url = '/mainframes/42'
16
- expect(Api::Mainframe).to receive(:find_by_url).with url, {}
17
- Api::Mainframe.find(42)
18
- end
19
- end
20
-
21
- describe '.all' do
22
- let(:mainframe) { double 'Mainframe' }
23
- let(:collection) { double 'Api' }
24
-
25
- before do
26
- collection.stub(:mainframes) { [mainframe] }
27
- Api::Mainframe.any_instance.stub(:find_by_url) { collection }
28
- end
29
-
30
- it 'should be an array' do
31
- expect(Api::Mainframe.all).to be_a Array
32
- end
33
-
34
- it 'should return the root collection' do
35
- expect(Api::Mainframe.all).to eq [mainframe]
36
- end
37
-
38
- it 'should pass options to the HyperResource initializer' do
39
- klass = Api::Mainframe
40
- options = { token: 'token' }
41
- expect(klass).to receive(:new).with(options).and_return klass.new
42
- Api::Mainframe.all(options)
43
- end
44
- end
45
-
46
- describe '.create' do
47
- it 'should create a new top-level resource' do
48
- mainframes = double 'Api'
49
- Api.stub_chain(:new, :mainframes) { mainframes }
50
- expect(mainframes).to receive(:create).with(foo: 'bar')
51
- Api::Mainframe.create(foo: 'bar')
52
- end
53
- end
54
- end