protip 0.13.1 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/protip/resource.rb +82 -134
- data/lib/protip/resource/creatable.rb +18 -0
- data/lib/protip/resource/destroyable.rb +15 -0
- data/lib/protip/resource/extra_methods.rb +23 -0
- data/lib/protip/resource/search_methods.rb +40 -0
- data/lib/protip/resource/updateable.rb +19 -0
- data/lib/protip/wrapper.rb +73 -24
- data/test/unit/protip/resource_test.rb +128 -63
- data/test/unit/protip/wrapper_test.rb +70 -2
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59a49ea006074b202ebfd0e2efff1c73aed317c9
|
4
|
+
data.tar.gz: fbd024d6595d62474d21154c00b9a91987ba3f6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53ee5dabef500a1987f28573379483168fa1a2ebe1872fc2e5e4cce49be7d3ee867138bfe8076b4ab9ce28fd79c808b6680951fa602783973d8d149b153de9c0
|
7
|
+
data.tar.gz: 5ba932bebcb02f060babaaa8c4afe58e2e722a7f3816adddc61aeae93d632d0702f2e243f9ca0d98ea7b452fb49a0a1b5d088de67e98705bb2d00d58f7e936cc
|
data/lib/protip/resource.rb
CHANGED
@@ -24,111 +24,18 @@ require 'protip/wrapper'
|
|
24
24
|
|
25
25
|
require 'protip/messages/array'
|
26
26
|
|
27
|
+
require 'protip/resource/creatable'
|
28
|
+
require 'protip/resource/updateable'
|
29
|
+
require 'protip/resource/destroyable'
|
30
|
+
require 'protip/resource/extra_methods'
|
31
|
+
require 'protip/resource/search_methods'
|
32
|
+
|
27
33
|
module Protip
|
28
34
|
module Resource
|
29
|
-
|
30
|
-
# Internal handlers for index/show actions. Never use these directly; instead, use `.all` and
|
31
|
-
# `.find` on the resource you're working with, since those methods will adjust their
|
32
|
-
# signatures to correctly parse a set of query parameters if supported.
|
33
|
-
module SearchMethods
|
34
|
-
# Fetch a list from the server at the collection's base endpoint. Expects the server response
|
35
|
-
# to be an array containing encoded messages that can be used to instantiate our resource.
|
36
|
-
#
|
37
|
-
# @param resource_class [Class] The resource type that we're fetching.
|
38
|
-
# @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
|
39
|
-
# @return [Array] The array of resources (each is an instance of the resource class we were
|
40
|
-
# initialized with).
|
41
|
-
def self.index(resource_class, query)
|
42
|
-
response = resource_class.client.request path: resource_class.base_path,
|
43
|
-
method: Net::HTTP::Get,
|
44
|
-
message: query,
|
45
|
-
response_type: Protip::Messages::Array
|
46
|
-
response.messages.map do |message|
|
47
|
-
resource_class.new resource_class.message.decode(message)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Fetch a single resource from the server.
|
52
|
-
#
|
53
|
-
# @param resource_class [Class] The resource type that we're fetching.
|
54
|
-
# @param id [String] The ID to be used in the URL to fetch the resource.
|
55
|
-
# @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
|
56
|
-
# @return [Protip::Resource] An instance of our resource class, created from the server
|
57
|
-
# response.
|
58
|
-
def self.show(resource_class, id, query)
|
59
|
-
response = resource_class.client.request path: "#{resource_class.base_path}/#{id}",
|
60
|
-
method: Net::HTTP::Get,
|
61
|
-
message: query,
|
62
|
-
response_type: resource_class.message
|
63
|
-
resource_class.new response
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Mixin for a resource that has an active `:create` action. Should be treated as private,
|
68
|
-
# and will be included automatically when appropriate.
|
69
|
-
module Creatable
|
70
|
-
private
|
71
|
-
# POST the resource to the server and update our internal message. Private, since
|
72
|
-
# we should generally do this through the `save` method.
|
73
|
-
def create!
|
74
|
-
raise RuntimeError.new("Can't re-create a persisted object") if persisted?
|
75
|
-
self.message = self.class.client.request path: self.class.base_path,
|
76
|
-
method: Net::HTTP::Post,
|
77
|
-
message: message,
|
78
|
-
response_type: self.class.message
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Mixin for a resource that has an active `:update` action. Should be treated as private,
|
83
|
-
# and will be included automatically when appropriate.
|
84
|
-
module Updatable
|
85
|
-
private
|
86
|
-
# PUT the resource on the server and update our internal message. Private, since
|
87
|
-
# we should generally do this through the `save` method.
|
88
|
-
def update!
|
89
|
-
raise RuntimeError.new("Can't update a non-persisted object") if !persisted?
|
90
|
-
self.message = self.class.client.request path: "#{self.class.base_path}/#{id}",
|
91
|
-
method: Net::HTTP::Put,
|
92
|
-
message: message,
|
93
|
-
response_type: self.class.message
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# Mixin for a resource that has an active `:destroy` action. Should be treated as private,
|
98
|
-
# and will be included automatically when appropriate.
|
99
|
-
module Destroyable
|
100
|
-
def destroy
|
101
|
-
raise RuntimeError.new("Can't destroy a non-persisted object") if !persisted?
|
102
|
-
self.message = self.class.client.request path: "#{self.class.base_path}/#{id}",
|
103
|
-
method: Net::HTTP::Delete,
|
104
|
-
message: nil,
|
105
|
-
response_type: self.class.message
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# Internal helpers for non-resourceful member/collection methods. Never use these directly;
|
110
|
-
# instead, use the instance/class methods which have been dynamically defined on the resource
|
111
|
-
# you're working with.
|
112
|
-
module ExtraMethods
|
113
|
-
def self.member(resource, action, method, message, response_type)
|
114
|
-
response = resource.class.client.request path: "#{resource.class.base_path}/#{resource.id}/#{action}",
|
115
|
-
method: method,
|
116
|
-
message: message,
|
117
|
-
response_type: response_type
|
118
|
-
nil == response ? nil : ::Protip::Wrapper.new(response, resource.class.converter)
|
119
|
-
end
|
120
|
-
def self.collection(resource_class, action, method, message, response_type)
|
121
|
-
response = resource_class.client.request path: "#{resource_class.base_path}/#{action}",
|
122
|
-
method: method,
|
123
|
-
message: message,
|
124
|
-
response_type: response_type
|
125
|
-
nil == response ? nil : ::Protip::Wrapper.new(response, resource_class.converter)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
35
|
extend ActiveSupport::Concern
|
130
36
|
|
131
|
-
# Backport the ActiveModel::Model functionality
|
37
|
+
# Backport the ActiveModel::Model functionality
|
38
|
+
# https://github.com/rails/rails/blob/097ca3f1f84bb9a2d3cda3f2cce7974a874efdf4/activemodel/lib/active_model/model.rb#L95
|
132
39
|
include ActiveModel::Validations
|
133
40
|
include ActiveModel::Conversion
|
134
41
|
|
@@ -142,18 +49,25 @@ module Protip
|
|
142
49
|
def_delegator :@wrapper, :message
|
143
50
|
def_delegator :@wrapper, :as_json
|
144
51
|
end
|
52
|
+
|
145
53
|
module ClassMethods
|
146
54
|
|
55
|
+
VALID_ACTIONS = %i(show index create update destroy)
|
56
|
+
|
147
57
|
attr_accessor :client
|
148
58
|
|
149
|
-
attr_reader :message
|
59
|
+
attr_reader :message, :nested_resources
|
60
|
+
|
61
|
+
attr_writer :base_path, :converter
|
150
62
|
|
151
|
-
attr_writer :base_path
|
152
63
|
def base_path
|
153
|
-
@base_path == nil
|
64
|
+
if @base_path == nil
|
65
|
+
raise(RuntimeError.new 'Base path not yet set')
|
66
|
+
else
|
67
|
+
@base_path.gsub(/\/$/, '')
|
68
|
+
end
|
154
69
|
end
|
155
70
|
|
156
|
-
attr_writer :converter
|
157
71
|
def converter
|
158
72
|
@converter || (@_standard_converter ||= Protip::StandardConverter.new)
|
159
73
|
end
|
@@ -161,14 +75,51 @@ module Protip
|
|
161
75
|
private
|
162
76
|
|
163
77
|
# Primary entry point for defining resourceful behavior.
|
164
|
-
def resource(actions:, message:, query: nil)
|
165
|
-
if @message
|
166
|
-
|
167
|
-
|
78
|
+
def resource(actions:, message:, query: nil, nested_resources: {})
|
79
|
+
raise RuntimeError.new('Only one call to `resource` is allowed') if @message
|
80
|
+
validate_actions!(actions)
|
81
|
+
validate_nested_resources!(nested_resources)
|
168
82
|
|
169
|
-
# Define attribute readers/writers
|
170
83
|
@message = message
|
171
|
-
@
|
84
|
+
@nested_resources = nested_resources
|
85
|
+
|
86
|
+
define_attribute_accessors(@message)
|
87
|
+
define_oneof_group_methods(@message)
|
88
|
+
define_resource_query_methods(query, actions)
|
89
|
+
|
90
|
+
include(::Protip::Resource::Creatable) if actions.include?(:create)
|
91
|
+
include(::Protip::Resource::Updatable) if actions.include?(:update)
|
92
|
+
include(::Protip::Resource::Destroyable) if actions.include?(:destroy)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_nested_resources!(nested_resources)
|
96
|
+
nested_resources.each do |key, resource_klass|
|
97
|
+
unless key.is_a?(Symbol)
|
98
|
+
raise "#{key} must be a Symbol, but is a #{key.class}"
|
99
|
+
end
|
100
|
+
unless resource_klass < ::Protip::Resource
|
101
|
+
raise "#{resource_klass} is not a Protip::Resource"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_actions!(actions)
|
107
|
+
actions.map!{|action| action.to_sym}
|
108
|
+
(actions - VALID_ACTIONS).each do |action|
|
109
|
+
raise ArgumentError.new("Unrecognized action: #{action}")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Allow calls to oneof groups to get the set oneof field
|
114
|
+
def define_oneof_group_methods(message)
|
115
|
+
message.descriptor.each_oneof do |oneof_field|
|
116
|
+
def_delegator :@wrapper, :"#{oneof_field.name}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Define attribute readers/writers
|
121
|
+
def define_attribute_accessors(message)
|
122
|
+
message.descriptor.each do |field|
|
172
123
|
def_delegator :@wrapper, :"#{field.name}"
|
173
124
|
if ::Protip::Wrapper.matchable?(field)
|
174
125
|
def_delegator :@wrapper, :"#{field.name}?"
|
@@ -182,25 +133,21 @@ module Protip
|
|
182
133
|
# needed for ActiveModel::Dirty
|
183
134
|
send("#{field.name}_will_change!") if new_wrapped_value != old_wrapped_value
|
184
135
|
end
|
185
|
-
end
|
186
136
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
# Validate arguments
|
191
|
-
actions.map!{|action| action.to_sym}
|
192
|
-
(actions - %i(show index create update destroy)).each do |action|
|
193
|
-
raise ArgumentError.new("Unrecognized action: #{action}")
|
137
|
+
# needed for ActiveModel::Dirty
|
138
|
+
define_attribute_method field.name
|
194
139
|
end
|
140
|
+
end
|
195
141
|
|
196
|
-
|
197
|
-
|
142
|
+
# For index/show, we want a different number of method arguments
|
143
|
+
# depending on whether a query message was provided.
|
144
|
+
def define_resource_query_methods(query, actions)
|
198
145
|
if query
|
199
146
|
if actions.include?(:show)
|
200
147
|
define_singleton_method :find do |id, query_params = {}|
|
201
148
|
wrapper = ::Protip::Wrapper.new(query.new, converter)
|
202
149
|
wrapper.assign_attributes query_params
|
203
|
-
SearchMethods.show(self, id, wrapper.message)
|
150
|
+
::Protip::Resource::SearchMethods.show(self, id, wrapper.message)
|
204
151
|
end
|
205
152
|
end
|
206
153
|
|
@@ -208,26 +155,22 @@ module Protip
|
|
208
155
|
define_singleton_method :all do |query_params = {}|
|
209
156
|
wrapper = ::Protip::Wrapper.new(query.new, converter)
|
210
157
|
wrapper.assign_attributes query_params
|
211
|
-
SearchMethods.index(self, wrapper.message)
|
158
|
+
::Protip::Resource::SearchMethods.index(self, wrapper.message)
|
212
159
|
end
|
213
160
|
end
|
214
161
|
else
|
215
162
|
if actions.include?(:show)
|
216
163
|
define_singleton_method :find do |id|
|
217
|
-
SearchMethods.show(self, id, nil)
|
164
|
+
::Protip::Resource::SearchMethods.show(self, id, nil)
|
218
165
|
end
|
219
166
|
end
|
220
167
|
|
221
168
|
if actions.include?(:index)
|
222
169
|
define_singleton_method :all do
|
223
|
-
SearchMethods.index(self, nil)
|
170
|
+
::Protip::Resource::SearchMethods.index(self, nil)
|
224
171
|
end
|
225
172
|
end
|
226
173
|
end
|
227
|
-
|
228
|
-
include(Creatable) if actions.include?(:create)
|
229
|
-
include(Updatable) if actions.include?(:update)
|
230
|
-
include(Destroyable) if actions.include?(:destroy)
|
231
174
|
end
|
232
175
|
|
233
176
|
def member(action:, method:, request: nil, response: nil)
|
@@ -235,11 +178,11 @@ module Protip
|
|
235
178
|
define_method action do |request_params = {}|
|
236
179
|
wrapper = ::Protip::Wrapper.new(request.new, self.class.converter)
|
237
180
|
wrapper.assign_attributes request_params
|
238
|
-
ExtraMethods.member self, action, method, wrapper.message, response
|
181
|
+
::Protip::Resource::ExtraMethods.member self, action, method, wrapper.message, response
|
239
182
|
end
|
240
183
|
else
|
241
184
|
define_method action do
|
242
|
-
ExtraMethods.member self, action, method, nil, response
|
185
|
+
::Protip::Resource::ExtraMethods.member self, action, method, nil, response
|
243
186
|
end
|
244
187
|
end
|
245
188
|
end
|
@@ -249,11 +192,15 @@ module Protip
|
|
249
192
|
define_singleton_method action do |request_params = {}|
|
250
193
|
wrapper = ::Protip::Wrapper.new(request.new, converter)
|
251
194
|
wrapper.assign_attributes request_params
|
252
|
-
ExtraMethods.collection self,
|
195
|
+
::Protip::Resource::ExtraMethods.collection self,
|
196
|
+
action,
|
197
|
+
method,
|
198
|
+
wrapper.message,
|
199
|
+
response
|
253
200
|
end
|
254
201
|
else
|
255
202
|
define_singleton_method action do
|
256
|
-
ExtraMethods.collection self, action, method, nil, response
|
203
|
+
::Protip::Resource::ExtraMethods.collection self, action, method, nil, response
|
257
204
|
end
|
258
205
|
end
|
259
206
|
end
|
@@ -280,7 +227,7 @@ module Protip
|
|
280
227
|
end
|
281
228
|
|
282
229
|
def message=(message)
|
283
|
-
@wrapper = Protip::Wrapper.new(message, self.class.converter)
|
230
|
+
@wrapper = Protip::Wrapper.new(message, self.class.converter, self.class.nested_resources)
|
284
231
|
end
|
285
232
|
|
286
233
|
def save
|
@@ -328,5 +275,6 @@ module Protip
|
|
328
275
|
@previously_changed = changes
|
329
276
|
@changed_attributes.clear
|
330
277
|
end
|
278
|
+
|
331
279
|
end
|
332
280
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Protip
|
2
|
+
module Resource
|
3
|
+
# Mixin for a resource that has an active `:create` action. Should be treated as private,
|
4
|
+
# and will be included automatically when appropriate.
|
5
|
+
module Creatable
|
6
|
+
private
|
7
|
+
# POST the resource to the server and update our internal message. Private, since
|
8
|
+
# we should generally do this through the `save` method.
|
9
|
+
def create!
|
10
|
+
raise RuntimeError.new("Can't re-create a persisted object") if persisted?
|
11
|
+
self.message = self.class.client.request path: self.class.base_path,
|
12
|
+
method: Net::HTTP::Post,
|
13
|
+
message: message,
|
14
|
+
response_type: self.class.message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Protip
|
2
|
+
module Resource
|
3
|
+
# Mixin for a resource that has an active `:destroy` action. Should be treated as private,
|
4
|
+
# and will be included automatically when appropriate.
|
5
|
+
module Destroyable
|
6
|
+
def destroy
|
7
|
+
raise RuntimeError.new("Can't destroy a non-persisted object") if !persisted?
|
8
|
+
self.message = self.class.client.request path: "#{self.class.base_path}/#{id}",
|
9
|
+
method: Net::HTTP::Delete,
|
10
|
+
message: nil,
|
11
|
+
response_type: self.class.message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Protip
|
2
|
+
module Resource
|
3
|
+
# Internal helpers for non-resourceful member/collection methods. Never use these directly;
|
4
|
+
# instead, use the instance/class methods which have been dynamically defined on the resource
|
5
|
+
# you're working with.
|
6
|
+
module ExtraMethods
|
7
|
+
def self.member(resource, action, method, message, response_type)
|
8
|
+
response = resource.class.client.request path: "#{resource.class.base_path}/#{resource.id}/#{action}",
|
9
|
+
method: method,
|
10
|
+
message: message,
|
11
|
+
response_type: response_type
|
12
|
+
nil == response ? nil : ::Protip::Wrapper.new(response, resource.class.converter)
|
13
|
+
end
|
14
|
+
def self.collection(resource_class, action, method, message, response_type)
|
15
|
+
response = resource_class.client.request path: "#{resource_class.base_path}/#{action}",
|
16
|
+
method: method,
|
17
|
+
message: message,
|
18
|
+
response_type: response_type
|
19
|
+
nil == response ? nil : ::Protip::Wrapper.new(response, resource_class.converter)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Protip
|
2
|
+
module Resource
|
3
|
+
# Internal handlers for index/show actions. Never use these directly; instead, use `.all` and
|
4
|
+
# `.find` on the resource you're working with, since those methods will adjust their
|
5
|
+
# signatures to correctly parse a set of query parameters if supported.
|
6
|
+
module SearchMethods
|
7
|
+
# Fetch a list from the server at the collection's base endpoint. Expects the server response
|
8
|
+
# to be an array containing encoded messages that can be used to instantiate our resource.
|
9
|
+
#
|
10
|
+
# @param resource_class [Class] The resource type that we're fetching.
|
11
|
+
# @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
|
12
|
+
# @return [Array] The array of resources (each is an instance of the resource class we were
|
13
|
+
# initialized with).
|
14
|
+
def self.index(resource_class, query)
|
15
|
+
response = resource_class.client.request path: resource_class.base_path,
|
16
|
+
method: Net::HTTP::Get,
|
17
|
+
message: query,
|
18
|
+
response_type: Protip::Messages::Array
|
19
|
+
response.messages.map do |message|
|
20
|
+
resource_class.new resource_class.message.decode(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetch a single resource from the server.
|
25
|
+
#
|
26
|
+
# @param resource_class [Class] The resource type that we're fetching.
|
27
|
+
# @param id [String] The ID to be used in the URL to fetch the resource.
|
28
|
+
# @param query [::Protobuf::Message|NilClass] An optional query to send along with the request.
|
29
|
+
# @return [Protip::Resource] An instance of our resource class, created from the server
|
30
|
+
# response.
|
31
|
+
def self.show(resource_class, id, query)
|
32
|
+
response = resource_class.client.request path: "#{resource_class.base_path}/#{id}",
|
33
|
+
method: Net::HTTP::Get,
|
34
|
+
message: query,
|
35
|
+
response_type: resource_class.message
|
36
|
+
resource_class.new response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Protip
|
2
|
+
module Resource
|
3
|
+
# Mixin for a resource that has an active `:update` action. Should be treated as private,
|
4
|
+
# and will be included automatically when appropriate.
|
5
|
+
module Updatable
|
6
|
+
private
|
7
|
+
# PUT the resource on the server and update our internal message. Private, since
|
8
|
+
# we should generally do this through the `save` method.
|
9
|
+
def update!
|
10
|
+
raise RuntimeError.new("Can't update a non-persisted object") if !persisted?
|
11
|
+
self.message = self.class.client.request path: "#{self.class.base_path}/#{id}",
|
12
|
+
method: Net::HTTP::Put,
|
13
|
+
message: message,
|
14
|
+
response_type: self.class.message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/protip/wrapper.rb
CHANGED
@@ -7,18 +7,24 @@ module Protip
|
|
7
7
|
# - mass assignment of attributes
|
8
8
|
# - standardized creation of nested messages that can't be converted to/from Ruby objects
|
9
9
|
class Wrapper
|
10
|
-
|
11
|
-
|
10
|
+
|
11
|
+
attr_reader :message, :converter, :nested_resources
|
12
|
+
|
13
|
+
def initialize(message, converter, nested_resources={})
|
12
14
|
@message = message
|
13
15
|
@converter = converter
|
16
|
+
@nested_resources = nested_resources
|
14
17
|
end
|
15
18
|
|
16
19
|
def respond_to?(name)
|
17
20
|
if super
|
18
21
|
true
|
19
22
|
else
|
23
|
+
# Responds to calls to oneof groups by name
|
24
|
+
return true if message.class.descriptor.lookup_oneof(name.to_s)
|
25
|
+
|
26
|
+
# Responds to field getters, setters, and in the scalar enum case, query methods
|
20
27
|
message.class.descriptor.any? do |field|
|
21
|
-
# getter, setter, and in the scalar enum case, query method
|
22
28
|
regex = /^#{field.name}[=#{self.class.matchable?(field) ? '\\?' : ''}]?$/
|
23
29
|
name.to_s =~ regex
|
24
30
|
end
|
@@ -26,27 +32,22 @@ module Protip
|
|
26
32
|
end
|
27
33
|
|
28
34
|
def method_missing(name, *args)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
raise ArgumentError unless args.length == 0
|
46
|
-
get field
|
47
|
-
else
|
48
|
-
super
|
49
|
-
end
|
35
|
+
descriptor = message.class.descriptor
|
36
|
+
|
37
|
+
is_setter_method = name =~ /=$/
|
38
|
+
return method_missing_setter(name, *args) if is_setter_method
|
39
|
+
|
40
|
+
is_query_method = name =~ /\?$/
|
41
|
+
return method_missing_query(name, *args) if is_query_method
|
42
|
+
|
43
|
+
field = descriptor.detect{|field| field.name.to_sym == name}
|
44
|
+
return method_missing_field(field, *args) if field
|
45
|
+
|
46
|
+
oneof_descriptor = descriptor.lookup_oneof(name.to_s)
|
47
|
+
# For calls to a oneof group, return the active oneof field, or nil if there isn't one
|
48
|
+
return method_missing_oneof(oneof_descriptor) if oneof_descriptor
|
49
|
+
|
50
|
+
super
|
50
51
|
end
|
51
52
|
|
52
53
|
# Create a nested field on our message. For example, given the following definitions:
|
@@ -174,10 +175,14 @@ module Protip
|
|
174
175
|
# Helper for getting values - converts the value for the given field to one that we can return to the user
|
175
176
|
def to_ruby_value(field, value)
|
176
177
|
if field.type == :message
|
178
|
+
field_name_sym = field.name.to_sym
|
177
179
|
if nil == value
|
178
180
|
nil
|
179
181
|
elsif converter.convertible?(field.subtype.msgclass)
|
180
182
|
converter.to_object value
|
183
|
+
elsif nested_resources.has_key?(field_name_sym)
|
184
|
+
resource_klass = nested_resources[field_name_sym]
|
185
|
+
resource_klass.new value
|
181
186
|
else
|
182
187
|
self.class.new value, converter
|
183
188
|
end
|
@@ -199,10 +204,14 @@ module Protip
|
|
199
204
|
if field.type == :message
|
200
205
|
if nil == value
|
201
206
|
nil
|
207
|
+
# This check must happen before the nested_resources check to ensure nested messages
|
208
|
+
# are set properly
|
202
209
|
elsif value.is_a?(field.subtype.msgclass)
|
203
210
|
value
|
204
211
|
elsif converter.convertible?(field.subtype.msgclass)
|
205
212
|
converter.to_message value, field.subtype.msgclass
|
213
|
+
elsif nested_resources.has_key?(field.name.to_sym)
|
214
|
+
value.message
|
206
215
|
else
|
207
216
|
raise ArgumentError.new "Cannot convert from Ruby object: \"#{field}\""
|
208
217
|
end
|
@@ -225,5 +234,45 @@ module Protip
|
|
225
234
|
get(field) == sym
|
226
235
|
|
227
236
|
end
|
237
|
+
|
238
|
+
def method_missing_oneof(oneof_descriptor)
|
239
|
+
oneof_field_name = message.send(oneof_descriptor.name)
|
240
|
+
return if oneof_field_name.nil?
|
241
|
+
oneof_field_name = oneof_field_name.to_s
|
242
|
+
oneof_field = oneof_descriptor.detect {|field| field.name == oneof_field_name}
|
243
|
+
oneof_field ? get(oneof_field) : nil
|
244
|
+
end
|
245
|
+
|
246
|
+
def method_missing_field(field, *args)
|
247
|
+
if field
|
248
|
+
raise ArgumentError unless args.length == 0
|
249
|
+
get(field)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def method_missing_query(name, *args)
|
254
|
+
field = message.class.descriptor.detect do |field|
|
255
|
+
self.class.matchable?(field) && :"#{field.name}?" == name
|
256
|
+
end
|
257
|
+
if args.length == 1
|
258
|
+
# this is an enum query, e.g. `state?(:CREATED)`
|
259
|
+
matches? field, args[0]
|
260
|
+
elsif args.length == 0
|
261
|
+
# this is a boolean query, e.g. `approved?`
|
262
|
+
get field
|
263
|
+
else
|
264
|
+
raise ArgumentError
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def method_missing_setter(name, *args)
|
269
|
+
field = message.class.descriptor.detect{|field| :"#{field.name}=" == name}
|
270
|
+
if field
|
271
|
+
raise ArgumentError unless args.length == 1
|
272
|
+
attributes = {}.tap { |hash| hash[field.name] = args[0] }
|
273
|
+
assign_attributes attributes
|
274
|
+
return args[0] # return the input value (to match ActiveRecord behavior)
|
275
|
+
end
|
276
|
+
end
|
228
277
|
end
|
229
278
|
end
|
@@ -7,6 +7,8 @@ require 'protip/resource'
|
|
7
7
|
module Protip::ResourceTest # Namespace for internal constants
|
8
8
|
describe Protip::Resource do
|
9
9
|
let :pool do
|
10
|
+
# See https://github.com/google/protobuf/blob/master/ruby/tests/generated_code.rb for
|
11
|
+
# examples of field types you can add here
|
10
12
|
pool = Google::Protobuf::DescriptorPool.new
|
11
13
|
pool.build do
|
12
14
|
add_enum 'number' do
|
@@ -30,6 +32,11 @@ module Protip::ResourceTest # Namespace for internal constants
|
|
30
32
|
repeated :booleans, :bool, 9
|
31
33
|
optional :google_bool_value, :message, 10, "google.protobuf.BoolValue"
|
32
34
|
repeated :google_bool_values, :message, 11, "google.protobuf.BoolValue"
|
35
|
+
|
36
|
+
oneof :oneof_group do
|
37
|
+
optional :oneof_string1, :string, 12
|
38
|
+
optional :oneof_string2, :string, 13
|
39
|
+
end
|
33
40
|
end
|
34
41
|
|
35
42
|
add_message 'resource_query' do
|
@@ -81,95 +88,153 @@ module Protip::ResourceTest # Namespace for internal constants
|
|
81
88
|
include Protip::Converter
|
82
89
|
end.new
|
83
90
|
end
|
91
|
+
describe 'with basic resource' do
|
92
|
+
before do
|
93
|
+
resource_class.class_exec(converter, resource_message_class) do |converter, message|
|
94
|
+
resource actions: [], message: message
|
95
|
+
self.converter = converter
|
96
|
+
end
|
97
|
+
end
|
84
98
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
99
|
+
it 'can only be invoked once' do
|
100
|
+
assert_raises RuntimeError do
|
101
|
+
resource_class.class_exec(resource_message_class) do |message|
|
102
|
+
resource actions: [], message: message
|
103
|
+
end
|
104
|
+
end
|
89
105
|
end
|
90
|
-
end
|
91
106
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
resource
|
107
|
+
it 'defines accessors for the fields on its message' do
|
108
|
+
resource = resource_class.new
|
109
|
+
[:id, :string].each do |method|
|
110
|
+
assert_respond_to resource, method
|
96
111
|
end
|
112
|
+
refute_respond_to resource, :foo
|
97
113
|
end
|
98
|
-
end
|
99
114
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
115
|
+
it 'defines accessors for oneof groups on its message' do
|
116
|
+
resource = resource_class.new
|
117
|
+
group_name = 'oneof_group'
|
118
|
+
assert resource.message.class.descriptor.lookup_oneof(group_name)
|
119
|
+
assert_respond_to resource, group_name
|
104
120
|
end
|
105
|
-
refute_respond_to resource, :foo
|
106
|
-
end
|
107
121
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
assert_equal 'intern', resource.string
|
113
|
-
end
|
122
|
+
it 'returns nil if the oneof group accessor called when the underlying fields are not set' do
|
123
|
+
resource = resource_class.new
|
124
|
+
assert_nil resource.oneof_group
|
125
|
+
end
|
114
126
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
127
|
+
it 'returns the active oneof field when a oneof group accessor is called' do
|
128
|
+
resource = resource_class.new
|
129
|
+
foo, bar = 'foo', 'bar'
|
130
|
+
resource.oneof_string1 = foo
|
131
|
+
assert_equal resource.oneof_string1, resource.oneof_group
|
132
|
+
resource.oneof_string2 = bar
|
133
|
+
assert_equal resource.oneof_string2, resource.oneof_group
|
134
|
+
resource.oneof_string2 = bar
|
135
|
+
resource.oneof_string1 = foo
|
136
|
+
assert_equal resource.oneof_string1, resource.oneof_group
|
137
|
+
end
|
120
138
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
resource.
|
139
|
+
it 'sets fields on the underlying message when simple setters are called' do
|
140
|
+
resource = resource_class.new
|
141
|
+
resource.string = 'intern'
|
142
|
+
assert_equal 'intern', resource.message.string
|
143
|
+
assert_equal 'intern', resource.string
|
126
144
|
end
|
127
|
-
end
|
128
145
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
146
|
+
it 'never checks with the converter when setting simple types' do
|
147
|
+
converter.expects(:convertible?).never
|
148
|
+
resource = resource_class.new
|
149
|
+
resource.string = 'intern'
|
150
|
+
end
|
133
151
|
|
134
|
-
|
135
|
-
|
152
|
+
it 'checks with the converter when setting message types' do
|
153
|
+
converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(false)
|
154
|
+
resource = resource_class.new
|
155
|
+
assert_raises(ArgumentError) do
|
156
|
+
resource.nested_message = 5
|
157
|
+
end
|
158
|
+
end
|
136
159
|
|
137
|
-
|
138
|
-
|
139
|
-
|
160
|
+
it 'converts message types to and from their Ruby values when the converter allows' do
|
161
|
+
converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(true)
|
162
|
+
converter.expects(:to_message).once.with(6, nested_message_class).returns(nested_message_class.new number: 100)
|
163
|
+
converter.expects(:to_object).at_least_once.with(nested_message_class.new number: 100).returns 'intern'
|
140
164
|
|
141
|
-
|
142
|
-
|
143
|
-
it 'defines query methods for the scalar enums on its message' do
|
144
|
-
assert_respond_to resource, :number?
|
145
|
-
assert resource.number?(:ZERO)
|
146
|
-
refute resource.number?(:ONE)
|
147
|
-
end
|
165
|
+
resource = resource_class.new
|
166
|
+
resource.nested_message = 6
|
148
167
|
|
149
|
-
|
150
|
-
|
151
|
-
refute resource.boolean?
|
168
|
+
assert_equal nested_message_class.new(number: 100), resource.message.nested_message, 'object was not converted'
|
169
|
+
assert_equal 'intern', resource.nested_message, 'message was not converted'
|
152
170
|
end
|
153
171
|
|
154
|
-
|
155
|
-
|
156
|
-
|
172
|
+
describe '(query methods)' do
|
173
|
+
let(:resource) { resource_class.new }
|
174
|
+
it 'defines query methods for the scalar enums on its message' do
|
175
|
+
assert_respond_to resource, :number?
|
176
|
+
assert resource.number?(:ZERO)
|
177
|
+
refute resource.number?(:ONE)
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'defines query methods for the booleans on its message' do
|
181
|
+
assert_respond_to resource, :boolean?
|
182
|
+
refute resource.boolean?
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'defines query methods for the google.protobuf.BoolValues on its message' do
|
186
|
+
assert_respond_to resource, :google_bool_value?
|
187
|
+
refute resource.google_bool_value?
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'does not define query methods for repeated enums' do
|
191
|
+
refute_respond_to resource, :numbers?
|
192
|
+
assert_raises NoMethodError do
|
193
|
+
resource.numbers?(:ZERO)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'does not define query methods for non-enum fields' do
|
198
|
+
refute_respond_to resource, :inner?
|
199
|
+
assert_raises NoMethodError do
|
200
|
+
resource.inner?(:ZERO)
|
201
|
+
end
|
202
|
+
end
|
157
203
|
end
|
204
|
+
end
|
205
|
+
describe 'with empty nested resources' do
|
206
|
+
it 'does not throw an error' do
|
207
|
+
resource_class.class_exec(converter, resource_message_class) do |converter, message|
|
208
|
+
resource actions: [], message: message, nested_resources: {}
|
209
|
+
self.converter = converter
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
158
213
|
|
159
|
-
|
160
|
-
|
161
|
-
assert_raises
|
162
|
-
|
214
|
+
describe 'with invalid nested resource key' do
|
215
|
+
it 'throws an error' do
|
216
|
+
assert_raises RuntimeError do
|
217
|
+
resource_class.class_exec(converter, resource_message_class) do |converter, message|
|
218
|
+
resource actions: [],
|
219
|
+
message: message,
|
220
|
+
nested_resources: {'snoop' => Protip::Resource}
|
221
|
+
self.converter = converter
|
222
|
+
end
|
163
223
|
end
|
164
224
|
end
|
225
|
+
end
|
165
226
|
|
166
|
-
|
167
|
-
|
168
|
-
assert_raises
|
169
|
-
|
227
|
+
describe 'with invalid nested resource class' do
|
228
|
+
it 'throws an error' do
|
229
|
+
assert_raises RuntimeError do
|
230
|
+
resource_class.class_exec(converter, resource_message_class) do |converter, message|
|
231
|
+
resource actions: [], message: message, nested_resources: {dogg: Object}
|
232
|
+
self.converter = converter
|
233
|
+
end
|
170
234
|
end
|
171
235
|
end
|
172
236
|
end
|
237
|
+
|
173
238
|
end
|
174
239
|
|
175
240
|
# index/find/member/collection actions should all convert more complex Ruby objects to submessages in their
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
require 'google/protobuf'
|
4
|
-
require 'protip/converter'
|
5
4
|
require 'protip/wrapper'
|
5
|
+
require 'protip/resource'
|
6
6
|
|
7
7
|
module Protip::WrapperTest # namespace for internal constants
|
8
8
|
describe Protip::Wrapper do
|
@@ -44,6 +44,11 @@ module Protip::WrapperTest # namespace for internal constants
|
|
44
44
|
|
45
45
|
optional :google_bool_value, :message, 10, "google.protobuf.BoolValue"
|
46
46
|
repeated :google_bool_values, :message, 11, "google.protobuf.BoolValue"
|
47
|
+
|
48
|
+
oneof :oneof_group do
|
49
|
+
optional :oneof_string1, :string, 12
|
50
|
+
optional :oneof_string2, :string, 13
|
51
|
+
end
|
47
52
|
end
|
48
53
|
end
|
49
54
|
pool
|
@@ -55,6 +60,24 @@ module Protip::WrapperTest # namespace for internal constants
|
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
63
|
+
# Stubbed API client
|
64
|
+
let :client do
|
65
|
+
mock.responds_like_instance_of(Class.new { include Protip::Client })
|
66
|
+
end
|
67
|
+
|
68
|
+
# Call `resource_class` to get an empty resource type.
|
69
|
+
let :resource_class do
|
70
|
+
resource_class = Class.new do
|
71
|
+
include Protip::Resource
|
72
|
+
self.base_path = 'base_path'
|
73
|
+
class << self
|
74
|
+
attr_accessor :client
|
75
|
+
end
|
76
|
+
end
|
77
|
+
resource_class.client = client
|
78
|
+
resource_class
|
79
|
+
end
|
80
|
+
|
58
81
|
let(:wrapped_message) do
|
59
82
|
message_class.new(inner: inner_message_class.new(value: 25), string: 'test')
|
60
83
|
end
|
@@ -74,6 +97,9 @@ module Protip::WrapperTest # namespace for internal constants
|
|
74
97
|
assert_respond_to wrapper, :inner
|
75
98
|
assert_respond_to wrapper, :inner_blank
|
76
99
|
end
|
100
|
+
it 'adds accessors for oneof groups' do
|
101
|
+
assert_respond_to wrapper, :oneof_group
|
102
|
+
end
|
77
103
|
it 'adds queries for scalar matchable fields' do
|
78
104
|
assert_respond_to wrapper, :number?, 'enum field should respond to query'
|
79
105
|
assert_respond_to wrapper, :boolean?, 'bool field should respond to query'
|
@@ -313,7 +339,9 @@ module Protip::WrapperTest # namespace for internal constants
|
|
313
339
|
end
|
314
340
|
|
315
341
|
it 'contains keys for all fields of the parent message' do
|
316
|
-
|
342
|
+
keys = %i(string strings inner inners inner_blank number numbers boolean booleans
|
343
|
+
google_bool_value google_bool_values oneof_string1 oneof_string2)
|
344
|
+
assert_equal keys.sort, wrapper.to_h.keys.sort
|
317
345
|
end
|
318
346
|
it 'assigns nil for missing nested messages' do
|
319
347
|
hash = wrapper.to_h
|
@@ -336,6 +364,13 @@ module Protip::WrapperTest # namespace for internal constants
|
|
336
364
|
end
|
337
365
|
|
338
366
|
describe '#get' do
|
367
|
+
before do
|
368
|
+
resource_class.class_exec(converter, inner_message_class) do |converter, message|
|
369
|
+
resource actions: [], message: message
|
370
|
+
self.converter = converter
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
339
374
|
it 'does not convert simple fields' do
|
340
375
|
converter.expects(:convertible?).never
|
341
376
|
converter.expects(:to_object).never
|
@@ -354,6 +389,14 @@ module Protip::WrapperTest # namespace for internal constants
|
|
354
389
|
assert_equal Protip::Wrapper.new(inner_message_class.new(value: 25), converter), wrapper.inner
|
355
390
|
end
|
356
391
|
|
392
|
+
it 'wraps nested resource messages in their defined resource' do
|
393
|
+
message = wrapped_message
|
394
|
+
klass = resource_class
|
395
|
+
wrapper = Protip::Wrapper.new(message, Protip::StandardConverter.new, {inner: klass})
|
396
|
+
assert_equal klass, wrapper.inner.class
|
397
|
+
assert_equal message.inner, wrapper.inner.message
|
398
|
+
end
|
399
|
+
|
357
400
|
it 'returns nil for messages that have not been set' do
|
358
401
|
converter.expects(:convertible?).never
|
359
402
|
converter.expects(:to_object).never
|
@@ -362,6 +405,14 @@ module Protip::WrapperTest # namespace for internal constants
|
|
362
405
|
end
|
363
406
|
|
364
407
|
describe 'attribute writer' do # generated via method_missing?
|
408
|
+
|
409
|
+
before do
|
410
|
+
resource_class.class_exec(converter, inner_message_class) do |converter, message|
|
411
|
+
resource actions: [], message: message
|
412
|
+
self.converter = converter
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
365
416
|
it 'does not convert simple fields' do
|
366
417
|
converter.expects(:convertible?).never
|
367
418
|
converter.expects(:to_message).never
|
@@ -404,6 +455,23 @@ module Protip::WrapperTest # namespace for internal constants
|
|
404
455
|
assert_equal inner_message_class.new(value: 50), wrapper.message.inner
|
405
456
|
end
|
406
457
|
|
458
|
+
it 'for nested resources, sets the resource\'s message' do
|
459
|
+
message = wrapped_message
|
460
|
+
klass = resource_class
|
461
|
+
new_inner_message = inner_message_class.new(value: 50)
|
462
|
+
|
463
|
+
resource = klass.new new_inner_message
|
464
|
+
wrapper = Protip::Wrapper.new(message, Protip::StandardConverter.new, {inner: klass})
|
465
|
+
|
466
|
+
resource.expects(:message).once.returns(new_inner_message)
|
467
|
+
wrapper.inner = resource
|
468
|
+
|
469
|
+
assert_equal new_inner_message,
|
470
|
+
wrapper.message.inner,
|
471
|
+
'Wrapper did not set its message\'s inner message value to the value of the '\
|
472
|
+
'given resource\'s message'
|
473
|
+
end
|
474
|
+
|
407
475
|
it 'raises an error when setting an enum field to an undefined value' do
|
408
476
|
assert_raises RangeError do
|
409
477
|
wrapper.number = :NAN
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protip
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- AngelList
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -155,6 +155,11 @@ files:
|
|
155
155
|
- lib/protip/messages/errors.rb
|
156
156
|
- lib/protip/messages/types.rb
|
157
157
|
- lib/protip/resource.rb
|
158
|
+
- lib/protip/resource/creatable.rb
|
159
|
+
- lib/protip/resource/destroyable.rb
|
160
|
+
- lib/protip/resource/extra_methods.rb
|
161
|
+
- lib/protip/resource/search_methods.rb
|
162
|
+
- lib/protip/resource/updateable.rb
|
158
163
|
- lib/protip/standard_converter.rb
|
159
164
|
- lib/protip/wrapper.rb
|
160
165
|
- test/functional/protip/resource_test.rb
|
@@ -182,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
187
|
version: '0'
|
183
188
|
requirements: []
|
184
189
|
rubyforge_project:
|
185
|
-
rubygems_version: 2.
|
190
|
+
rubygems_version: 2.4.5.1
|
186
191
|
signing_key:
|
187
192
|
specification_version: 4
|
188
193
|
summary: Resources backed by protobuf messages
|