protip 0.9.7
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 +7 -0
- data/lib/protip.rb +7 -0
- data/lib/protip/client.rb +68 -0
- data/lib/protip/error.rb +37 -0
- data/lib/protip/messages/array.pb.rb +27 -0
- data/lib/protip/messages/errors.pb.rb +34 -0
- data/lib/protip/resource.rb +273 -0
- data/test/functional/protip/resource_test.rb +191 -0
- data/test/test_helper.rb +3 -0
- data/test/unit/protip/resource_test.rb +494 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/protip.rb
ADDED
@@ -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
|
data/lib/protip/error.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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: []
|