protip 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f3d97797f1ded3cce21e24d653a5960c292f3af
4
+ data.tar.gz: d1fa2cea1e89f3fdb5ebf9be570e5dde1144397a
5
+ SHA512:
6
+ metadata.gz: 5775bd4b337ab6d1f31ea6de1c0fc3d2b2f07a8075dda4bdcdb0581b69dd61b5154d1dd5bffd7dbfc069e34d8bd533740411d3cb97e107657ee433d8729c35c4
7
+ data.tar.gz: 496f91676d206299a1ebbc4328722be7976b685d1d02988ac44a76472519e19e740896a6abb9f93d8d46a600a46aee2d9cae4ecf4dc34fb7ae45b8d8d945f688
@@ -0,0 +1,7 @@
1
+ require 'protip/client'
2
+ require 'protip/resource'
3
+
4
+ # Register the mime type with Rails, if Rails exists.
5
+ if defined?(Mime::Type)
6
+ Mime::Type.register 'application/x-protobuf', :protobuf
7
+ end
@@ -0,0 +1,68 @@
1
+ require 'protip/error'
2
+ require 'net/http'
3
+
4
+ module Protip
5
+ module Client
6
+ extend ActiveSupport::Concern
7
+
8
+ attr_accessor :base_uri
9
+
10
+ # Makes a request and parses the response as a message of the given type.
11
+ # For internal use only; use the appropriate resource to make your requests.
12
+ #
13
+ # @param path [String] the URI path (exluding the base URI)
14
+ # @param method [Class] the HTTP method (e.g. `::Net::HTTP::Get`, `::Net::HTTP::Post`)
15
+ # @param message [Protobuf::Message|nil] the message to send as the request body
16
+ # @param response_type [Class] the `::Protobuf::Message` subclass that should be
17
+ # expected as a response
18
+ # @return [::Protobuf::Message] the decoded response from the server
19
+ def request(path:, method:, message:, response_type:)
20
+
21
+ raise RuntimeError.new('base_uri is not set') unless base_uri
22
+
23
+ uri = URI.join base_uri, path
24
+
25
+ request = method.new uri
26
+ request.body = (message == nil ? nil : message.encode)
27
+ request['Accept'] = 'application/x-protobuf'
28
+ request.content_type = 'application/x-protobuf'
29
+
30
+ prepare_request(request)
31
+
32
+ # TODO: Shared connection object for persisent connections.
33
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
34
+ http.request request
35
+ end
36
+
37
+ if response.is_a?(Net::HTTPUnprocessableEntity)
38
+ raise ::Protip::UnprocessableEntityError.new(request, response)
39
+ elsif response.is_a?(Net::HTTPNotFound)
40
+ raise ::Protip::NotFoundError.new(request, response)
41
+ elsif !response.is_a?(Net::HTTPSuccess)
42
+ raise ::Protip::Error.new(request, response)
43
+ end
44
+
45
+ if response_type
46
+ begin
47
+ response_type.decode response.body
48
+ # NotImplementedError catches the "Group is deprecated" exception raised by protobuf on some bad inputs.
49
+ # We may be able to remove it if we switch to a different protobuf gem.
50
+ rescue StandardError, NotImplementedError => error
51
+ raise ::Protip::ParseError.new error, request, response
52
+ end
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Invoked just before a request is sent to the API server. No-op by default, but
61
+ # implementations can override to add e.g. secret keys and user agent headers.
62
+ #
63
+ # @param request [Net::HTTPGenericRequest] the raw request object which is about to be sent
64
+ def prepare_request(request)
65
+ # No-op by default.
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,37 @@
1
+ # Missing dependency from the protobuf require
2
+ require 'protobuf'
3
+
4
+ require 'protip/messages/errors.pb'
5
+
6
+ module Protip
7
+ class Error < RuntimeError
8
+ attr_reader :request, :response
9
+ def initialize(request, response)
10
+ @request = request
11
+ @response = response
12
+ end
13
+
14
+ def inspect
15
+ "[#{self.class}] #{request.uri} -> code #{response.code}"
16
+ end
17
+ end
18
+
19
+ class ParseError < Error
20
+ attr_reader :original_error
21
+ def initialize(original_error, *args)
22
+ super(*args)
23
+ @original_error = original_error
24
+ end
25
+ end
26
+
27
+ class UnprocessableEntityError < Error
28
+ # Get the parsed errors object from a 422 response.
29
+ #
30
+ # @return ::Protip::Messages::Errors
31
+ def errors
32
+ ::Protip::Messages::Errors.decode response.body
33
+ end
34
+ end
35
+
36
+ class NotFoundError < Error ; end
37
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # This file is auto-generated. DO NOT EDIT!
5
+ #
6
+ require 'protobuf/message'
7
+
8
+ module Protip
9
+ module Messages
10
+
11
+ ##
12
+ # Message Classes
13
+ #
14
+ class Array < ::Protobuf::Message; end
15
+
16
+
17
+ ##
18
+ # Message Fields
19
+ #
20
+ class Array
21
+ repeated :bytes, :messages, 1
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # This file is auto-generated. DO NOT EDIT!
5
+ #
6
+ require 'protobuf/message'
7
+
8
+ module Protip
9
+ module Messages
10
+
11
+ ##
12
+ # Message Classes
13
+ #
14
+ class Errors < ::Protobuf::Message; end
15
+ class FieldError < ::Protobuf::Message; end
16
+
17
+
18
+ ##
19
+ # Message Fields
20
+ #
21
+ class Errors
22
+ repeated :string, :messages, 1
23
+ repeated ::Protip::Messages::FieldError, :field_errors, 2
24
+ end
25
+
26
+ class FieldError
27
+ optional :string, :field, 1
28
+ optional :string, :message, 2
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1,273 @@
1
+ # Missing dependencies from the other requires
2
+ require 'active_model/callbacks'
3
+ require 'active_model/validator'
4
+ require 'active_support/callbacks'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ require 'active_support/concern'
8
+ require 'active_support/core_ext/object/blank'
9
+
10
+ require 'active_model/validations'
11
+ require 'active_model/conversion'
12
+ require 'active_model/naming'
13
+ require 'active_model/translation'
14
+ require 'active_model/errors'
15
+
16
+ require 'protip/error'
17
+
18
+ require 'protip/messages/array.pb'
19
+
20
+ module Protip
21
+ module Resource
22
+
23
+ # Internal handlers for index/show actions. Never use these directly; instead, use `.all` and
24
+ # `.find` on the resource you're working with, since those methods will adjust their
25
+ # signatures to correctly parse a set of query parameters if supported.
26
+ module SearchMethods
27
+ # Fetch a list from the server at the collection's base endpoint. Expects the server response
28
+ # to be an array containing encoded messages that can be used to instantiate our resource.
29
+ #
30
+ # @param resource_class [Class] The resource type that we're fetching.
31
+ # @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
32
+ # @return [Array] The array of resources (each is an instance of the resource class we were
33
+ # initialized with).
34
+ def self.index(resource_class, query)
35
+ response = resource_class.client.request path: resource_class.base_path,
36
+ method: Net::HTTP::Get,
37
+ message: query,
38
+ response_type: Protip::Messages::Array
39
+ response.messages.map do |message|
40
+ resource_class.new resource_class.message.decode(message)
41
+ end
42
+ end
43
+
44
+ # Fetch a single resource from the server.
45
+ #
46
+ # @param resource_class [Class] The resource type that we're fetching.
47
+ # @param id [String] The ID to be used in the URL to fetch the resource.
48
+ # @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
49
+ # @return [Protip::Resource] An instance of our resource class, created from the server
50
+ # response.
51
+ def self.show(resource_class, id, query)
52
+ response = resource_class.client.request path: "#{resource_class.base_path}/#{id}",
53
+ method: Net::HTTP::Get,
54
+ message: query,
55
+ response_type: resource_class.message
56
+ resource_class.new response
57
+ end
58
+ end
59
+
60
+ # Mixin for a resource that has an active `:create` action. Should be treated as private,
61
+ # and will be included automatically when appropriate.
62
+ module Creatable
63
+ private
64
+ # POST the resource to the server and update our internal message. Private, since
65
+ # we should generally do this through the `save` method.
66
+ def create!
67
+ raise RuntimeError.new("Can't re-create a persisted object") if persisted?
68
+ @message = self.class.client.request path: self.class.base_path,
69
+ method: Net::HTTP::Post,
70
+ message: message,
71
+ response_type: self.class.message
72
+ end
73
+ end
74
+
75
+ # Mixin for a resource that has an active `:update` action. Should be treated as private,
76
+ # and will be included automatically when appropriate.
77
+ module Updatable
78
+ private
79
+ # PUT the resource on the server and update our internal message. Private, since
80
+ # we should generally do this through the `save` method.
81
+ def update!
82
+ raise RuntimeError.new("Can't update a non-persisted object") if !persisted?
83
+ @message = self.class.client.request path: "#{self.class.base_path}/#{id}",
84
+ method: Net::HTTP::Put,
85
+ message: message,
86
+ response_type: self.class.message
87
+ end
88
+ end
89
+
90
+ # Mixin for a resource that has an active `:destroy` action. Should be treated as private,
91
+ # and will be included automatically when appropriate.
92
+ module Destroyable
93
+ def destroy
94
+ raise RuntimeError.new("Can't destroy a non-persisted object") if !persisted?
95
+ @message = self.class.client.request path: "#{self.class.base_path}/#{id}",
96
+ method: Net::HTTP::Delete,
97
+ message: nil,
98
+ response_type: self.class.message
99
+ end
100
+ end
101
+
102
+ # Internal helpers for non-resourceful member/collection methods. Never use these directly;
103
+ # instead, use the instance/class methods which have been dynamically defined on the resource
104
+ # you're working with.
105
+ module ExtraMethods
106
+ def self.member(resource, action, method, message, response_type)
107
+ resource.class.client.request path: "#{resource.class.base_path}/#{resource.id}/#{action}",
108
+ method: method,
109
+ message: message,
110
+ response_type: response_type
111
+ end
112
+ def self.collection(resource_class, action, method, message, response_type)
113
+ resource_class.client.request path: "#{resource_class.base_path}/#{action}",
114
+ method: method,
115
+ message: message,
116
+ response_type: response_type
117
+ end
118
+ end
119
+
120
+ extend ActiveSupport::Concern
121
+
122
+ # Backport the ActiveModel::Model functionality - https://github.com/rails/rails/blob/097ca3f1f84bb9a2d3cda3f2cce7974a874efdf4/activemodel/lib/active_model/model.rb#L95
123
+ include ActiveModel::Validations
124
+ include ActiveModel::Conversion
125
+
126
+ included do
127
+ extend ActiveModel::Naming
128
+ extend ActiveModel::Translation
129
+ end
130
+ module ClassMethods
131
+
132
+ attr_accessor :client
133
+ attr_reader :message
134
+
135
+ attr_writer :base_path
136
+ def base_path
137
+ @base_path == nil ? raise(RuntimeError.new 'Base path not yet set') : @base_path.gsub(/\/$/, '')
138
+ end
139
+
140
+ private
141
+
142
+ # Primary entry point for defining resourceful behavior.
143
+ def resource(actions:, message:, query: nil)
144
+ if @message
145
+ raise RuntimeError.new('Only one call to `resource` is allowed')
146
+ end
147
+
148
+ # Define attribute readers/writers
149
+ @message = message
150
+ @message.all_fields.each do |field|
151
+ define_method :"#{field.name}" do
152
+ @message.public_send field.name
153
+ end
154
+ define_method :"#{field.name}=" do |value|
155
+ @message.public_send :"#{field.name}=", value
156
+ end
157
+ end
158
+
159
+ # Validate arguments
160
+ actions.map!{|action| action.to_sym}
161
+ (actions - %i(show index create update destroy)).each do |action|
162
+ raise ArgumentError.new("Unrecognized action: #{action}")
163
+ end
164
+
165
+ # For index/show, we want a different number of method arguments
166
+ # depending on whehter a query message was provided.
167
+ if query
168
+ if actions.include?(:show)
169
+ define_singleton_method :find do |id, query_params = {}|
170
+ SearchMethods.show(self, id, query.new(query_params))
171
+ end
172
+ end
173
+
174
+ if actions.include?(:index)
175
+ define_singleton_method :all do |query_params = {}|
176
+ SearchMethods.index(self, query.new(query_params))
177
+ end
178
+ end
179
+ else
180
+ if actions.include?(:show)
181
+ define_singleton_method :find do |id|
182
+ SearchMethods.show(self, id, nil)
183
+ end
184
+ end
185
+
186
+ if actions.include?(:index)
187
+ define_singleton_method :all do
188
+ SearchMethods.index(self, nil)
189
+ end
190
+ end
191
+ end
192
+
193
+ include(Creatable) if actions.include?(:create)
194
+ include(Updatable) if actions.include?(:update)
195
+ include(Destroyable) if actions.include?(:destroy)
196
+ end
197
+
198
+ def member(action:, method:, request: nil, response: nil)
199
+ if request
200
+ define_method action do |request_params = {}|
201
+ ExtraMethods.member self, action, method, request.new(request_params), response
202
+ end
203
+ else
204
+ define_method action do
205
+ ExtraMethods.member self, action, method, nil, response
206
+ end
207
+ end
208
+ end
209
+
210
+ def collection(action:, method:, request: nil, response: nil)
211
+ if request
212
+ define_singleton_method action do |request_params = {}|
213
+ ExtraMethods.collection self, action, method, request.new(request_params), response
214
+ end
215
+ else
216
+ define_singleton_method action do
217
+ ExtraMethods.collection self, action, method, nil, response
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ attr_reader :message
224
+ def initialize(message_or_params = {})
225
+ if self.class.message == nil
226
+ raise RuntimeError.new('Must define a message class using `resource`')
227
+ end
228
+ if message_or_params.is_a?(self.class.message)
229
+ @message = message_or_params
230
+ else
231
+ @message = self.class.message.new(message_or_params)
232
+ end
233
+
234
+ super()
235
+ end
236
+
237
+ def save
238
+ success = true
239
+ begin
240
+ if persisted?
241
+ # TODO: use `ActiveModel::Dirty` to only send changed attributes?
242
+ update!
243
+ else
244
+ create!
245
+ end
246
+ rescue Protip::UnprocessableEntityError => error
247
+ success = false
248
+ error.errors.messages.each do |message|
249
+ errors.add :base, message
250
+ end
251
+ error.errors.field_errors.each do |field_error|
252
+ errors.add field_error.field, field_error.message
253
+ end
254
+ end
255
+ success
256
+ end
257
+
258
+ def persisted?
259
+ message.field?(:id)
260
+ end
261
+
262
+ def attributes
263
+ self.class.message.all_fields.map{|field| field.name}.inject({}) do |hash, attribute_name|
264
+ hash[attribute_name] = message.field?(attribute_name) ? public_send(attribute_name) : nil
265
+ hash
266
+ end
267
+ end
268
+
269
+ def errors
270
+ @errors ||= ActiveModel::Errors.new(self)
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,191 @@
1
+ require 'test_helper'
2
+
3
+ require 'protip'
4
+
5
+ module Protip::ResourceTestFunctional # Namespace for internal constants
6
+ describe 'Protip::Resource (functional)' do
7
+
8
+ before do
9
+ WebMock.disable_net_connect!
10
+ end
11
+
12
+ # Make sure none of these are structurally identical (e.g. give fields
13
+ # different positions), to avoid potential errors where a message is
14
+ # incorrectly encode. Also define them as local constants so they don't
15
+ # interfere with any other tests.
16
+ class ResourceMessage < ::Protobuf::Message
17
+ optional :int64, :id, 1
18
+ optional :string, :ordered_tests, 2
19
+ end
20
+
21
+ class ResourceQuery < ::Protobuf::Message
22
+ optional :string, :param, 3
23
+ end
24
+
25
+ class NameResponse < ::Protobuf::Message
26
+ optional :string, :name, 4
27
+ end
28
+
29
+ class SearchRequest < ::Protobuf::Message
30
+ optional :string, :term, 5
31
+ end
32
+
33
+ class SearchResponse < ::Protobuf::Message
34
+ repeated :string, :results, 6
35
+ end
36
+
37
+ class FetchRequest < ::Protobuf::Message
38
+ repeated :string, :names, 7
39
+ end
40
+
41
+ class Client
42
+ include Protip::Client
43
+ def base_uri
44
+ 'https://external.service'
45
+ end
46
+ end
47
+
48
+ class Resource
49
+ include Protip::Resource
50
+ resource actions: [:index, :show, :create, :update, :destroy],
51
+ query: ResourceQuery, message: ResourceMessage
52
+
53
+ member action: :archive, method: Net::HTTP::Put
54
+ member action: :name, method: Net::HTTP::Get, response: NameResponse
55
+
56
+ collection action: :search, method: Net::HTTP::Get, request: SearchRequest, response: SearchResponse
57
+ collection action: :fetch, method: Net::HTTP::Post, request: FetchRequest
58
+
59
+ self.base_path = 'resources'
60
+ self.client = Client.new
61
+ end
62
+
63
+ describe '.all' do
64
+ describe 'with a successful server response' do
65
+ before do
66
+ response = Protip::Messages::Array.new(messages: ['bilbo', 'baggins'].each_with_index.map do |name, index|
67
+ ResourceMessage.new(id: index, ordered_tests: name).encode
68
+ end)
69
+ stub_request(:get, 'https://external.service/resources')
70
+ .to_return body: response.encode
71
+ end
72
+
73
+ it 'requests resources from the index endpoint' do
74
+ results = Resource.all param: 'val'
75
+
76
+ assert_requested :get, 'https://external.service/resources',
77
+ times: 1, body: ResourceQuery.new(param: 'val').encode
78
+
79
+ assert_equal 2, results.length, 'incorrect number of resources were returned'
80
+ results.each { |result| assert_instance_of Resource, result, 'incorrect type was parsed'}
81
+
82
+ assert_equal({ordered_tests: 'bilbo', id: 0}, results[0].attributes)
83
+ assert_equal({ordered_tests: 'baggins', id: 1}, results[1].attributes)
84
+ end
85
+
86
+ it 'allows requests without parameters' do
87
+ results = Resource.all
88
+ assert_requested :get, 'https://external.service/resources',
89
+ times: 1, body: ResourceQuery.new.encode
90
+ assert_equal 2, results.length, 'incorrect number of resources were returned'
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '.find' do
96
+ describe 'with a successful server response' do
97
+ before do
98
+ response = ResourceMessage.new(id: 311, ordered_tests: 'i_suck_and_my_tests_are_order_dependent!').encode
99
+ stub_request(:get, 'https://external.service/resources/311').to_return body: response.encode
100
+ end
101
+
102
+ it 'requests the resource from the show endpoint' do
103
+ resource = Resource.find 311, param: 'val'
104
+ assert_requested :get, 'https://external.service/resources/311', times: 1,
105
+ body: ResourceQuery.new(param: 'val').encode
106
+ assert_instance_of Resource, resource
107
+ assert_equal 311, resource.id
108
+ assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
109
+ end
110
+
111
+ it 'allows requests without parameters' do
112
+ resource = Resource.find 311
113
+ assert_requested :get, 'https://external.service/resources/311', times: 1,
114
+ body: ResourceQuery.new.encode
115
+ assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
116
+ end
117
+ end
118
+ end
119
+
120
+ describe '#save' do
121
+ let :resource_message do
122
+ ResourceMessage.new(id: 666, ordered_tests: 'yes')
123
+ end
124
+ let :errors_message do
125
+ Protip::Messages::Errors.new({
126
+ messages: ['base1', 'base2'],
127
+ field_errors: [
128
+ {field: 'ordered_tests', message: 'are not OK'}
129
+ ]
130
+ })
131
+ end
132
+
133
+ # Create and update cases are similar - we just modify the ID attribute on
134
+ # the initial resource, the HTTP method, and the expected URL.
135
+ [
136
+ [nil, :post, 'https://external.service/resources'],
137
+ [666, :put, 'https://external.service/resources/666']
138
+ ].each do |id, method, uri|
139
+ describe "with a #{id ? 'persisted' : 'non-persisted'} resource" do
140
+ before do
141
+ @resource = Resource.new id: id
142
+ end
143
+
144
+ describe 'with a successful server response' do
145
+ before do
146
+ stub_request(method, uri).to_return body: resource_message.encode
147
+ end
148
+
149
+ it 'returns true' do
150
+ assert @resource.save, 'save was not successful'
151
+ end
152
+
153
+ it 'saves the resource and parses the server response' do
154
+ @resource.ordered_tests = 'no'
155
+ @resource.save
156
+
157
+ assert_requested method, uri,
158
+ times: 1, body: ResourceMessage.new(id: id, ordered_tests: 'no').encode
159
+ assert_equal 'yes', @resource.ordered_tests
160
+ end
161
+ end
162
+
163
+ describe 'with a 422 server response' do
164
+ before do
165
+ stub_request(method, uri)
166
+ .to_return body: errors_message.encode, status: 422
167
+ end
168
+
169
+ it 'returns false' do
170
+ refute @resource.save, 'save appeared successful'
171
+ end
172
+
173
+ it 'adds errors based on the server response' do
174
+ @resource.save
175
+ assert_equal ['base1', 'base2'], @resource.errors['base']
176
+ assert_equal ['are not OK'], @resource.errors['ordered_tests']
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ describe '.member' do
184
+ # TODO
185
+ end
186
+
187
+ describe '.collection' do
188
+ # TODO
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,3 @@
1
+ require 'minitest/autorun'
2
+ require 'mocha/mini_test'
3
+ require 'webmock/minitest'
@@ -0,0 +1,494 @@
1
+ require 'test_helper'
2
+
3
+ require 'protip/client'
4
+ require 'protip/resource'
5
+
6
+ module Protip::ResourceTest # Namespace for internal constants
7
+ describe Protip::Resource do
8
+ class ResourceMessage < ::Protobuf::Message
9
+ optional :int64, :id, 1
10
+ optional :string, :string, 2
11
+ optional :string, :string2, 3
12
+ end
13
+ class ResourceQuery < ::Protobuf::Message
14
+ optional :string, :param, 1
15
+ end
16
+
17
+ # Give these things a different structure than ResourceQuery,
18
+ # just to avoid any possibility of decoding as the incorrect
19
+ # type but still yielding correct results.
20
+ class ActionQuery < ::Protobuf::Message
21
+ optional :string, :param, 4
22
+ end
23
+ class ActionResponse < ::Protobuf::Message
24
+ optional :string, :response, 3
25
+ end
26
+
27
+ # Stubbed API client
28
+ let :client do
29
+ mock.responds_like_instance_of(Class.new { include Protip::Client })
30
+ end
31
+
32
+ # Call `resource_class` to get an empty resource type.
33
+ let :resource_class do
34
+ resource_class = Class.new do
35
+ include Protip::Resource
36
+ self.base_path = 'base_path'
37
+ class << self
38
+ attr_accessor :client
39
+ end
40
+ end
41
+ resource_class.client = client
42
+ resource_class
43
+ end
44
+
45
+ describe '.resource' do
46
+ before do
47
+ resource_class.class_eval do
48
+ resource actions: [], message: ResourceMessage
49
+ end
50
+ end
51
+ it 'can only be invoked once' do
52
+ assert_raises RuntimeError do
53
+ resource_class.class_eval do
54
+ resource actions: [], message: ResourceMessage
55
+ end
56
+ end
57
+ end
58
+
59
+ it 'defines accessors for the fields on its message' do
60
+ resource = resource_class.new
61
+ [:id, :id=, :string, :string=].each do |method|
62
+ assert_respond_to resource, method
63
+ end
64
+ refute_respond_to resource, :foo
65
+ end
66
+
67
+ it 'sets fields on the underlying message when setters are called' do
68
+ resource = resource_class.new
69
+ resource.string = 'intern'
70
+ assert_equal 'intern', resource.message.string
71
+ assert_equal 'intern', resource.string
72
+ end
73
+ end
74
+
75
+ describe '.all' do
76
+ let :response do
77
+ Protip::Messages::Array.new({
78
+ messages: [
79
+ ResourceMessage.new(string: 'banjo', id: 1),
80
+ ResourceMessage.new(string: 'kazooie', id: 2),
81
+ ].map(&:encode)
82
+ })
83
+ end
84
+
85
+ it 'does not exist if the resource has not been defined' do
86
+ refute_respond_to resource_class, :all
87
+ end
88
+
89
+ it 'does not exist if the resource is defined without the index action' do
90
+ resource_class.class_eval do
91
+ resource actions: [:show], message: ResourceMessage
92
+ end
93
+ refute_respond_to resource_class, :all
94
+ end
95
+
96
+ describe 'without a query' do
97
+ before do
98
+ resource_class.class_eval do
99
+ resource actions: [:index], message: ResourceMessage
100
+ end
101
+ end
102
+
103
+ it 'requests an array from the index URL' do
104
+ client.expects(:request)
105
+ .once
106
+ .with(method: Net::HTTP::Get, path: 'base_path', message: nil, response_type: Protip::Messages::Array)
107
+ .returns(response)
108
+ resource_class.all
109
+ end
110
+
111
+ it 'fails if we try to pass in a query' do
112
+ assert_raises ArgumentError do
113
+ resource_class.all(query: 'param')
114
+ end
115
+ end
116
+
117
+ # Doesn't matter whether we have a query or not
118
+ it 'parses the response into an array of resources' do
119
+ client.stubs(:request).returns(response)
120
+ results = resource_class.all
121
+
122
+ assert_equal 2, results.length
123
+ results.each { |result| assert_instance_of resource_class, result }
124
+
125
+ assert_equal 'banjo', results[0].string
126
+ assert_equal 1, results[0].id
127
+
128
+ assert_equal 'kazooie', results[1].string
129
+ assert_equal 2, results[1].id
130
+ end
131
+
132
+ # Doesn't matter whether we have a query or not
133
+ it 'allows an empty array' do
134
+ client.stubs(:request).returns(Protip::Messages::Array.new)
135
+ results = resource_class.all
136
+
137
+ assert_equal [], results
138
+ end
139
+ end
140
+
141
+ describe 'with a query' do
142
+ before do
143
+ resource_class.class_eval do
144
+ resource actions: [:index], message: ResourceMessage, query: ResourceQuery
145
+ end
146
+ end
147
+
148
+ it 'requests an array from the index URL with the query' do
149
+ client.expects(:request)
150
+ .once
151
+ .with(method: Net::HTTP::Get, path: 'base_path',
152
+ message: ResourceQuery.new(param: 'val'), response_type: Protip::Messages::Array
153
+ ).returns(response)
154
+ resource_class.all(param: 'val')
155
+ end
156
+
157
+ it 'allows a request with an empty query' do
158
+ client.expects(:request)
159
+ .with(method: Net::HTTP::Get, path: 'base_path',
160
+ message: ResourceQuery.new, response_type: Protip::Messages::Array)
161
+ .returns(response)
162
+ resource_class.all
163
+ end
164
+ end
165
+ end
166
+
167
+ describe '.find' do
168
+ let :response do
169
+ ResourceMessage.new(string: 'pitbull', id: 100)
170
+ end
171
+
172
+ it 'does not exist if the resource has not been defined' do
173
+ refute_respond_to resource_class, :find
174
+ end
175
+
176
+ it 'does not exist if the resource is defined without the show action' do
177
+ resource_class.class_eval do
178
+ resource actions: [:index], message: ResourceMessage
179
+ end
180
+ refute_respond_to resource_class, :find
181
+ end
182
+
183
+ describe 'without a query' do
184
+ before do
185
+ resource_class.class_eval do
186
+ resource actions: [:show], message: ResourceMessage
187
+ end
188
+ end
189
+
190
+ it 'requests its message type from the show URL' do
191
+ client.expects(:request)
192
+ .once
193
+ .with(method: Net::HTTP::Get, path: 'base_path/3', message: nil, response_type: ResourceMessage)
194
+ .returns(response)
195
+ resource_class.find 3
196
+ end
197
+
198
+ it 'fails if we try to pass in a query' do
199
+ assert_raises ArgumentError do
200
+ resource_class.find 2, param: 'val'
201
+ end
202
+ end
203
+
204
+ # Doesn't matter whether we have a query or not
205
+ it 'parses the response message into a resource' do
206
+ client.stubs(:request).returns(response)
207
+ resource = resource_class.find 100
208
+ assert_instance_of resource_class, resource
209
+
210
+ assert_equal 100, resource.id
211
+ assert_equal 'pitbull', resource.string
212
+ end
213
+ end
214
+
215
+ describe 'with a query' do
216
+ before do
217
+ resource_class.class_eval do
218
+ resource actions: [:show], message: ResourceMessage, query: ResourceQuery
219
+ end
220
+ end
221
+
222
+ it 'requests its message type from the show URL with the query' do
223
+ client.expects(:request)
224
+ .once
225
+ .with(method: Net::HTTP::Get, path: 'base_path/5',
226
+ message: ResourceQuery.new(param: 'val'), response_type: ResourceMessage)
227
+ .returns(response)
228
+ resource_class.find 5, param: 'val'
229
+ end
230
+
231
+ it 'allows a request with an empty query' do
232
+ client.expects(:request)
233
+ .once
234
+ .with(method: Net::HTTP::Get, path: 'base_path/6',
235
+ message: ResourceQuery.new, response_type: ResourceMessage)
236
+ .returns(response)
237
+ resource_class.find 6
238
+ end
239
+ end
240
+ end
241
+
242
+ describe '#save' do
243
+ let :response do
244
+ ResourceMessage.new(string: 'pit', string2: 'bull', id: 200)
245
+ end
246
+
247
+ describe 'for a new record' do
248
+ before do
249
+ resource_class.class_eval do
250
+ resource actions: [:create], message: ResourceMessage
251
+ end
252
+ resource_class.any_instance.stubs(:persisted?).returns(false)
253
+ end
254
+
255
+ it 'sends the resource to the server' do
256
+ client.expects(:request)
257
+ .once
258
+ .with(method: Net::HTTP::Post, path: 'base_path',
259
+ message: ResourceMessage.new(string: 'time', string2: 'flees'), response_type: ResourceMessage)
260
+ .returns(response)
261
+
262
+ # Set via initializer and direct setter
263
+ resource = resource_class.new(string: 'time')
264
+ resource.string2 = 'flees'
265
+ resource.save
266
+ end
267
+
268
+ it 'returns true' do
269
+ client.stubs(:request).returns(response)
270
+ resource = resource_class.new string: 'flees'
271
+ assert resource.save, 'save returned false'
272
+ end
273
+
274
+ it 'updates its internal message store with the server response' do
275
+ client.stubs(:request).returns(response)
276
+ resource = resource_class.new
277
+ resource.save
278
+ assert_equal response, resource.message
279
+ end
280
+ end
281
+
282
+ describe 'for an existing record' do
283
+ before do
284
+ resource_class.class_eval do
285
+ resource actions: [:update], message: ResourceMessage
286
+ end
287
+ resource_class.any_instance.stubs(:persisted?).returns(true)
288
+ end
289
+
290
+ it 'sends the resource to the server' do
291
+ client.expects(:request)
292
+ .once
293
+ .with(method: Net::HTTP::Put, path: 'base_path/4',
294
+ message: ResourceMessage.new(id: 4, string: 'pitbull'), response_type: ResourceMessage)
295
+ .returns(response)
296
+
297
+ resource = resource_class.new(id: 4, string: 'pitbull')
298
+ resource.save
299
+ end
300
+
301
+ it 'returns true' do
302
+ client.stubs(:request).returns(response)
303
+ resource = resource_class.new id: 3
304
+ assert resource.save, 'save returned false'
305
+ end
306
+
307
+ it 'updates its internal message store with the server repsonse' do
308
+ client.stubs(:request).returns(response)
309
+ resource = resource_class.new id: 5
310
+ resource.save
311
+ assert_equal response, resource.message
312
+ end
313
+ end
314
+
315
+ describe 'when validation errors are thrown' do
316
+ before do
317
+ # Set up an errors instance variable that we can set actual messages on
318
+ @errors = Protip::Messages::Errors.new
319
+
320
+ exception = Protip::UnprocessableEntityError.new mock, mock
321
+ exception.stubs(:errors).returns @errors
322
+ client.stubs(:request).raises(exception)
323
+
324
+ resource_class.class_eval do
325
+ resource actions: [:update, :create], message: ResourceMessage
326
+ end
327
+ @resource = resource_class.new
328
+ end
329
+
330
+ it 'parses base errors' do
331
+ @errors.messages = ['message1', 'message2']
332
+ @resource.save
333
+
334
+ assert_equal ['message1', 'message2'], @resource.errors['base']
335
+ end
336
+
337
+ it 'parses field errors' do
338
+ @errors.field_errors = [
339
+ Protip::Messages::FieldError.new(field: 'string', message: 'message1'),
340
+ Protip::Messages::FieldError.new(field: 'id', message: 'message2'),
341
+ Protip::Messages::FieldError.new(field: 'string', message: 'message3'),
342
+ ]
343
+ @resource.save
344
+
345
+ assert_equal ['message1', 'message3'], @resource.errors['string']
346
+ assert_equal ['message2'], @resource.errors['id']
347
+ end
348
+
349
+ it 'returns false' do
350
+ refute @resource.save, 'save returned true'
351
+ end
352
+ end
353
+ end
354
+
355
+ describe '#destroy' do
356
+ describe 'for an existing record' do
357
+ let :response do
358
+ ResourceMessage.new(id: 5, string: 'deleted')
359
+ end
360
+ before do
361
+ resource_class.class_eval do
362
+ resource actions: [:destroy], message: ResourceMessage
363
+ end
364
+ resource_class.any_instance.stubs(:persisted?).returns(true)
365
+ end
366
+
367
+ it 'sends a delete request to the server' do
368
+ client.expects(:request)
369
+ .once
370
+ .with(method: Net::HTTP::Delete, path: 'base_path/79', message: nil, response_type: ResourceMessage)
371
+ .returns(response)
372
+ resource_class.new(id: 79).destroy
373
+ end
374
+
375
+ it 'updates its internal message with the server response' do
376
+ client.stubs(:request).returns(response)
377
+ resource = resource_class.new(id: 80)
378
+
379
+ resource.destroy
380
+ assert_equal response, resource.message
381
+ end
382
+ end
383
+ end
384
+
385
+ # member/collection have almost the same behavior, except for the URL and the target on which they're
386
+ # called. We assume that a `let(:target)` block has already been defined, which will yield the receiver
387
+ # of the non-resourceful method to be defined (e.g. a resource instance or resource class).
388
+ #
389
+ # @param defining_method [String] member or collection, e.g. the method to call in a `class_eval` block
390
+ # @param path [String] the URI that the client should expect to receive for an action of this type
391
+ # named 'action'
392
+ def self.describe_non_resourceful_action(defining_method, path)
393
+
394
+ # let(:target) is assumed to have been defined
395
+
396
+ let :response do
397
+ ActionResponse.new(response: 'bilbo')
398
+ end
399
+
400
+ before do
401
+ resource_class.class_eval do
402
+ resource actions: [], message: ResourceMessage
403
+ end
404
+ end
405
+ describe 'without a request or response type' do
406
+ before do
407
+ resource_class.class_eval do
408
+ send defining_method, action: :action, method: Net::HTTP::Put
409
+ end
410
+ end
411
+
412
+ it 'sends a request with no body and no response type to the expected endpoint' do
413
+ client.expects(:request)
414
+ .once
415
+ .with(method: Net::HTTP::Put, path: path, message: nil, response_type: nil)
416
+ .returns(nil)
417
+ target.action
418
+ end
419
+
420
+ it 'does not accept request parameters' do
421
+ assert_raises ArgumentError do
422
+ target.action param: 'val'
423
+ end
424
+ end
425
+
426
+ it 'returns nil' do
427
+ client.stubs(:request).returns(nil)
428
+ assert_nil target.action
429
+ end
430
+ end
431
+ describe 'with a request type' do
432
+ before do
433
+ resource_class.class_eval do
434
+ send defining_method, action: :action, method: Net::HTTP::Post, request: ActionQuery
435
+ end
436
+ end
437
+
438
+ it 'sends a request with a body to the expected endpoint' do
439
+ client.expects(:request)
440
+ .once
441
+ .with(method: Net::HTTP::Post, path: path,
442
+ message: ActionQuery.new(param: 'tom cruise'), response_type: nil)
443
+ .returns(nil)
444
+ target.action param: 'tom cruise'
445
+ end
446
+
447
+ it 'allows a request with no parameters' do
448
+ client.expects(:request)
449
+ .once
450
+ .with(method: Net::HTTP::Post, path: path,
451
+ message: ActionQuery.new, response_type: nil)
452
+ .returns(nil)
453
+ target.action
454
+ end
455
+ end
456
+
457
+ describe 'with a response type' do
458
+ before do
459
+ resource_class.class_eval do
460
+ send defining_method, action: :action, method: Net::HTTP::Get, response: ActionResponse
461
+ end
462
+ end
463
+
464
+ it 'sends a request with a specified response type to the expected endpoint' do
465
+ client.expects(:request)
466
+ .once
467
+ .with(method: Net::HTTP::Get, path: path,
468
+ message: nil, response_type: ActionResponse)
469
+ .returns(response)
470
+ target.action
471
+ end
472
+
473
+ it 'returns the server response' do
474
+ client.stubs(:request).returns(response)
475
+ assert_equal response, target.action
476
+ end
477
+ end
478
+ end
479
+
480
+ describe '.member' do
481
+ let :target do
482
+ resource_class.new id: 42
483
+ end
484
+ describe_non_resourceful_action 'member', 'base_path/42/action'
485
+ end
486
+
487
+ describe '.collection' do
488
+ let :target do
489
+ resource_class
490
+ end
491
+ describe_non_resourceful_action 'collection', 'base_path/action'
492
+ end
493
+ end
494
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.7
5
+ platform: ruby
6
+ authors:
7
+ - AngelList
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.0.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '5.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.0.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '5.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: protobuf
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.5'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.5'
67
+ - !ruby/object:Gem::Dependency
68
+ name: minitest
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '5.0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '5.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: mocha
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.1'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '1.1'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rake
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '10.0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '10.0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: simplecov
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '0.10'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.10'
123
+ - !ruby/object:Gem::Dependency
124
+ name: webmock
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '1.20'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '1.20'
137
+ description:
138
+ email:
139
+ - team@angel.co
140
+ - k2@angel.co
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - lib/protip.rb
146
+ - lib/protip/client.rb
147
+ - lib/protip/error.rb
148
+ - lib/protip/messages/array.pb.rb
149
+ - lib/protip/messages/errors.pb.rb
150
+ - lib/protip/resource.rb
151
+ - test/functional/protip/resource_test.rb
152
+ - test/test_helper.rb
153
+ - test/unit/protip/resource_test.rb
154
+ homepage:
155
+ licenses: []
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 2.1.0
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 2.4.5
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: Resources backed by protobuf messages
177
+ test_files: []