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 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