aptible-resource 0.1.1 → 0.2.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.
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