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.
- checksums.yaml +4 -4
- data/definitions/google/protobuf/wrappers.proto +99 -0
- data/definitions/protip/messages/array.proto +3 -2
- data/definitions/protip/messages/errors.proto +5 -4
- data/definitions/protip/messages/types.proto +6 -4
- data/lib/google/protobuf/wrappers.rb +48 -0
- data/lib/protip/client.rb +3 -4
- data/lib/protip/error.rb +2 -3
- data/lib/protip/messages/array.rb +16 -0
- data/lib/protip/messages/errors.rb +22 -0
- data/lib/protip/messages/types.rb +18 -0
- data/lib/protip/resource.rb +11 -9
- data/lib/protip/standard_converter.rb +12 -41
- data/lib/protip/wrapper.rb +48 -26
- data/test/functional/protip/resource_test.rb +173 -138
- data/test/unit/protip/resource_test.rb +107 -85
- data/test/unit/protip/standard_converter_test.rb +14 -11
- data/test/unit/protip/wrapper_test.rb +105 -37
- metadata +12 -12
- data/definitions/protip/messages/wrappers.proto +0 -60
- data/lib/protip/messages/array.pb.rb +0 -27
- data/lib/protip/messages/errors.pb.rb +0 -34
- data/lib/protip/messages/types.pb.rb +0 -26
- data/lib/protip/messages/wrappers.pb.rb +0 -64
data/lib/protip/wrapper.rb
CHANGED
@@ -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.
|
21
|
+
message.class.descriptor.any?{|field| :"#{field.name}=" == name.to_sym}
|
23
22
|
else
|
24
|
-
message.class.
|
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.
|
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.
|
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.
|
66
|
-
if !field
|
67
|
-
raise "
|
68
|
-
elsif
|
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.
|
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.
|
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
|
91
|
-
if field.
|
92
|
-
|
93
|
-
|
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.
|
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.
|
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.
|
120
|
-
if message[field.name]
|
130
|
+
if field.type == :message
|
131
|
+
if nil == message[field.name]
|
121
132
|
nil
|
122
133
|
else
|
123
|
-
if converter.convertible?(field.
|
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.
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
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
|
-
|
6
|
-
describe 'Protip::Resource (functional)' do
|
5
|
+
describe 'Protip::Resource (functional)' do
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
before do
|
8
|
+
WebMock.disable_net_connect!
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
32
|
+
add_message 'resource_query' do
|
33
|
+
optional :param, :string, 6
|
34
|
+
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
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:
|
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:
|
81
|
+
member action: :name, method: Net::HTTP::Get, response: name_response_class
|
59
82
|
|
60
|
-
collection action: :search, method: Net::HTTP::Get, request:
|
61
|
-
collection action: :fetch, method: Net::HTTP::Post, request:
|
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 =
|
87
|
+
self.client = client_class.new
|
65
88
|
end
|
89
|
+
resource_class
|
90
|
+
end
|
66
91
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
103
|
+
it 'requests resources from the index endpoint' do
|
104
|
+
results = resource_class.all param: 'val'
|
79
105
|
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
186
|
+
it 'returns true' do
|
187
|
+
assert @resource.save, 'save was not successful'
|
188
|
+
end
|
158
189
|
|
159
|
-
|
160
|
-
|
161
|
-
|
190
|
+
it 'saves the resource and parses the server response' do
|
191
|
+
@resource.ordered_tests = 'no'
|
192
|
+
@resource.save
|
162
193
|
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
211
|
+
it 'returns false' do
|
212
|
+
refute @resource.save, 'save appeared successful'
|
213
|
+
end
|
178
214
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
225
|
+
describe '.member' do
|
226
|
+
# TODO
|
227
|
+
end
|
192
228
|
|
193
|
-
|
194
|
-
|
195
|
-
end
|
229
|
+
describe '.collection' do
|
230
|
+
# TODO
|
196
231
|
end
|
197
232
|
end
|