protip 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|