protip 0.17.0 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/definitions/protip/messages/active_support/time_with_zone.proto +8 -0
- data/lib/protip/messages/active_support/time_with_zone.rb +19 -0
- data/lib/protip/resource.rb +36 -0
- data/lib/protip/resource/associations/association.rb +39 -0
- data/lib/protip/resource/associations/belongs_to_association.rb +64 -0
- data/lib/protip/resource/associations/belongs_to_polymorphic_association.rb +89 -0
- data/lib/protip/standard_converter.rb +25 -2
- data/test/functional/protip/resource_test.rb +56 -9
- data/test/test_helper.rb +1 -0
- data/test/unit/protip/resource/associations/association_test.rb +66 -0
- data/test/unit/protip/resource/associations/belongs_to_association_test.rb +131 -0
- data/test/unit/protip/resource/associations/belongs_to_polymorphic_association_test.rb +141 -0
- data/test/unit/protip/resource_test.rb +95 -0
- data/test/unit/protip/standard_converter_test.rb +33 -0
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c340cc60f0fd35abd3d10a61bb3e1f089a7a74f
|
4
|
+
data.tar.gz: b0638b042c3234a0915811aeb8b403c855cd8ce8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb76ebde93c39fca443494c1505360f426f5001927ada5be9da5976db7fac15bc066491d084c0ef6963f3b519c3141a9df4d3a683eacf7f911db2ff94f0236af
|
7
|
+
data.tar.gz: 06d98607aacfd60d8e9ddabb9117beecc78c7b0361bad1ccf329e786593efbbc54a6e2e23f0d7256a031ecfdf0fe270d2057f4f8a1aa5cf81fa93cd7d501aab2
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
2
|
+
# source: protip/messages/active_support/time_with_zone.proto
|
3
|
+
|
4
|
+
require 'google/protobuf'
|
5
|
+
|
6
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
7
|
+
add_message "protip.messages.ActiveSupport.TimeWithZone" do
|
8
|
+
optional :utc_timestamp, :int64, 1
|
9
|
+
optional :time_zone_name, :string, 2
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Protip
|
14
|
+
module Messages
|
15
|
+
module ActiveSupport
|
16
|
+
TimeWithZone = Google::Protobuf::DescriptorPool.generated_pool.lookup("protip.messages.ActiveSupport.TimeWithZone").msgclass
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/protip/resource.rb
CHANGED
@@ -29,6 +29,8 @@ require 'protip/resource/updateable'
|
|
29
29
|
require 'protip/resource/destroyable'
|
30
30
|
require 'protip/resource/extra_methods'
|
31
31
|
require 'protip/resource/search_methods'
|
32
|
+
require 'protip/resource/associations/belongs_to_association'
|
33
|
+
require 'protip/resource/associations/belongs_to_polymorphic_association'
|
32
34
|
|
33
35
|
module Protip
|
34
36
|
module Resource
|
@@ -204,6 +206,40 @@ module Protip
|
|
204
206
|
end
|
205
207
|
end
|
206
208
|
end
|
209
|
+
|
210
|
+
def belongs_to(association_name, options = {})
|
211
|
+
association = ::Protip::Resource::Associations::BelongsToAssociation.new(self, association_name, options)
|
212
|
+
association.define_accessors!
|
213
|
+
association
|
214
|
+
end
|
215
|
+
|
216
|
+
def belongs_to_polymorphic(association_name, options = {}, &block)
|
217
|
+
# We evaluate the block in the context of a wrapper that stores simple belongs-to associations
|
218
|
+
# as they're being created.
|
219
|
+
nested_association_creator = Class.new do
|
220
|
+
attr_reader :associations
|
221
|
+
def initialize(resource_class)
|
222
|
+
@resource_class = resource_class
|
223
|
+
@associations = []
|
224
|
+
end
|
225
|
+
def belongs_to(*args)
|
226
|
+
# Just forward the belongs_to call and store the result so we can pass it to the polymorphic association
|
227
|
+
@associations << @resource_class.send(:belongs_to, *args)
|
228
|
+
end
|
229
|
+
end.new(self)
|
230
|
+
|
231
|
+
nested_association_creator.instance_eval(&block)
|
232
|
+
|
233
|
+
association = ::Protip::Resource::Associations::BelongsToPolymorphicAssociation.new self,
|
234
|
+
association_name, nested_association_creator.associations, options
|
235
|
+
association.define_accessors!
|
236
|
+
association
|
237
|
+
end
|
238
|
+
|
239
|
+
def references_through_one_of(id_field, options = {})
|
240
|
+
::Protip::Resource::Associations::ReferencesThroughOneOfAssociation.new(self, id_field, options)
|
241
|
+
.define_accessors!
|
242
|
+
end
|
207
243
|
end
|
208
244
|
|
209
245
|
def initialize(message_or_attributes = {})
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Base module for a reference that can be defined on a resource. References
|
2
|
+
# are similar to +belongs_to+ associations in ActiveRecord.
|
3
|
+
module Protip
|
4
|
+
module Resource
|
5
|
+
module Associations
|
6
|
+
module Association
|
7
|
+
|
8
|
+
def define_accessors!
|
9
|
+
resource_class.class_exec(self, association_name) do |association, association_name|
|
10
|
+
define_method(association_name) do
|
11
|
+
association.read(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
define_method(:"#{association_name}=") do |value|
|
15
|
+
association.write(self, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Individual reference classes must implement
|
21
|
+
def resource_class
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def association_name
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(resource)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(resource, value)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections' # For classifying/constantizing strings
|
2
|
+
|
3
|
+
require 'protip/resource/associations/association'
|
4
|
+
|
5
|
+
module Protip
|
6
|
+
module Resource
|
7
|
+
module Associations
|
8
|
+
class BelongsToAssociation
|
9
|
+
|
10
|
+
include Protip::Resource::Associations::Association
|
11
|
+
|
12
|
+
attr_reader :resource_class, :association_name, :id_field
|
13
|
+
|
14
|
+
def initialize(resource_class, association_name, id_field: nil, class_name: nil)
|
15
|
+
# The resource type that houses the association
|
16
|
+
@resource_class = resource_class
|
17
|
+
|
18
|
+
# The name for generating accessor methods
|
19
|
+
@association_name = association_name
|
20
|
+
|
21
|
+
# The field that holds the ID for the association
|
22
|
+
@id_field = (id_field || self.class.default_id_field(association_name)).to_sym
|
23
|
+
|
24
|
+
@class_name = (class_name || self.class.default_class_name(association_name)).to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def associated_resource_class
|
28
|
+
@associated_resource_class ||= @class_name.constantize
|
29
|
+
end
|
30
|
+
|
31
|
+
def read(resource)
|
32
|
+
id = resource.public_send(@id_field)
|
33
|
+
if id == nil
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
associated_resource_class.find id
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def write(resource, value)
|
41
|
+
if value != nil
|
42
|
+
unless value.is_a?(associated_resource_class)
|
43
|
+
raise ArgumentError.new("Cannot assign #{value.class} to #{resource_class}##{@id_field}")
|
44
|
+
end
|
45
|
+
unless value.persisted?
|
46
|
+
raise "Cannot assign non-persisted resource to association #{resource_class}##{association_name}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
resource.public_send(:"#{@id_field}=", value.try(:id))
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
def default_id_field(association_name)
|
54
|
+
"#{association_name}_id".to_sym
|
55
|
+
end
|
56
|
+
def default_class_name(association_name)
|
57
|
+
association_name.to_s.classify
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'protip/resource/associations/association'
|
2
|
+
require 'protip/resource/associations/belongs_to_association'
|
3
|
+
|
4
|
+
module Protip
|
5
|
+
module Resource
|
6
|
+
module Associations
|
7
|
+
class BelongsToPolymorphicAssociation
|
8
|
+
|
9
|
+
include Protip::Resource::Associations::Association
|
10
|
+
attr_reader :resource_class, :association_name, :id_field
|
11
|
+
|
12
|
+
# Define a polymorphic association based on a one-of field. The options for the oneof must all be IDs with
|
13
|
+
# an associated +Protip::Resource::Associations::BelongsToAssociation+ that's already been created.
|
14
|
+
#
|
15
|
+
# @param [Class] resource_class The +Protip::Resource+ class that holds a reference to an associated object.
|
16
|
+
# @param [String] association_name The name to use when defining accessors for the associated object.
|
17
|
+
# @param [Array<Protip::Resource::Associations::BelongsToAssociation>] nested_associations The individual
|
18
|
+
# associations corresponding to the fields within the `oneof`.
|
19
|
+
# @param [Symbol|String] id_field The name of the `oneof` field that holds the association ID. Defaults to
|
20
|
+
# `#{association_name}_id`.
|
21
|
+
def initialize(resource_class, association_name, nested_associations, id_field: nil)
|
22
|
+
# The class where accessors will be defined
|
23
|
+
@resource_class = resource_class
|
24
|
+
|
25
|
+
# The name of the accessor methods
|
26
|
+
@association_name = association_name.to_sym
|
27
|
+
|
28
|
+
# The oneof field that holds the ID of the foreign resource
|
29
|
+
@id_field = (id_field ||
|
30
|
+
Protip::Resource::Associations::BelongsToAssociation.default_id_field(association_name)).to_sym
|
31
|
+
@oneof = @resource_class.message.descriptor.lookup_oneof(@id_field.to_s)
|
32
|
+
raise "Invalid field name for polymorphic association: #{@id_field}" unless @oneof
|
33
|
+
|
34
|
+
# Internally, keep the nested associations indexed by ID field
|
35
|
+
@_nested_associations = {}
|
36
|
+
nested_associations.each do |association|
|
37
|
+
if @_nested_associations.has_key? association.id_field.to_sym
|
38
|
+
raise ArgumentError.new("Duplicate association for #{id_field}")
|
39
|
+
end
|
40
|
+
@_nested_associations[association.id_field.to_sym] = association
|
41
|
+
end
|
42
|
+
field_names = @oneof.map{|desc| desc.name.to_sym}
|
43
|
+
unless (field_names.length == @_nested_associations.length &&
|
44
|
+
@_nested_associations.keys.all?{|id_field| field_names.include? id_field})
|
45
|
+
raise ArgumentError.new(
|
46
|
+
'Polymorphic association requires an association to be defined for all nested fields'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def read(resource)
|
52
|
+
field = resource.message.public_send(id_field)
|
53
|
+
if field
|
54
|
+
@_nested_associations[field].read(resource)
|
55
|
+
else
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def write(resource, value)
|
61
|
+
if value == nil
|
62
|
+
@oneof.each do |field_descriptor|
|
63
|
+
resource.public_send(:"#{field_descriptor.name}=", nil)
|
64
|
+
end
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
# Find the nested reference matching this association type
|
68
|
+
matching_references = @_nested_associations.select do |id_field, reference|
|
69
|
+
value.is_a? reference.associated_resource_class
|
70
|
+
end
|
71
|
+
|
72
|
+
# Make sure we found exactly one
|
73
|
+
if matching_references.empty?
|
74
|
+
raise ArgumentError.new("Could not find matching reference for value of type #{value.class}")
|
75
|
+
end
|
76
|
+
if matching_references.length > 1
|
77
|
+
raise ArgumentError.new(
|
78
|
+
"Value of type #{value.class} matched with #{matching_references.keys.map(&:to_s).join(', ')}"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# And forward the write operation
|
83
|
+
matching_references.values.first.write(resource, value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'money'
|
2
2
|
|
3
|
+
require 'active_support/time_with_zone'
|
4
|
+
|
3
5
|
require 'protip/converter'
|
4
6
|
|
5
7
|
require 'protip/messages/currency'
|
@@ -67,8 +69,29 @@ module Protip
|
|
67
69
|
conversions['google.protobuf.BoolValue'] = {
|
68
70
|
to_object: ->(message) { message.value },
|
69
71
|
to_message: lambda do |value, message_class|
|
70
|
-
|
71
|
-
|
72
|
+
message_class.new value: value_to_boolean(value)
|
73
|
+
end
|
74
|
+
}
|
75
|
+
|
76
|
+
## ActiveSupport objects
|
77
|
+
conversions['protip.messages.ActiveSupport.TimeWithZone'] = {
|
78
|
+
to_object: ->(message) {
|
79
|
+
ActiveSupport::TimeWithZone.new(
|
80
|
+
Time.at(message.utc_timestamp).utc,
|
81
|
+
ActiveSupport::TimeZone.new(message.time_zone_name)
|
82
|
+
)
|
83
|
+
},
|
84
|
+
to_message: ->(value, message_class) {
|
85
|
+
if !value.is_a?(::ActiveSupport::TimeWithZone) && (value.is_a?(Time) || value.is_a?(DateTime))
|
86
|
+
value = ::ActiveSupport::TimeWithZone.new(value.to_time.utc, ::ActiveSupport::TimeZone.new('UTC'))
|
87
|
+
end
|
88
|
+
raise ArgumentError unless value.is_a?(::ActiveSupport::TimeWithZone)
|
89
|
+
|
90
|
+
message_class.new(
|
91
|
+
utc_timestamp: value.to_i,
|
92
|
+
time_zone_name: value.time_zone.name,
|
93
|
+
)
|
94
|
+
}
|
72
95
|
}
|
73
96
|
|
74
97
|
def convertible?(message_class)
|
@@ -28,26 +28,27 @@ describe 'Protip::Resource (functional)' do
|
|
28
28
|
optional :ordered_tests, :string, 3
|
29
29
|
optional :nested_message, :message, 4, 'nested_message'
|
30
30
|
optional :nested_int, :message, 5, 'google.protobuf.Int64Value'
|
31
|
+
optional :association_id, :message, 6, 'google.protobuf.Int64Value'
|
31
32
|
end
|
32
33
|
|
33
34
|
add_message 'resource_query' do
|
34
|
-
optional :param, :string,
|
35
|
+
optional :param, :string, 7
|
35
36
|
end
|
36
37
|
|
37
38
|
add_message 'name_response' do
|
38
|
-
optional :name, :string,
|
39
|
+
optional :name, :string, 8
|
39
40
|
end
|
40
41
|
|
41
42
|
add_message 'search_request' do
|
42
|
-
optional :term, :string,
|
43
|
+
optional :term, :string, 9
|
43
44
|
end
|
44
45
|
|
45
46
|
add_message 'search_response' do
|
46
|
-
repeated :results, :string,
|
47
|
+
repeated :results, :string, 10
|
47
48
|
end
|
48
49
|
|
49
50
|
add_message 'fetch_request' do
|
50
|
-
repeated :names, :string,
|
51
|
+
repeated :names, :string, 11
|
51
52
|
end
|
52
53
|
end
|
53
54
|
pool
|
@@ -112,10 +113,19 @@ describe 'Protip::Resource (functional)' do
|
|
112
113
|
assert_equal 2, results.length, 'incorrect number of resources were returned'
|
113
114
|
results.each { |result| assert_instance_of resource_class, result, 'incorrect type was parsed'}
|
114
115
|
|
115
|
-
assert_equal(
|
116
|
-
|
117
|
-
|
118
|
-
|
116
|
+
assert_equal(
|
117
|
+
{
|
118
|
+
'ordered_tests' => 'bilbo', 'id' => 0, 'nested_message' => nil, 'nested_int' => 42, 'association_id' => nil
|
119
|
+
},
|
120
|
+
results[0].attributes
|
121
|
+
)
|
122
|
+
assert_equal(
|
123
|
+
{
|
124
|
+
'ordered_tests' => 'baggins', 'id' => 1, 'nested_message' => nil, 'nested_int' => 43,
|
125
|
+
'association_id' => nil
|
126
|
+
},
|
127
|
+
results[1].attributes
|
128
|
+
)
|
119
129
|
end
|
120
130
|
|
121
131
|
it 'allows requests without parameters' do
|
@@ -232,4 +242,41 @@ describe 'Protip::Resource (functional)' do
|
|
232
242
|
describe '.collection' do
|
233
243
|
# TODO
|
234
244
|
end
|
245
|
+
|
246
|
+
describe '.belongs_to' do
|
247
|
+
before do
|
248
|
+
resource_class.class_eval do
|
249
|
+
belongs_to :association, class_name: 'ResourceClass', id_field: :association_id
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'returns nil when no association ID has been set' do
|
254
|
+
Object.stub_const(:ResourceClass, resource_class) do
|
255
|
+
resource_class.expects(:find).never
|
256
|
+
assert_nil resource_class.new.association
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'fetches the associated resource' do
|
261
|
+
Object.stub_const(:ResourceClass, resource_class) do
|
262
|
+
associated = mock
|
263
|
+
resource_class.expects(:find).once.with(123).returns(associated)
|
264
|
+
assert_equal associated, resource_class.new(association_id: 123).association
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'allows writing the associated resource' do
|
269
|
+
Object.stub_const(:ResourceClass, resource_class) do
|
270
|
+
associated = resource_class.new
|
271
|
+
associated.id = 5
|
272
|
+
base = resource_class.new
|
273
|
+
base.association = associated
|
274
|
+
assert_equal 5, base.association_id
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe '.belongs_to_polymorphic' do
|
280
|
+
# TODO
|
281
|
+
end
|
235
282
|
end
|
data/test/test_helper.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'google/protobuf'
|
4
|
+
require 'protip/resource/associations/association'
|
5
|
+
|
6
|
+
describe Protip::Resource::Associations::Association do
|
7
|
+
let :resource_class do
|
8
|
+
pool = Google::Protobuf::DescriptorPool.new
|
9
|
+
pool.build do
|
10
|
+
add_message 'ResourceMessage' do
|
11
|
+
optional :id, :string, 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
Class.new do
|
15
|
+
include Protip::Resource
|
16
|
+
resource actions: [], message: pool.lookup('ResourceMessage').msgclass
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let :association_class do
|
21
|
+
Class.new do
|
22
|
+
include Protip::Resource::Associations::Association
|
23
|
+
attr_reader :resource_class, :association_name
|
24
|
+
def initialize(resource_class, association_name)
|
25
|
+
@resource_class, @association_name = resource_class, association_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#define_accessors!' do
|
31
|
+
let :association do
|
32
|
+
association_class.new(resource_class, :reference)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'defines read and write methods' do
|
36
|
+
# Sanity checks
|
37
|
+
refute_includes resource_class.instance_methods, :reference, 'reader already set'
|
38
|
+
refute_includes resource_class.instance_methods, :reference=, 'writer already set'
|
39
|
+
|
40
|
+
association.define_accessors!
|
41
|
+
|
42
|
+
assert_includes resource_class.instance_methods, :reference, 'reader not set'
|
43
|
+
assert_includes resource_class.instance_methods, :reference=, 'writer not set'
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '(after invoked)' do
|
47
|
+
let :resource do
|
48
|
+
resource_class.new
|
49
|
+
end
|
50
|
+
|
51
|
+
before do
|
52
|
+
association.define_accessors!
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'receives reader calls from resource instances' do
|
56
|
+
association.expects(:read).once.with(resource)
|
57
|
+
resource.reference
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'receives writer calls from resource instances' do
|
61
|
+
association.expects(:write).once.with(resource, 'test')
|
62
|
+
resource.reference = 'test'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'protip/resource/associations/belongs_to_association'
|
4
|
+
|
5
|
+
describe Protip::Resource::Associations::BelongsToAssociation do
|
6
|
+
let :pool do
|
7
|
+
pool = Google::Protobuf::DescriptorPool.new
|
8
|
+
pool.build do
|
9
|
+
# Allow nil ID fields
|
10
|
+
add_message 'google.protobuf.StringValue' do
|
11
|
+
optional :value, :string, 1
|
12
|
+
end
|
13
|
+
add_message 'ResourceMessage' do
|
14
|
+
optional :id, :message, 1, 'google.protobuf.StringValue'
|
15
|
+
optional :referenced_resource_id, :message, 2, 'google.protobuf.StringValue'
|
16
|
+
end
|
17
|
+
add_message 'ReferencedResourceMessage' do
|
18
|
+
optional :id, :message, 1, 'google.protobuf.StringValue'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
pool
|
22
|
+
end
|
23
|
+
let :resource_class do
|
24
|
+
klass = Class.new do
|
25
|
+
include Protip::Resource
|
26
|
+
end
|
27
|
+
klass.class_exec(pool) do |pool|
|
28
|
+
resource actions: [], message: pool.lookup('ResourceMessage').msgclass
|
29
|
+
end
|
30
|
+
klass
|
31
|
+
end
|
32
|
+
|
33
|
+
let :referenced_resource_class do
|
34
|
+
klass = Class.new do
|
35
|
+
include Protip::Resource
|
36
|
+
end
|
37
|
+
klass.class_exec(pool) do |pool|
|
38
|
+
resource actions: [], message: pool.lookup('ReferencedResourceMessage').msgclass
|
39
|
+
end
|
40
|
+
klass
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#initialize' do
|
44
|
+
describe '(class_name option)' do
|
45
|
+
# These rely on private behavior - that `associated_resource_class` gives the association class after init
|
46
|
+
it 'chooses a default class based on the association name' do
|
47
|
+
Object.stub_const 'ReferencedResource', referenced_resource_class do
|
48
|
+
reference = Protip::Resource::Associations::BelongsToAssociation.new resource_class,
|
49
|
+
:referenced_resource
|
50
|
+
assert_equal referenced_resource_class, reference.associated_resource_class
|
51
|
+
end
|
52
|
+
end
|
53
|
+
it 'allows a class name to be set' do
|
54
|
+
Object.stub_const 'Foo', referenced_resource_class do
|
55
|
+
reference = Protip::Resource::Associations::BelongsToAssociation.new resource_class,
|
56
|
+
:referenced_resource, class_name: 'Foo'
|
57
|
+
assert_equal referenced_resource_class, reference.associated_resource_class
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
describe '(id_field option)' do
|
62
|
+
it 'chooses a default ID field based on the association name' do
|
63
|
+
Object.stub_const 'ReferencedResource', referenced_resource_class do
|
64
|
+
reference = Protip::Resource::Associations::BelongsToAssociation.new resource_class,
|
65
|
+
:referenced_resource
|
66
|
+
assert_equal :referenced_resource_id, reference.id_field
|
67
|
+
end
|
68
|
+
end
|
69
|
+
it 'allows an association name to be set' do
|
70
|
+
Object.stub_const 'ReferencedResource', referenced_resource_class do
|
71
|
+
reference = Protip::Resource::Associations::BelongsToAssociation.new resource_class,
|
72
|
+
:referenced_resource, id_field: :foo_bar
|
73
|
+
assert_equal :foo_bar, reference.id_field
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '(accessors)' do
|
80
|
+
let :reference do
|
81
|
+
reference = Protip::Resource::Associations::BelongsToAssociation.new resource_class, :referenced_resource
|
82
|
+
reference.stubs(:associated_resource_class).returns(referenced_resource_class) # internal behavior
|
83
|
+
reference
|
84
|
+
end
|
85
|
+
|
86
|
+
let :resource do
|
87
|
+
resource_class.new
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#read' do
|
91
|
+
it 'finds the association by ID' do
|
92
|
+
referenced_resource = mock
|
93
|
+
referenced_resource_class.expects(:find).once.with('asdf').returns(referenced_resource)
|
94
|
+
resource.referenced_resource_id = 'asdf'
|
95
|
+
assert_equal referenced_resource, reference.read(resource)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns nil if the resource has not been set' do
|
99
|
+
resource.referenced_resource_id = nil
|
100
|
+
referenced_resource_class.expects(:find).never
|
101
|
+
assert_nil reference.read(resource)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#write' do
|
106
|
+
it 'raises an error if given the wrong resource type' do
|
107
|
+
assert_raises ArgumentError do
|
108
|
+
reference.write(resource, resource)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'raises an error if given a non-persisted resource' do
|
113
|
+
assert_raises RuntimeError do
|
114
|
+
reference.write(resource, referenced_resource_class.new)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'assigns nil if nil is given' do
|
119
|
+
resource.referenced_resource_id = 'asdf'
|
120
|
+
reference.write(resource, nil)
|
121
|
+
assert_nil resource.referenced_resource_id
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'assigns the resource ID if a persisted resource of the correct type is given' do
|
125
|
+
reference.write(resource, referenced_resource_class.new(id: 'foo'))
|
126
|
+
assert_equal 'foo', resource.referenced_resource_id
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'protip/resource/associations/belongs_to_polymorphic_association'
|
4
|
+
|
5
|
+
describe Protip::Resource::Associations::BelongsToPolymorphicAssociation do
|
6
|
+
|
7
|
+
let :resource_class do
|
8
|
+
pool = Google::Protobuf::DescriptorPool.new
|
9
|
+
pool.build do
|
10
|
+
add_message 'ResourceMessage' do
|
11
|
+
optional :id, :string, 1
|
12
|
+
oneof :reference_id do
|
13
|
+
optional :rick_ross_id, :string, 2
|
14
|
+
optional :fetty_wap_id, :string, 3
|
15
|
+
end
|
16
|
+
optional :other_id, :string, 4
|
17
|
+
end
|
18
|
+
end
|
19
|
+
Class.new do
|
20
|
+
include Protip::Resource
|
21
|
+
resource actions: [], message: pool.lookup('ResourceMessage').msgclass
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
let :rick_ross_association do
|
26
|
+
association = mock.responds_like_instance_of(Protip::Resource::Associations::BelongsToAssociation)
|
27
|
+
association.stubs(:id_field).returns(:rick_ross_id)
|
28
|
+
association
|
29
|
+
end
|
30
|
+
|
31
|
+
let :fetty_wap_association do
|
32
|
+
association = mock.responds_like_instance_of(Protip::Resource::Associations::BelongsToAssociation)
|
33
|
+
association.stubs(:id_field).returns(:fetty_wap_id)
|
34
|
+
association
|
35
|
+
end
|
36
|
+
|
37
|
+
let :other_association do
|
38
|
+
association = mock.responds_like_instance_of(Protip::Resource::Associations::BelongsToAssociation)
|
39
|
+
association.stubs(:id_field).returns(:other_id)
|
40
|
+
association
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#initialize' do
|
44
|
+
it 'raises an error unless a belongs-to association is provided for all nested fields' do
|
45
|
+
error = assert_raises ArgumentError do
|
46
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation.new resource_class,
|
47
|
+
:reference, [rick_ross_association]
|
48
|
+
end
|
49
|
+
assert_match /requires an association to be defined/, error.message
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'raises an error if a belongs-to association is provided for a field outside the oneof' do
|
53
|
+
error = assert_raises ArgumentError do
|
54
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation.new resource_class,
|
55
|
+
:reference, [rick_ross_association, other_association]
|
56
|
+
end
|
57
|
+
assert_match /requires an association to be defined/, error.message
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'raises an error if a duplicate belongs-to association is provided' do
|
61
|
+
error = assert_raises ArgumentError do
|
62
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation.new resource_class,
|
63
|
+
:reference, [rick_ross_association, rick_ross_association, fetty_wap_association]
|
64
|
+
end
|
65
|
+
assert_match /Duplicate association/, error.message
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'allows the oneof ID field to be specified' do
|
69
|
+
association = Protip::Resource::Associations::BelongsToPolymorphicAssociation.new resource_class,
|
70
|
+
:foo, [rick_ross_association, fetty_wap_association], id_field: :reference_id
|
71
|
+
assert_equal :reference_id, association.id_field
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '(accessors)' do
|
76
|
+
let(:association) do
|
77
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation.new resource_class, :reference,
|
78
|
+
[rick_ross_association, fetty_wap_association]
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#read' do
|
82
|
+
it 'forwards to the nested association that\'s currently been set' do
|
83
|
+
# Create a test instance with one of the fields set
|
84
|
+
resource = resource_class.new fetty_wap_id: 'test'
|
85
|
+
|
86
|
+
rick_ross_association.expects(:read).never
|
87
|
+
fetty_wap_association.expects(:read).once.with(resource).returns('come my way')
|
88
|
+
|
89
|
+
assert_equal 'come my way', association.read(resource)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'returns nil if no nested association has been set' do
|
93
|
+
resource = resource_class.new
|
94
|
+
|
95
|
+
rick_ross_association.expects(:read).never
|
96
|
+
fetty_wap_association.expects(:read).never
|
97
|
+
|
98
|
+
assert_nil association.read(resource)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#write' do
|
103
|
+
let(:rick_ross_class) { Class.new }
|
104
|
+
let(:fetty_wap_class) { Class.new }
|
105
|
+
before do
|
106
|
+
rick_ross_association.stubs(:associated_resource_class).returns(rick_ross_class)
|
107
|
+
fetty_wap_association.stubs(:associated_resource_class).returns(fetty_wap_class)
|
108
|
+
end
|
109
|
+
it 'forwards to the nested association that matches the class being set' do
|
110
|
+
resource = resource_class.new
|
111
|
+
fetty_wap = fetty_wap_class.new
|
112
|
+
|
113
|
+
rick_ross_association.expects(:write).never
|
114
|
+
fetty_wap_association.expects(:write).once.with(resource, fetty_wap)
|
115
|
+
|
116
|
+
association.write(resource, fetty_wap)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Sanity check, try out forwarding for the other type as well
|
120
|
+
it 'forwards to the nested association that matches the class being set, if that association comes first' do
|
121
|
+
resource = resource_class.new
|
122
|
+
rick_ross = rick_ross_class.new
|
123
|
+
|
124
|
+
rick_ross_association.expects(:write).once.with(resource, rick_ross)
|
125
|
+
fetty_wap_association.expects(:write).never
|
126
|
+
|
127
|
+
association.write(resource, rick_ross)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'wipes all associations if nil is given' do
|
131
|
+
resource = resource_class.new
|
132
|
+
resource.rick_ross_id = 'asfd'
|
133
|
+
|
134
|
+
association.write(resource, nil)
|
135
|
+
|
136
|
+
assert_nil resource.rick_ross_id
|
137
|
+
assert_nil resource.fetty_wap_id
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -865,6 +865,101 @@ module Protip::ResourceTest # Namespace for internal constants
|
|
865
865
|
describe_non_resourceful_action 'collection', 'base_path/action'
|
866
866
|
end
|
867
867
|
|
868
|
+
# Common tests for both types of belongs_to association. Assumes a `let(:association)` statement
|
869
|
+
# has been provided, to give a mock of the appropriate association with `define_accessors!` stubbed out
|
870
|
+
# If a block is needed to run the method, it can be provided
|
871
|
+
def self.describe_association_method!(method, association_class, &block)
|
872
|
+
describe '(common behvaior)' do
|
873
|
+
it 'defines accessors' do
|
874
|
+
association_class.expects(:new).once.returns(association)
|
875
|
+
association.expects(:define_accessors!).once
|
876
|
+
resource_class.class_exec(method) { |method| send method, :association_name, &block }
|
877
|
+
end
|
878
|
+
|
879
|
+
it 'returns the created association' do
|
880
|
+
association_class.expects(:new).once.returns(association)
|
881
|
+
resource_class.class_exec(method) { |method |@result = send method, :association_name, &block }
|
882
|
+
assert_equal association, resource_class.instance_variable_get(:'@result'), 'association was not returned'
|
883
|
+
end
|
884
|
+
|
885
|
+
it 'raises an error on invalid options' do
|
886
|
+
error = assert_raises ArgumentError do
|
887
|
+
resource_class.class_exec(method) do |method|
|
888
|
+
send method, :association_name, bad_option: 'bad', &block
|
889
|
+
end
|
890
|
+
end
|
891
|
+
assert_match /bad_option/, error.message
|
892
|
+
end
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
describe '.belongs_to' do
|
897
|
+
let :association do
|
898
|
+
association = mock.responds_like_instance_of Protip::Resource::Associations::BelongsToAssociation
|
899
|
+
association.stubs(:define_accessors!)
|
900
|
+
association
|
901
|
+
end
|
902
|
+
|
903
|
+
it 'creates a belongs_to association and passes in options' do
|
904
|
+
Protip::Resource::Associations::BelongsToAssociation.expects(:new).once
|
905
|
+
.with(resource_class, :association_name, class_name: 'Foo')
|
906
|
+
.returns(association)
|
907
|
+
resource_class.class_eval { belongs_to :association_name, class_name: 'Foo' }
|
908
|
+
end
|
909
|
+
|
910
|
+
describe_association_method! :belongs_to, Protip::Resource::Associations::BelongsToAssociation
|
911
|
+
end
|
912
|
+
|
913
|
+
describe '.belongs_to_polymorphic' do
|
914
|
+
let :association do
|
915
|
+
association = mock.responds_like_instance_of Protip::Resource::Associations::BelongsToPolymorphicAssociation
|
916
|
+
association.stubs(:define_accessors!)
|
917
|
+
association
|
918
|
+
end
|
919
|
+
|
920
|
+
it 'creates a polymorphic belongs_to association, passing in nested associations from its yielded block' do
|
921
|
+
nested_association = mock.responds_like_instance_of Protip::Resource::Associations::BelongsToAssociation
|
922
|
+
resource_class.expects(:belongs_to).once.with(:foo).returns(nested_association)
|
923
|
+
|
924
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation.expects(:new).once
|
925
|
+
.with(resource_class, :bar, [nested_association], id_field: 'field').returns(association)
|
926
|
+
|
927
|
+
resource_class.class_eval do
|
928
|
+
belongs_to_polymorphic :bar, id_field: 'field' do
|
929
|
+
belongs_to :foo
|
930
|
+
end
|
931
|
+
end
|
932
|
+
end
|
933
|
+
|
934
|
+
describe_association_method!(:belongs_to_polymorphic,
|
935
|
+
Protip::Resource::Associations::BelongsToPolymorphicAssociation) { }
|
936
|
+
end
|
937
|
+
|
938
|
+
# {
|
939
|
+
# references_through_one_of: Protip::Resource::Associations::ReferencesThroughOneOfAssociation,
|
940
|
+
# }.each do |method, association_class|
|
941
|
+
# describe ".#{method}" do
|
942
|
+
# it 'creates an association of the correct type and defines accessors' do
|
943
|
+
# association = mock.responds_like_instance_of(association_class)
|
944
|
+
# association_class.expects(:new).once.with(resource_class, :some_id, {class_name: 'Foo'}).returns(association)
|
945
|
+
# association.expects(:define_accessors!)
|
946
|
+
# resource_class.class_exec(method) do
|
947
|
+
# send(method, :some_id, class_name: 'Foo')
|
948
|
+
# end
|
949
|
+
# end
|
950
|
+
#
|
951
|
+
# it 'raises an error on invalid options' do
|
952
|
+
# error = assert_raises ArgumentError do
|
953
|
+
# resource_class.class_exec(method) do
|
954
|
+
# send(method, :some_id, bad_option: 'bad')
|
955
|
+
# end
|
956
|
+
# end
|
957
|
+
#
|
958
|
+
# assert_match /bad_option/, error.message
|
959
|
+
# end
|
960
|
+
# end
|
961
|
+
# end
|
962
|
+
|
868
963
|
describe '.converter' do
|
869
964
|
describe 'default value' do
|
870
965
|
it 'defaults to the standard converter' do
|
@@ -2,6 +2,7 @@ require 'test_helper'
|
|
2
2
|
require 'money'
|
3
3
|
|
4
4
|
require 'google/protobuf/wrappers'
|
5
|
+
require 'protip/messages/active_support/time_with_zone'
|
5
6
|
require 'protip/standard_converter'
|
6
7
|
|
7
8
|
describe Protip::StandardConverter do
|
@@ -99,6 +100,15 @@ describe Protip::StandardConverter do
|
|
99
100
|
assert_equal 250, money.fractional
|
100
101
|
assert_equal ::Money.new(250, 'CAD'), money
|
101
102
|
end
|
103
|
+
|
104
|
+
it 'converts times with zones' do
|
105
|
+
message = ::Protip::Messages::ActiveSupport::TimeWithZone.new utc_timestamp: 1451610000,
|
106
|
+
time_zone_name: 'America/Los_Angeles'
|
107
|
+
time = converter.to_object(message)
|
108
|
+
assert_instance_of ::ActiveSupport::TimeWithZone, time
|
109
|
+
assert_equal 1451610000, time.to_i
|
110
|
+
assert_equal '2015-12-31T17:00:00-08:00', time.iso8601
|
111
|
+
end
|
102
112
|
end
|
103
113
|
|
104
114
|
describe '#to_message' do
|
@@ -166,5 +176,28 @@ describe Protip::StandardConverter do
|
|
166
176
|
end
|
167
177
|
end
|
168
178
|
end
|
179
|
+
|
180
|
+
it 'converts times with zones' do
|
181
|
+
time_with_zone = ::ActiveSupport::TimeWithZone.new(Time.new(2016, 1, 1, 0, 0, 0, 0),
|
182
|
+
::ActiveSupport::TimeZone.new('America/New_York'))
|
183
|
+
message = converter.to_message(time_with_zone, ::Protip::Messages::ActiveSupport::TimeWithZone)
|
184
|
+
assert_equal 1451606400, message.utc_timestamp
|
185
|
+
assert_equal 'America/New_York', message.time_zone_name
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'converts times without zones' do
|
189
|
+
time = Time.new(2016, 1, 1, 0, 0, 0, -3600)
|
190
|
+
message = converter.to_message(time, ::Protip::Messages::ActiveSupport::TimeWithZone)
|
191
|
+
assert_equal 1451610000, message.utc_timestamp
|
192
|
+
assert_equal 'UTC', message.time_zone_name
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'converts datetimes without zones' do
|
196
|
+
datetime = DateTime.new(2016, 1, 1, 0, 0, 0, '-1')
|
197
|
+
message = converter.to_message(datetime, ::Protip::Messages::ActiveSupport::TimeWithZone)
|
198
|
+
assert_equal 1451610000, message.utc_timestamp
|
199
|
+
assert_equal 'UTC', message.time_zone_name
|
200
|
+
|
201
|
+
end
|
169
202
|
end
|
170
203
|
end
|
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.
|
4
|
+
version: 0.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- AngelList
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-02-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -98,6 +98,20 @@ dependencies:
|
|
98
98
|
- - "~>"
|
99
99
|
- !ruby/object:Gem::Version
|
100
100
|
version: '5.0'
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: minitest-stub-const
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0.5'
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0.5'
|
101
115
|
- !ruby/object:Gem::Dependency
|
102
116
|
name: mocha
|
103
117
|
requirement: !ruby/object:Gem::Requirement
|
@@ -165,6 +179,7 @@ extensions: []
|
|
165
179
|
extra_rdoc_files: []
|
166
180
|
files:
|
167
181
|
- definitions/google/protobuf/wrappers.proto
|
182
|
+
- definitions/protip/messages/active_support/time_with_zone.proto
|
168
183
|
- definitions/protip/messages/array.proto
|
169
184
|
- definitions/protip/messages/currency.proto
|
170
185
|
- definitions/protip/messages/errors.proto
|
@@ -176,6 +191,7 @@ files:
|
|
176
191
|
- lib/protip/client.rb
|
177
192
|
- lib/protip/converter.rb
|
178
193
|
- lib/protip/error.rb
|
194
|
+
- lib/protip/messages/active_support/time_with_zone.rb
|
179
195
|
- lib/protip/messages/array.rb
|
180
196
|
- lib/protip/messages/currency.rb
|
181
197
|
- lib/protip/messages/errors.rb
|
@@ -183,6 +199,9 @@ files:
|
|
183
199
|
- lib/protip/messages/range.rb
|
184
200
|
- lib/protip/messages/types.rb
|
185
201
|
- lib/protip/resource.rb
|
202
|
+
- lib/protip/resource/associations/association.rb
|
203
|
+
- lib/protip/resource/associations/belongs_to_association.rb
|
204
|
+
- lib/protip/resource/associations/belongs_to_polymorphic_association.rb
|
186
205
|
- lib/protip/resource/creatable.rb
|
187
206
|
- lib/protip/resource/destroyable.rb
|
188
207
|
- lib/protip/resource/extra_methods.rb
|
@@ -192,6 +211,9 @@ files:
|
|
192
211
|
- lib/protip/wrapper.rb
|
193
212
|
- test/functional/protip/resource_test.rb
|
194
213
|
- test/test_helper.rb
|
214
|
+
- test/unit/protip/resource/associations/association_test.rb
|
215
|
+
- test/unit/protip/resource/associations/belongs_to_association_test.rb
|
216
|
+
- test/unit/protip/resource/associations/belongs_to_polymorphic_association_test.rb
|
195
217
|
- test/unit/protip/resource_test.rb
|
196
218
|
- test/unit/protip/standard_converter_test.rb
|
197
219
|
- test/unit/protip/wrapper_test.rb
|