protip 0.10.7 → 0.11.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.
@@ -1,5 +1,4 @@
1
1
  require 'active_support/concern'
2
- require 'protobuf'
3
2
 
4
3
  module Protip
5
4
 
@@ -19,18 +18,18 @@ module Protip
19
18
  true
20
19
  else
21
20
  if name =~ /=$/
22
- message.class.fields.any?{|field| :"#{field.name}=" == name.to_sym}
21
+ message.class.descriptor.any?{|field| :"#{field.name}=" == name.to_sym}
23
22
  else
24
- message.class.fields.any?{|field| field.name == name.to_sym}
23
+ message.class.descriptor.any?{|field| field.name.to_sym == name.to_sym}
25
24
  end
26
25
  end
27
26
  end
28
27
 
29
28
  def method_missing(name, *args)
30
- if (name =~ /=$/ && field = message.class.fields.detect{|field| :"#{field.name}=" == name})
29
+ if (name =~ /=$/ && field = message.class.descriptor.detect{|field| :"#{field.name}=" == name})
31
30
  raise ArgumentError unless args.length == 1
32
31
  set field, args[0]
33
- elsif (field = message.class.fields.detect{|field| field.name == name})
32
+ elsif (field = message.class.descriptor.detect{|field| field.name.to_sym == name})
34
33
  raise ArgumentError unless args.length == 0
35
34
  get field
36
35
  else
@@ -62,14 +61,16 @@ module Protip
62
61
  # @return [Protip::Wrapper] The created field
63
62
  def build(field_name, attributes = {})
64
63
 
65
- field = message.class.fields.detect{|field| field.name == field_name.to_sym}
66
- if !field.is_a?(Protobuf::Field::MessageField)
67
- raise "Not a message field: #{field_name}"
68
- elsif converter.convertible?(field.type_class)
64
+ field = message.class.descriptor.detect{|field| field.name.to_sym == field_name.to_sym}
65
+ if !field
66
+ raise "No field named #{field_name}"
67
+ elsif field.type != :message
68
+ raise "Can only build message fields: #{field_name}"
69
+ elsif converter.convertible?(field.subtype.msgclass)
69
70
  raise "Cannot build a convertible field: #{field.name}"
70
71
  end
71
72
 
72
- message[field_name] = field.type_class.new
73
+ message[field_name.to_s] = field.subtype.msgclass.new
73
74
  wrapper = get(field)
74
75
  wrapper.assign_attributes attributes
75
76
  wrapper
@@ -85,12 +86,19 @@ module Protip
85
86
  # @return [NilClass]
86
87
  def assign_attributes(attributes)
87
88
  attributes.each do |field_name, value|
88
- field = message.class.fields.detect{|field| field.name == field_name.to_sym}
89
+ field = message.class.descriptor.detect{|field| field.name == field_name.to_s}
90
+ if !field
91
+ raise ArgumentError.new("Unrecognized field: #{field_name}")
92
+ end
89
93
 
90
- # For inconvertible nested messages, the value should be a hash - just pass it through to the nested message
91
- if field.is_a?(Protobuf::Field::MessageField) && !converter.convertible?(field.type_class)
92
- wrapper = get(field) || build(field.name) # Create the field if it doesn't already exist
93
- wrapper.assign_attributes value
94
+ # For inconvertible nested messages, the value should be either a hash or a message
95
+ if field.type == :message && !converter.convertible?(field.subtype.msgclass)
96
+ if value.is_a?(field.subtype.msgclass) # If a message, set it directly
97
+ set(field, value)
98
+ else # If a hash, pass it through to the nested message
99
+ wrapper = get(field) || build(field.name) # Create the field if it doesn't already exist
100
+ wrapper.assign_attributes value
101
+ end
94
102
  # Otherwise, if the field is a convertible message or a simple type, we set the value directly
95
103
  else
96
104
  set(field, value)
@@ -102,7 +110,7 @@ module Protip
102
110
 
103
111
  def as_json
104
112
  json = {}
105
- message.class.fields.each do |name|
113
+ message.class.descriptor.map(&:name).each do |name|
106
114
  value = public_send(name)
107
115
  json[name.to_s] = value.respond_to?(:as_json) ? value.as_json : value
108
116
  end
@@ -110,17 +118,20 @@ module Protip
110
118
  end
111
119
 
112
120
  def ==(wrapper)
113
- wrapper.is_a?(self.class) && message == wrapper.message && converter == wrapper.converter
121
+ wrapper.class == self.class &&
122
+ wrapper.message.class == message.class &&
123
+ message.class.encode(message) == wrapper.message.class.encode(wrapper.message) &&
124
+ converter == wrapper.converter
114
125
  end
115
126
 
116
127
  private
117
128
 
118
129
  def get(field)
119
- if field.is_a?(Protobuf::Field::MessageField)
120
- if message[field.name].nil?
130
+ if field.type == :message
131
+ if nil == message[field.name]
121
132
  nil
122
133
  else
123
- if converter.convertible?(field.type_class)
134
+ if converter.convertible?(field.subtype.msgclass)
124
135
  converter.to_object message[field.name]
125
136
  else
126
137
  self.class.new message[field.name], converter
@@ -132,16 +143,27 @@ module Protip
132
143
  end
133
144
 
134
145
  def set(field, value)
135
- if field.is_a?(Protobuf::Field::MessageField)
136
- if value.is_a? Protobuf::Message
137
- message[field.name] = value
138
- elsif converter.convertible?(field.type_class)
139
- message[field.name] = converter.to_message value, field.type_class
146
+ if field.label == :repeated
147
+ message[field.name].replace value.map{|v| to_protobuf_value field, v}
148
+ else
149
+ message[field.name] = to_protobuf_value(field, value)
150
+ end
151
+ end
152
+
153
+ # Helper for setting values - converts the value for the given field to one that we can set directly
154
+ def to_protobuf_value(field, value)
155
+ if field.type == :message
156
+ if nil == value
157
+ nil
158
+ elsif value.is_a?(field.subtype.msgclass)
159
+ value
160
+ elsif converter.convertible?(field.subtype.msgclass)
161
+ converter.to_message value, field.subtype.msgclass
140
162
  else
141
163
  raise ArgumentError.new "Cannot convert from Ruby object: \"#{field}\""
142
164
  end
143
165
  else
144
- message[field.name] = value
166
+ value
145
167
  end
146
168
  end
147
169
  end
@@ -2,196 +2,231 @@ require 'test_helper'
2
2
 
3
3
  require 'protip'
4
4
 
5
- module Protip::ResourceTestFunctional # Namespace for internal constants
6
- describe 'Protip::Resource (functional)' do
5
+ describe 'Protip::Resource (functional)' do
7
6
 
8
- before do
9
- WebMock.disable_net_connect!
10
- end
7
+ before do
8
+ WebMock.disable_net_connect!
9
+ end
11
10
 
12
- # Make sure none of these are structurally identical (e.g. give fields
13
- # different positions), to avoid potential errors where a message is
14
- # incorrectly encoded but still accidentally correctly decoded.
15
- class NestedMessage < ::Protobuf::Message
16
- optional :string, :inconvertible_value, 1
17
- end
18
- class ResourceMessage < ::Protobuf::Message
19
- optional :int64, :id, 2
20
- optional :string, :ordered_tests, 3
21
- optional NestedMessage, :nested_message, 4
22
- optional Protip::Int64Value, :nested_int, 5
23
- end
11
+ let :pool do
12
+ pool = Google::Protobuf::DescriptorPool.new
13
+ pool.build do
14
+ add_message 'google.protobuf.Int64Value' do # TODO: can we get this directly into the pool from the global pool?
15
+ optional :value, :int64, 1
16
+ end
24
17
 
25
- class ResourceQuery < ::Protobuf::Message
26
- optional :string, :param, 6
27
- end
18
+ # Make sure none of these are structurally identical (e.g. give fields
19
+ # different positions), to avoid potential errors where a message is
20
+ # incorrectly encoded but still accidentally correctly decoded.
28
21
 
29
- class NameResponse < ::Protobuf::Message
30
- optional :string, :name, 7
31
- end
22
+ add_message 'nested_message' do
23
+ optional :inconvertible_value, :string, 1
24
+ end
25
+ add_message 'resource_message' do
26
+ optional :id, :message, 2, 'google.protobuf.Int64Value'
27
+ optional :ordered_tests, :string, 3
28
+ optional :nested_message, :message, 4, 'nested_message'
29
+ optional :nested_int, :message, 5, 'google.protobuf.Int64Value'
30
+ end
32
31
 
33
- class SearchRequest < ::Protobuf::Message
34
- optional :string, :term, 8
35
- end
32
+ add_message 'resource_query' do
33
+ optional :param, :string, 6
34
+ end
36
35
 
37
- class SearchResponse < ::Protobuf::Message
38
- repeated :string, :results, 9
39
- end
36
+ add_message 'name_response' do
37
+ optional :name, :string, 7
38
+ end
39
+
40
+ add_message 'search_request' do
41
+ optional :term, :string, 8
42
+ end
40
43
 
41
- class FetchRequest < ::Protobuf::Message
42
- repeated :string, :names, 10
44
+ add_message 'search_response' do
45
+ repeated :results, :string, 9
46
+ end
47
+
48
+ add_message 'fetch_request' do
49
+ repeated :names, :string, 10
50
+ end
51
+ end
52
+ pool
53
+ end
54
+ %w(nested_message resource_message resource_query name_response search_request search_response fetch_request).each do |name|
55
+ let(:"#{name}_class") do
56
+ pool.lookup(name).msgclass
43
57
  end
58
+ end
59
+ let :int_message_class do
60
+ pool.lookup('google.protobuf.Int64Value').msgclass
61
+ end
44
62
 
45
- class Client
63
+ let :client_class do
64
+ Class.new do
46
65
  include Protip::Client
47
66
  def base_uri
48
67
  'https://external.service'
49
68
  end
50
69
  end
70
+ end
51
71
 
52
- class Resource
72
+ let :resource_class do
73
+ resource_class = Class.new do
53
74
  include Protip::Resource
75
+ end
76
+ resource_class.class_exec(resource_query_class, resource_message_class, name_response_class, search_request_class, search_response_class, fetch_request_class, client_class) do |resource_query_class, resource_message_class, name_response_class, search_request_class, search_response_class, fetch_request_class, client_class|
54
77
  resource actions: [:index, :show, :create, :update, :destroy],
55
- query: ResourceQuery, message: ResourceMessage
78
+ query: resource_query_class, message: resource_message_class
56
79
 
57
80
  member action: :archive, method: Net::HTTP::Put
58
- member action: :name, method: Net::HTTP::Get, response: NameResponse
81
+ member action: :name, method: Net::HTTP::Get, response: name_response_class
59
82
 
60
- collection action: :search, method: Net::HTTP::Get, request: SearchRequest, response: SearchResponse
61
- collection action: :fetch, method: Net::HTTP::Post, request: FetchRequest
83
+ collection action: :search, method: Net::HTTP::Get, request: search_request_class, response: search_response_class
84
+ collection action: :fetch, method: Net::HTTP::Post, request: fetch_request_class
62
85
 
63
86
  self.base_path = 'resources'
64
- self.client = Client.new
87
+ self.client = client_class.new
65
88
  end
89
+ resource_class
90
+ end
66
91
 
67
- describe '.all' do
68
- describe 'with a successful server response' do
69
- before do
70
- response = Protip::Messages::Array.new(messages: ['bilbo', 'baggins'].each_with_index.map do |name, index|
71
- ResourceMessage.new(id: index, ordered_tests: name, nested_int: {value: index + 42}).encode
72
- end)
73
- stub_request(:get, 'https://external.service/resources')
74
- .to_return body: response.encode
75
- end
92
+ describe '.all' do
93
+ describe 'with a successful server response' do
94
+ before do
95
+ response = Protip::Messages::Array.new(messages: ['bilbo', 'baggins'].each_with_index.map do |name, index|
96
+ message = resource_message_class.new(id: int_message_class.new(value: index), ordered_tests: name, nested_int: int_message_class.new(value: index + 42))
97
+ message.class.encode(message).encode('UTF-8')
98
+ end)
99
+ stub_request(:get, 'https://external.service/resources')
100
+ .to_return body: response.class.encode(response)
101
+ end
76
102
 
77
- it 'requests resources from the index endpoint' do
78
- results = Resource.all param: 'val'
103
+ it 'requests resources from the index endpoint' do
104
+ results = resource_class.all param: 'val'
79
105
 
80
- assert_requested :get, 'https://external.service/resources',
81
- times: 1, body: ResourceQuery.new(param: 'val').encode
106
+ assert_requested :get, 'https://external.service/resources',
107
+ times: 1, body: resource_query_class.encode(resource_query_class.new(param: 'val'))
82
108
 
83
- assert_equal 2, results.length, 'incorrect number of resources were returned'
84
- results.each { |result| assert_instance_of Resource, result, 'incorrect type was parsed'}
109
+ assert_equal 2, results.length, 'incorrect number of resources were returned'
110
+ results.each { |result| assert_instance_of resource_class, result, 'incorrect type was parsed'}
85
111
 
86
- assert_equal({ordered_tests: 'bilbo', id: 0, nested_message: nil, nested_int: 42},
87
- results[0].attributes)
88
- assert_equal({ordered_tests: 'baggins', id: 1, nested_message: nil, nested_int: 43},
89
- results[1].attributes)
90
- end
112
+ assert_equal({'ordered_tests' => 'bilbo', 'id' => 0, 'nested_message' => nil, 'nested_int' => 42},
113
+ results[0].attributes)
114
+ assert_equal({'ordered_tests' => 'baggins', 'id' => 1, 'nested_message' => nil, 'nested_int' => 43},
115
+ results[1].attributes)
116
+ end
91
117
 
92
- it 'allows requests without parameters' do
93
- results = Resource.all
94
- assert_requested :get, 'https://external.service/resources',
95
- times: 1, body: ResourceQuery.new.encode
96
- assert_equal 2, results.length, 'incorrect number of resources were returned'
97
- end
118
+ it 'allows requests without parameters' do
119
+ results = resource_class.all
120
+ assert_requested :get, 'https://external.service/resources',
121
+ times: 1, body: resource_query_class.encode(resource_query_class.new)
122
+ assert_equal 2, results.length, 'incorrect number of resources were returned'
98
123
  end
99
124
  end
125
+ end
100
126
 
101
- describe '.find' do
102
- describe 'with a successful server response' do
103
- before do
104
- response = ResourceMessage.new(id: 311, ordered_tests: 'i_suck_and_my_tests_are_order_dependent!').encode
105
- stub_request(:get, 'https://external.service/resources/311').to_return body: response.encode
106
- end
127
+ describe '.find' do
128
+ describe 'with a successful server response' do
129
+ before do
130
+ message = resource_message_class.new(id: int_message_class.new(value: 311), ordered_tests: 'i_suck_and_my_tests_are_order_dependent!')
131
+ response = message.class.encode(message)
132
+ stub_request(:get, 'https://external.service/resources/311').to_return body: response.encode
133
+ end
107
134
 
108
- it 'requests the resource from the show endpoint' do
109
- resource = Resource.find 311, param: 'val'
110
- assert_requested :get, 'https://external.service/resources/311', times: 1,
111
- body: ResourceQuery.new(param: 'val').encode
112
- assert_instance_of Resource, resource
113
- assert_equal 311, resource.id
114
- assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
115
- end
135
+ it 'requests the resource from the show endpoint' do
136
+ resource = resource_class.find 311, param: 'val'
137
+ assert_requested :get, 'https://external.service/resources/311', times: 1,
138
+ body: resource_query_class.encode(resource_query_class.new(param: 'val'))
139
+ assert_instance_of resource_class, resource
140
+ assert_equal 311, resource.id
141
+ assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
142
+ end
116
143
 
117
- it 'allows requests without parameters' do
118
- resource = Resource.find 311
119
- assert_requested :get, 'https://external.service/resources/311', times: 1,
120
- body: ResourceQuery.new.encode
121
- assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
122
- end
144
+ it 'allows requests without parameters' do
145
+ resource = resource_class.find 311
146
+ assert_requested :get, 'https://external.service/resources/311', times: 1,
147
+ body: resource_query_class.encode(resource_query_class.new)
148
+ assert_equal 'i_suck_and_my_tests_are_order_dependent!', resource.ordered_tests
123
149
  end
124
150
  end
151
+ end
125
152
 
126
- describe '#save' do
127
- let :resource_message do
128
- ResourceMessage.new(id: 666, ordered_tests: 'yes')
129
- end
130
- let :errors_message do
131
- Protip::Messages::Errors.new({
132
- messages: ['base1', 'base2'],
133
- field_errors: [
134
- {field: 'ordered_tests', message: 'are not OK'}
135
- ]
136
- })
137
- end
138
-
139
- # Create and update cases are similar - we just modify the ID attribute on
140
- # the initial resource, the HTTP method, and the expected URL.
141
- [
142
- [nil, :post, 'https://external.service/resources'],
143
- [666, :put, 'https://external.service/resources/666']
144
- ].each do |id, method, uri|
145
- describe "with a #{id ? 'persisted' : 'non-persisted'} resource" do
146
- before do
147
- @resource = Resource.new id: id, nested_int: 100
153
+ describe '#save' do
154
+ let :resource_message do
155
+ resource_message_class.new(id: int_message_class.new(value: 666), ordered_tests: 'yes')
156
+ end
157
+ let :errors_message do
158
+ Protip::Messages::Errors.new({
159
+ messages: ['base1', 'base2'],
160
+ field_errors: [
161
+ Protip::Messages::FieldError.new(field: 'ordered_tests', message: 'are not OK')
162
+ ]
163
+ })
164
+ end
165
+
166
+ # Create and update cases are similar - we just modify the ID attribute on
167
+ # the initial resource, the HTTP method, and the expected URL.
168
+ [
169
+ [nil, :post, 'https://external.service/resources'],
170
+ [666, :put, 'https://external.service/resources/666']
171
+ ].each do |id, method, uri|
172
+ describe "with a #{id ? 'persisted' : 'non-persisted'} resource" do
173
+ before do
174
+ attrs = {nested_int: 100}
175
+ if id
176
+ attrs[:id] = id
148
177
  end
178
+ @resource = resource_class.new attrs
179
+ end
149
180
 
150
- describe 'with a successful server response' do
151
- before do
152
- stub_request(method, uri).to_return body: resource_message.encode
153
- end
181
+ describe 'with a successful server response' do
182
+ before do
183
+ stub_request(method, uri).to_return body: resource_message.class.encode(resource_message)
184
+ end
154
185
 
155
- it 'returns true' do
156
- assert @resource.save, 'save was not successful'
157
- end
186
+ it 'returns true' do
187
+ assert @resource.save, 'save was not successful'
188
+ end
158
189
 
159
- it 'saves the resource and parses the server response' do
160
- @resource.ordered_tests = 'no'
161
- @resource.save
190
+ it 'saves the resource and parses the server response' do
191
+ @resource.ordered_tests = 'no'
192
+ @resource.save
162
193
 
163
- assert_requested method, uri,
164
- times: 1, body: ResourceMessage.new(id: id, ordered_tests: 'no', nested_int: {value: 100}).encode
165
- assert_equal 'yes', @resource.ordered_tests
194
+ attrs = {ordered_tests: 'no', nested_int: int_message_class.new(value: 100)}
195
+ if id
196
+ attrs[:id] = int_message_class.new(value: id)
166
197
  end
198
+ message = resource_message_class.new(attrs)
199
+ assert_requested method, uri,
200
+ times: 1, body: message.class.encode(message)
201
+ assert_equal 'yes', @resource.ordered_tests
167
202
  end
203
+ end
168
204
 
169
- describe 'with a 422 server response' do
170
- before do
171
- stub_request(method, uri)
172
- .to_return body: errors_message.encode, status: 422
173
- end
205
+ describe 'with a 422 server response' do
206
+ before do
207
+ stub_request(method, uri)
208
+ .to_return body: errors_message.class.encode(errors_message), status: 422
209
+ end
174
210
 
175
- it 'returns false' do
176
- refute @resource.save, 'save appeared successful'
177
- end
211
+ it 'returns false' do
212
+ refute @resource.save, 'save appeared successful'
213
+ end
178
214
 
179
- it 'adds errors based on the server response' do
180
- @resource.save
181
- assert_equal ['base1', 'base2'], @resource.errors['base']
182
- assert_equal ['are not OK'], @resource.errors['ordered_tests']
183
- end
215
+ it 'adds errors based on the server response' do
216
+ @resource.save
217
+ assert_equal ['base1', 'base2'], @resource.errors['base']
218
+ assert_equal ['are not OK'], @resource.errors['ordered_tests']
184
219
  end
185
220
  end
186
221
  end
187
222
  end
223
+ end
188
224
 
189
- describe '.member' do
190
- # TODO
191
- end
225
+ describe '.member' do
226
+ # TODO
227
+ end
192
228
 
193
- describe '.collection' do
194
- # TODO
195
- end
229
+ describe '.collection' do
230
+ # TODO
196
231
  end
197
232
  end