protip 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|