protip 0.17.0 → 0.18.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/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
|