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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c99dbb33000837b699628b8a836790ef3f11fb2
4
- data.tar.gz: 0c137bcc95256ef4c85977aaeca953e2c137a8e9
3
+ metadata.gz: 7c340cc60f0fd35abd3d10a61bb3e1f089a7a74f
4
+ data.tar.gz: b0638b042c3234a0915811aeb8b403c855cd8ce8
5
5
  SHA512:
6
- metadata.gz: 79e28c350f9dd492a2d0fea1f586e5cd1c28066ab78cead4d5a3be04344803192eae92f031ea95e3532d3f6c8ac90f355d5daa4db9002e12380ed4f0552b4f6f
7
- data.tar.gz: 91a2a3a68ebc88a3640c31d7fc4c6f1d8e94538fc821759bacbbcf56ec2ccdfa71fd82aff60789aaed606fa9170661e4f19da20a0e46ef9873e3783fc127b50f
6
+ metadata.gz: fb76ebde93c39fca443494c1505360f426f5001927ada5be9da5976db7fac15bc066491d084c0ef6963f3b519c3141a9df4d3a683eacf7f911db2ff94f0236af
7
+ data.tar.gz: 06d98607aacfd60d8e9ddabb9117beecc78c7b0361bad1ccf329e786593efbbc54a6e2e23f0d7256a031ecfdf0fe270d2057f4f8a1aa5cf81fa93cd7d501aab2
@@ -0,0 +1,8 @@
1
+ syntax = "proto3";
2
+
3
+ package protip.messages.ActiveSupport;
4
+
5
+ message TimeWithZone {
6
+ int64 utc_timestamp = 1;
7
+ string time_zone_name = 2;
8
+ }
@@ -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
@@ -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
- message_class.new value: value_to_boolean(value)
71
- end
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, 6
35
+ optional :param, :string, 7
35
36
  end
36
37
 
37
38
  add_message 'name_response' do
38
- optional :name, :string, 7
39
+ optional :name, :string, 8
39
40
  end
40
41
 
41
42
  add_message 'search_request' do
42
- optional :term, :string, 8
43
+ optional :term, :string, 9
43
44
  end
44
45
 
45
46
  add_message 'search_response' do
46
- repeated :results, :string, 9
47
+ repeated :results, :string, 10
47
48
  end
48
49
 
49
50
  add_message 'fetch_request' do
50
- repeated :names, :string, 10
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({'ordered_tests' => 'bilbo', 'id' => 0, 'nested_message' => nil, 'nested_int' => 42},
116
- results[0].attributes)
117
- assert_equal({'ordered_tests' => 'baggins', 'id' => 1, 'nested_message' => nil, 'nested_int' => 43},
118
- results[1].attributes)
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
@@ -1,5 +1,6 @@
1
1
  require 'minitest/autorun'
2
2
  require 'mocha/mini_test'
3
3
  require 'webmock/minitest'
4
+ require 'minitest/stub/const'
4
5
 
5
6
  require 'minitest/pride'
@@ -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.17.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-01-26 00:00:00.000000000 Z
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