protip 0.10.7 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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