protip 0.12.4 → 0.13.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: 3ddcb2b4b3bd6f69e1166447dffb4dc3aa02760c
4
- data.tar.gz: 8dedbff1aff8667b6efb681076434fd8dc0405f3
3
+ metadata.gz: 1170fab1e59f11eaf4e5d9fa6180d7086e5fe5fc
4
+ data.tar.gz: dece3a11f6abd2f216e78ee7df65245b03dd1c55
5
5
  SHA512:
6
- metadata.gz: f225ba2453ee71bb97c0546f5fb4e702b96f046c612bd85c6b3277658c417138b17bb812c6895f5c6ebace61d8bddf13d792357c11c9ac785bededf41c1426c7
7
- data.tar.gz: 600c78b9cb83e256ff1fc9cef872bb68de7040c9a2b6b6a37e4f74c243ac5d83b8e5bf10474254767406458712238f5fd854a5838340d95a5a33c1e781c719b5
6
+ metadata.gz: 07f0f92583e771802269412f33d383bcf878d35939b97906db016fa8c3af12500bdcffe9a5eb6545233f4cbfc878c2a842a01db66d54a8463ec2b18a45080684
7
+ data.tar.gz: dfaf66f2608d722e89b64bbbf144c12d36e788205e69431c528e7678507f487419a50c90e061a6fadc55d1eee5163e02e5a0acb5eb5a6675cc77c7f458e6cdf6
@@ -13,6 +13,9 @@ require 'active_model/naming'
13
13
  require 'active_model/translation'
14
14
  require 'active_model/errors'
15
15
 
16
+ require 'active_model/attribute_methods' # ActiveModel::Dirty depends on this
17
+ require 'active_model/dirty'
18
+
16
19
  require 'forwardable'
17
20
 
18
21
  require 'protip/error'
@@ -129,6 +132,8 @@ module Protip
129
132
  include ActiveModel::Validations
130
133
  include ActiveModel::Conversion
131
134
 
135
+ include ActiveModel::Dirty
136
+
132
137
  included do
133
138
  extend ActiveModel::Naming
134
139
  extend ActiveModel::Translation
@@ -136,7 +141,6 @@ module Protip
136
141
 
137
142
  def_delegator :@wrapper, :message
138
143
  def_delegator :@wrapper, :as_json
139
- def_delegator :@wrapper, :assign_attributes
140
144
  end
141
145
  module ClassMethods
142
146
 
@@ -166,12 +170,23 @@ module Protip
166
170
  @message = message
167
171
  @message.descriptor.each do |field|
168
172
  def_delegator :@wrapper, :"#{field.name}"
169
- def_delegator :@wrapper, :"#{field.name}="
170
173
  if ::Protip::Wrapper.matchable?(field)
171
174
  def_delegator :@wrapper, :"#{field.name}?"
172
175
  end
176
+
177
+ define_method "#{field.name}=" do |new_value|
178
+ old_wrapped_value = @wrapper.send(field.name)
179
+ @wrapper.send("#{field.name}=", new_value)
180
+ new_wrapped_value = @wrapper.send(field.name)
181
+
182
+ # needed for ActiveModel::Dirty
183
+ send("#{field.name}_will_change!") if new_wrapped_value != old_wrapped_value
184
+ end
173
185
  end
174
186
 
187
+ # needed for ActiveModel::Dirty
188
+ define_attribute_methods @message.descriptor.map(&:name)
189
+
175
190
  # Validate arguments
176
191
  actions.map!{|action| action.to_sym}
177
192
  (actions - %i(show index create update destroy)).each do |action|
@@ -258,6 +273,12 @@ module Protip
258
273
  super()
259
274
  end
260
275
 
276
+ def assign_attributes(attributes)
277
+ # the resource needs to call its own setters so that fields get marked as dirty
278
+ attributes.each { |field_name, value| send("#{field_name}=", value) }
279
+ nil # return nil to match ActiveRecord behavior
280
+ end
281
+
261
282
  def message=(message)
262
283
  @wrapper = Protip::Wrapper.new(message, self.class.converter)
263
284
  end
@@ -271,6 +292,7 @@ module Protip
271
292
  else
272
293
  create!
273
294
  end
295
+ changes_applied
274
296
  rescue Protip::UnprocessableEntityError => error
275
297
  success = false
276
298
  error.errors.messages.each do |message|
@@ -298,5 +320,13 @@ module Protip
298
320
  def errors
299
321
  @errors ||= ActiveModel::Errors.new(self)
300
322
  end
323
+
324
+ private
325
+
326
+ # needed for ActiveModel::Dirty
327
+ def changes_applied
328
+ @previously_changed = changes
329
+ @changed_attributes.clear
330
+ end
301
331
  end
302
332
  end
@@ -28,7 +28,9 @@ module Protip
28
28
  def method_missing(name, *args)
29
29
  if (name =~ /=$/ && field = message.class.descriptor.detect{|field| :"#{field.name}=" == name})
30
30
  raise ArgumentError unless args.length == 1
31
- set field, args[0]
31
+ attributes = {}.tap { |hash| hash[field.name] = args[0] }
32
+ assign_attributes attributes
33
+ args[0] # return the input value (to match ActiveRecord behavior)
32
34
  elsif (name =~ /\?$/ && field = message.class.descriptor.detect{|field| self.class.matchable?(field) && :"#{field.name}?" == name})
33
35
  raise ArgumentError unless args.length == 1
34
36
  matches? field, args[0]
@@ -98,9 +100,11 @@ module Protip
98
100
  if field.type == :message && !converter.convertible?(field.subtype.msgclass)
99
101
  if value.is_a?(field.subtype.msgclass) # If a message, set it directly
100
102
  set(field, value)
101
- else # If a hash, pass it through to the nested message
103
+ elsif value.is_a?(Hash) # If a hash, pass it through to the nested message
102
104
  wrapper = get(field) || build(field.name) # Create the field if it doesn't already exist
103
105
  wrapper.assign_attributes value
106
+ else # If value is a simple type (e.g. nil), set the value directly
107
+ set(field, value)
104
108
  end
105
109
  # Otherwise, if the field is a convertible message or a simple type, we set the value directly
106
110
  else
@@ -1,6 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  require 'protip'
4
+ require 'google/protobuf/wrappers'
4
5
 
5
6
  describe 'Protip::Resource (functional)' do
6
7
 
data/test/test_helper.rb CHANGED
@@ -2,4 +2,4 @@ require 'minitest/autorun'
2
2
  require 'mocha/mini_test'
3
3
  require 'webmock/minitest'
4
4
 
5
- require 'minitest/pride'
5
+ require 'minitest/pride'
@@ -112,7 +112,7 @@ module Protip::ResourceTest # Namespace for internal constants
112
112
  end
113
113
 
114
114
  it 'checks with the converter when setting message types' do
115
- converter.expects(:convertible?).once.with(nested_message_class).returns(false)
115
+ converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(false)
116
116
  resource = resource_class.new
117
117
  assert_raises(ArgumentError) do
118
118
  resource.nested_message = 5
@@ -120,9 +120,9 @@ module Protip::ResourceTest # Namespace for internal constants
120
120
  end
121
121
 
122
122
  it 'converts message types to and from their Ruby values when the converter allows' do
123
- converter.expects(:convertible?).times(2).with(nested_message_class).returns(true)
123
+ converter.expects(:convertible?).at_least_once.with(nested_message_class).returns(true)
124
124
  converter.expects(:to_message).once.with(6, nested_message_class).returns(nested_message_class.new number: 100)
125
- converter.expects(:to_object).once.with(nested_message_class.new number: 100).returns 'intern'
125
+ converter.expects(:to_object).at_least_once.with(nested_message_class.new number: 100).returns 'intern'
126
126
 
127
127
  resource = resource_class.new
128
128
  resource.nested_message = 6
@@ -434,13 +434,41 @@ module Protip::ResourceTest # Namespace for internal constants
434
434
  attrs = {id: 2}
435
435
  assert_equal resource_message_class.new(attrs), resource_class.new(attrs).message
436
436
  end
437
+ end
438
+ end
437
439
 
438
- it 'delegates to #assign_attributes on its wrapper object when a hash is given' do
439
- attrs = {id: 3}
440
- Protip::Wrapper.any_instance.expects(:assign_attributes).once.with({id: 3})
441
- resource_class.new(attrs)
440
+ describe 'attribute writer' do
441
+ before do
442
+ resource_class.class_exec(resource_message_class) do |resource_message_class|
443
+ resource actions: [], message: resource_message_class
442
444
  end
443
445
  end
446
+
447
+ it 'delegates writes to the wrapper object' do
448
+ resource = resource_class.new
449
+ test_string = 'new'
450
+ Protip::Wrapper.any_instance.expects(:string=).with(test_string)
451
+ resource.string = test_string
452
+ end
453
+
454
+ it 'marks the resource and attribute as changed if the value is changed' do
455
+ resource = resource_class.new string: 'original'
456
+ resource.string = 'new'
457
+ assert resource.changed?, 'resource should be marked as changed'
458
+ assert resource.string_changed?, 'string field should be marked as changed'
459
+ end
460
+
461
+ it 'does not mark the resource and attribute as changed if the value is not changed' do
462
+ resource = resource_class.new string: 'original'
463
+ resource.send :changes_applied # clear the changes
464
+ # establish that the changes were cleared
465
+ assert !resource.changed?, 'resource should be not marked as changed'
466
+ assert !resource.string_changed?, 'string field should not be marked as changed'
467
+
468
+ resource.string = 'original'
469
+ assert !resource.changed?, 'resource should be not marked as changed'
470
+ assert !resource.string_changed?, 'string field should not be marked as changed'
471
+ end
444
472
  end
445
473
 
446
474
  describe '#assign_attributes' do
@@ -449,11 +477,17 @@ module Protip::ResourceTest # Namespace for internal constants
449
477
  resource actions: [], message: resource_message_class
450
478
  end
451
479
  end
452
- it 'delegates to #assign_attributes on its wrapper object' do
480
+
481
+ it 'calls the attribute writer for each attribute' do
453
482
  resource = resource_class.new
483
+ test_string = 'whodunnit'
484
+ resource.expects(:string=).with(test_string)
485
+ resource.assign_attributes(string: test_string)
486
+ end
454
487
 
455
- Protip::Wrapper.any_instance.expects(:assign_attributes).once.with(string: 'whodunnit').returns('boo')
456
- assert_equal 'boo', resource.assign_attributes(string: 'whodunnit')
488
+ it 'returns nil' do
489
+ resource = resource_class.new
490
+ assert_nil resource.assign_attributes(string: 'asdf')
457
491
  end
458
492
  end
459
493
 
@@ -495,6 +529,14 @@ module Protip::ResourceTest # Namespace for internal constants
495
529
  resource.save
496
530
  assert_equal response, resource.message
497
531
  end
532
+
533
+ it 'marks changes as applied' do
534
+ client.stubs(:request).returns(response)
535
+ resource = resource_class.new(string: 'time')
536
+ assert resource.string_changed?, 'string should initially be changed'
537
+ assert resource.save
538
+ assert !resource.string_changed?, 'string should no longer be changed after save'
539
+ end
498
540
  end
499
541
 
500
542
  describe 'for an existing record' do
@@ -528,6 +570,14 @@ module Protip::ResourceTest # Namespace for internal constants
528
570
  resource.save
529
571
  assert_equal response, resource.message
530
572
  end
573
+
574
+ it 'marks changes as applied' do
575
+ client.stubs(:request).returns(response)
576
+ resource = resource_class.new id: 5, string: 'new_string'
577
+ assert resource.string_changed?, 'string should initially be changed'
578
+ assert resource.save
579
+ assert !resource.string_changed?, 'string should no longer be changed after save'
580
+ end
531
581
  end
532
582
 
533
583
  describe 'when validation errors are thrown' do
@@ -567,6 +617,13 @@ module Protip::ResourceTest # Namespace for internal constants
567
617
  it 'returns false' do
568
618
  refute @resource.save, 'save returned true'
569
619
  end
620
+
621
+ it 'does not mark changes as applied' do
622
+ @resource.string = 'new_string'
623
+ assert @resource.string_changed?, 'string should initially be changed'
624
+ refute @resource.save
625
+ assert @resource.string_changed?, 'string should still be changed after unsuccessful save'
626
+ end
570
627
  end
571
628
  end
572
629
 
@@ -216,6 +216,13 @@ module Protip::WrapperTest # namespace for internal constants
216
216
  wrapper.assign_attributes inner: {value: 50, note: 'noted'}
217
217
  end
218
218
 
219
+ it "sets message fields to nil when they're assigned nil" do
220
+ wrapped_message.inner = inner_message_class.new(value: 60)
221
+ assert wrapped_message.inner
222
+ wrapper.assign_attributes inner: nil
223
+ assert_nil wrapped_message.inner
224
+ end
225
+
219
226
  it 'allows messages to be assigned directly' do
220
227
  message = inner_message_class.new
221
228
  wrapper.assign_attributes inner: message
@@ -341,7 +348,7 @@ module Protip::WrapperTest # namespace for internal constants
341
348
  end
342
349
  end
343
350
 
344
- describe '#set' do
351
+ describe 'attribute writer' do # generated via method_missing?
345
352
  it 'does not convert simple fields' do
346
353
  converter.expects(:convertible?).never
347
354
  converter.expects(:to_message).never
@@ -351,7 +358,7 @@ module Protip::WrapperTest # namespace for internal constants
351
358
  end
352
359
 
353
360
  it 'converts convertible messages' do
354
- converter.expects(:convertible?).with(inner_message_class).once.returns(true)
361
+ converter.expects(:convertible?).at_least_once.with(inner_message_class).returns(true)
355
362
  converter.expects(:to_message).with(40, inner_message_class).returns(inner_message_class.new(value: 30))
356
363
 
357
364
  wrapper.inner = 40
@@ -359,7 +366,7 @@ module Protip::WrapperTest # namespace for internal constants
359
366
  end
360
367
 
361
368
  it 'removes message fields when assigning nil' do
362
- converter.expects(:convertible?).never
369
+ converter.expects(:convertible?).at_least_once.with(inner_message_class).returns(false)
363
370
  converter.expects(:to_message).never
364
371
 
365
372
  wrapper.inner = nil
@@ -367,7 +374,7 @@ module Protip::WrapperTest # namespace for internal constants
367
374
  end
368
375
 
369
376
  it 'raises an error when setting inconvertible messages' do
370
- converter.expects(:convertible?).with(inner_message_class).once.returns(false)
377
+ converter.expects(:convertible?).at_least_once.with(inner_message_class).returns(false)
371
378
  converter.expects(:to_message).never
372
379
  assert_raises ArgumentError do
373
380
  wrapper.inner = 'cannot convert me'
@@ -375,10 +382,12 @@ module Protip::WrapperTest # namespace for internal constants
375
382
  end
376
383
 
377
384
  it 'passes through messages without checking whether they are convertible' do
385
+ converter.expects(:convertible?).once.returns(true)
386
+ message = inner_message_class.new(value: 50)
387
+
378
388
  converter.expects(:convertible?).never
379
389
  converter.expects(:to_message).never
380
-
381
- wrapper.inner = inner_message_class.new(value: 50)
390
+ wrapper.inner = message
382
391
  assert_equal inner_message_class.new(value: 50), wrapper.message.inner
383
392
  end
384
393
 
@@ -410,6 +419,11 @@ module Protip::WrapperTest # namespace for internal constants
410
419
  wrapper.number = m
411
420
  assert_equal :ONE, wrapper.number
412
421
  end
422
+
423
+ it 'returns the input value' do
424
+ input_value = 'str'
425
+ assert_equal input_value, wrapper.send(:string=, input_value)
426
+ end
413
427
  end
414
428
 
415
429
  describe '#matches?' do
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.12.4
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AngelList
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-12 00:00:00.000000000 Z
11
+ date: 2015-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -182,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
182
  version: '0'
183
183
  requirements: []
184
184
  rubyforge_project:
185
- rubygems_version: 2.4.5
185
+ rubygems_version: 2.2.2
186
186
  signing_key:
187
187
  specification_version: 4
188
188
  summary: Resources backed by protobuf messages