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 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