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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23c197f44c8f1c1812859e1e7567fb8d0dde9ffb
4
- data.tar.gz: 45a759374549ebbe818e89fc958adaf47d199a8c
3
+ metadata.gz: 59a49ea006074b202ebfd0e2efff1c73aed317c9
4
+ data.tar.gz: fbd024d6595d62474d21154c00b9a91987ba3f6c
5
5
  SHA512:
6
- metadata.gz: 7e384ce1be77a5c0eb22052762da2ae105f3b0748025017ed4045db83a663e8ec3620b473a7ca2176b82e1d18685d075d5145bb8d99f17e7705f9d01c0a63f0c
7
- data.tar.gz: 1cd15cd584f2d42d8fd5f44205157a6e933dfebda42758ff8ea0c9e2892d2fdec147e397c007763474b9a3a73277a075a978a51e9ed7cef51a5810c4c73aec20
6
+ metadata.gz: 53ee5dabef500a1987f28573379483168fa1a2ebe1872fc2e5e4cce49be7d3ee867138bfe8076b4ab9ce28fd79c808b6680951fa602783973d8d149b153de9c0
7
+ data.tar.gz: 5ba932bebcb02f060babaaa8c4afe58e2e722a7f3816adddc61aeae93d632d0702f2e243f9ca0d98ea7b452fb49a0a1b5d088de67e98705bb2d00d58f7e936cc
@@ -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 - https://github.com/rails/rails/blob/097ca3f1f84bb9a2d3cda3f2cce7974a874efdf4/activemodel/lib/active_model/model.rb#L95
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 ? raise(RuntimeError.new 'Base path not yet set') : @base_path.gsub(/\/$/, '')
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
- raise RuntimeError.new('Only one call to `resource` is allowed')
167
- end
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
- @message.descriptor.each do |field|
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
- # needed for ActiveModel::Dirty
188
- define_attribute_methods @message.descriptor.map(&:name)
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
- # For index/show, we want a different number of method arguments
197
- # depending on whehter a query message was provided.
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, action, method, wrapper.message, response
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
@@ -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
- attr_reader :message, :converter
11
- def initialize(message, converter)
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
- if (name =~ /=$/ && field = message.class.descriptor.detect{|field| :"#{field.name}=" == name})
30
- raise ArgumentError unless args.length == 1
31
- attributes = {}.tap { |hash| hash[field.name] = args[0] }
32
- assign_attributes attributes
33
- args[0] # return the input value (to match ActiveRecord behavior)
34
- elsif (name =~ /\?$/ && field = message.class.descriptor.detect{|field| self.class.matchable?(field) && :"#{field.name}?" == name})
35
- if args.length == 1
36
- # this is an enum query, e.g. `state?(:CREATED)`
37
- matches? field, args[0]
38
- elsif args.length == 0
39
- # this is a boolean query, e.g. `approved?`
40
- get field
41
- else
42
- raise ArgumentError
43
- end
44
- elsif (field = message.class.descriptor.detect{|field| field.name.to_sym == name})
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
- before do
86
- resource_class.class_exec(converter, resource_message_class) do |converter, message|
87
- resource actions: [], message: message
88
- self.converter = converter
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
- it 'can only be invoked once' do
93
- assert_raises RuntimeError do
94
- resource_class.class_exec(resource_message_class) do |message|
95
- resource actions: [], message: message
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
- it 'defines accessors for the fields on its message' do
101
- resource = resource_class.new
102
- [:id, :string].each do |method|
103
- assert_respond_to resource, method
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
- it 'sets fields on the underlying message when simple setters are called' do
109
- resource = resource_class.new
110
- resource.string = 'intern'
111
- assert_equal 'intern', resource.message.string
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
- it 'never checks with the converter when setting simple types' do
116
- converter.expects(:convertible?).never
117
- resource = resource_class.new
118
- resource.string = 'intern'
119
- end
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
- it 'checks with the converter when setting message types' do
122
- converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(false)
123
- resource = resource_class.new
124
- assert_raises(ArgumentError) do
125
- resource.nested_message = 5
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
- it 'converts message types to and from their Ruby values when the converter allows' do
130
- converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(true)
131
- converter.expects(:to_message).once.with(6, nested_message_class).returns(nested_message_class.new number: 100)
132
- converter.expects(:to_object).at_least_once.with(nested_message_class.new number: 100).returns 'intern'
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
- resource = resource_class.new
135
- resource.nested_message = 6
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
- assert_equal nested_message_class.new(number: 100), resource.message.nested_message, 'object was not converted'
138
- assert_equal 'intern', resource.nested_message, 'message was not converted'
139
- end
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
- describe '(query methods)' do
142
- let(:resource) { resource_class.new }
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
- it 'defines query methods for the booleans on its message' do
150
- assert_respond_to resource, :boolean?
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
- it 'defines query methods for the google.protobuf.BoolValues on its message' do
155
- assert_respond_to resource, :google_bool_value?
156
- refute resource.google_bool_value?
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
- it 'does not define query methods for repeated enums' do
160
- refute_respond_to resource, :numbers?
161
- assert_raises NoMethodError do
162
- resource.numbers?(:ZERO)
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
- it 'does not define query methods for non-enum fields' do
167
- refute_respond_to resource, :inner?
168
- assert_raises NoMethodError do
169
- resource.inner?(:ZERO)
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
- assert_equal %i(string strings inner inners inner_blank number numbers boolean booleans google_bool_value google_bool_values).sort, wrapper.to_h.keys.sort
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.13.1
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-01 00:00:00.000000000 Z
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.2.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