contracted_value 0.1.2 → 0.2.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
  SHA256:
3
- metadata.gz: 1955c7fa4eaf8ae64db37e57575a6da241315ebd5855d72097e8e175292d0ee3
4
- data.tar.gz: d34b3b78b78e7e27466468851e96968293e978cadfc40104e3a67425e69eb4a5
3
+ metadata.gz: 26c83184ecb824d29bc3d258555f6f915d0478e7e1d5e8dde8bf8a21935b69a4
4
+ data.tar.gz: 73caf82b999e6e65cd158316982f5051a19f219eabe85e96434c98be72d973ca
5
5
  SHA512:
6
- metadata.gz: 125f0ff78bdf35d47b2fd7400ebb93db3a249d0b04abe1462394b732ad1d804326d7d89c7b04e0d3ca28cb738bc1f350a5091d2ae559490cfea468b864a1d03d
7
- data.tar.gz: 184cd1009cbd96af31d5fce7c075c483ce4762fc77936843c66a13df2c43b0aa95e70fe14e5e7c8ddaea1ebaf6a85084fc0345479af1e2a273cab6ca8942f64d
6
+ metadata.gz: b712d12af9079d88a5d44c56aaad07fa8e29ae56bed4f37f6fdcd5f36ab2c1e392e1afc56b1e84e98ef9d621a16843ca407dc31002345761c74e16e2301654cc
7
+ data.tar.gz: eece1cec55a0e03b15da9c7c97d4b8dc5aa55f3e7138ecc5448a2f0b36bcc73da5f6e0b245f4c1d03819342cdee0c39de2c8516afeae47d671bb8db841d8af80
@@ -6,11 +6,13 @@ on:
6
6
  - master
7
7
  paths-ignore:
8
8
  - 'README.md'
9
+ - 'CHANGELOG.md'
9
10
  push:
10
11
  branches:
11
12
  - master
12
13
  paths-ignore:
13
14
  - 'README.md'
15
+ - 'CHANGELOG.md'
14
16
 
15
17
  jobs:
16
18
  coverage:
@@ -24,7 +26,8 @@ jobs:
24
26
  os:
25
27
  - ubuntu-latest
26
28
  ruby:
27
- - "3.2"
29
+ - "3.4"
30
+ - "4.0"
28
31
  gemfile:
29
32
  - gemfiles/contracts_17_0.gemfile
30
33
  env:
@@ -33,7 +36,7 @@ jobs:
33
36
  runs-on: ${{ matrix.os }}
34
37
  steps:
35
38
  - name: Checkout
36
- uses: actions/checkout@v4
39
+ uses: actions/checkout@v6
37
40
 
38
41
  - name: Setup Ruby
39
42
  uses: ruby/setup-ruby@v1
@@ -6,11 +6,13 @@ on:
6
6
  - master
7
7
  paths-ignore:
8
8
  - 'README.md'
9
+ - 'CHANGELOG.md'
9
10
  push:
10
11
  branches:
11
12
  - master
12
13
  paths-ignore:
13
14
  - 'README.md'
15
+ - 'CHANGELOG.md'
14
16
 
15
17
  jobs:
16
18
  unit_tests:
@@ -24,9 +26,11 @@ jobs:
24
26
  os:
25
27
  - ubuntu-latest
26
28
  ruby:
27
- - "3.0"
28
29
  - "3.1"
29
30
  - "3.2"
31
+ - "3.3"
32
+ - "3.4"
33
+ - "4.0"
30
34
  gemfile:
31
35
  - gemfiles/contracts_17_0.gemfile
32
36
  allow_failures:
@@ -43,7 +47,7 @@ jobs:
43
47
  continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
44
48
  steps:
45
49
  - name: Checkout
46
- uses: actions/checkout@v4
50
+ uses: actions/checkout@v6
47
51
  - name: Setup Ruby
48
52
  uses: ruby/setup-ruby@v1
49
53
  with:
data/CHANGELOG.md CHANGED
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
 
6
6
  ## [Unreleased]
7
+ [Unreleased]: https://github.com/PikachuEXE/contracted_value/compare/v0.2.0...master
7
8
 
8
9
  ### Added
9
10
 
@@ -18,7 +19,26 @@ This project adheres to [Semantic Versioning](http://semver.org/).
18
19
  - Nothing
19
20
 
20
21
 
21
- ## [0.1.2]
22
+ ## [0.2.0] - 2026-04-29
23
+ [0.2.0]: https://github.com/PikachuEXE/contracted_value/releases/tag/v0.2.0
24
+
25
+ ### Added
26
+
27
+ - Add feature detect_unexpected_keys
28
+ (https://github.com/PikachuEXE/contracted_value/pull/9)
29
+
30
+
31
+ ## [0.1.3] - 2023-10-11
32
+ [0.1.3]: https://github.com/PikachuEXE/contracted_value/releases/tag/v0.1.3
33
+
34
+ ### Fixed
35
+
36
+ - Revert `Make code "Object Shape friendly"`
37
+ Strange issue discovered in an app in production environment
38
+
39
+
40
+ ## [0.1.2] - 2023-10-11
41
+ [0.1.2]: https://github.com/PikachuEXE/contracted_value/releases/tag/v0.1.2
22
42
 
23
43
  ### Changed
24
44
 
@@ -27,7 +47,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
27
47
  (https://github.com/PikachuEXE/contracted_value/pull/5)
28
48
 
29
49
 
30
- ## [0.1.1]
50
+ ## [0.1.1] - 2022-09-07
51
+ [0.1.1]: https://github.com/PikachuEXE/contracted_value/releases/tag/v0.1.1
31
52
 
32
53
  ### Changed
33
54
 
@@ -38,11 +59,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
38
59
  - Fix Ruby 3.x compatibility
39
60
 
40
61
 
41
- ## 0.1.0 - 2019-06-05
62
+ ## [0.1.0] - 2019-06-05
63
+ [0.1.0]: https://github.com/PikachuEXE/contracted_value/releases/tag/v0.1.0
42
64
 
43
65
  ### Added
44
66
 
45
67
  - Initial release
46
-
47
- [0.1.2]: https://github.com/PikachuEXE/contracted_value/compare/v0.1.1...v0.1.2
48
- [0.1.1]: https://github.com/PikachuEXE/contracted_value/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -247,6 +247,35 @@ WhatIsThis::Entry.new(
247
247
  ).something_optional # => nil
248
248
  ```
249
249
 
250
+ #### Unexpected keys
251
+ Call `detect_unexpected_keys` to detect unexpected keys by raising error
252
+ It's off by default
253
+
254
+
255
+ ```ruby
256
+ module ::WhatIsThis
257
+ class Entry < ::ContractedValue::Value
258
+ include ::Contracts::Core
259
+ include ::Contracts::Builtin
260
+
261
+ detect_unexpected_keys
262
+
263
+ attribute(
264
+ :something_required,
265
+ )
266
+ attribute(
267
+ :something_optional,
268
+ default_value: nil,
269
+ )
270
+ end
271
+ end
272
+
273
+ WhatIsThis::Entry.new(
274
+ something_required: 123,
275
+ something_unexpected: "whatever",
276
+ ) # => error
277
+ ```
278
+
250
279
 
251
280
  ### Object Freezing
252
281
  All input values are frozen using [`ice_nine`](https://github.com/dkubb/ice_nine) by default
@@ -353,6 +382,10 @@ Pikachu.new.name.frozen? # => true, as mentioned above default value are always
353
382
  Pikachu.new(name: "Pikaaaachuuu").name.frozen? # => false
354
383
  ```
355
384
 
385
+ #### `detect_unexpected_keys` inherited
386
+ If parent class already called `detect_unexpected_keys`, all children classes would have it enabled too
387
+ Otherwise you can call `detect_unexpected_keys` in child class(es) without affecting parent class
388
+
356
389
 
357
390
  ## Related gems
358
391
  Here is a list of gems which I found and I have tried some of them.
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ require "rspec/core/rake_task"
7
7
 
8
8
  RSpec::Core::RakeTask.new(:spec)
9
9
 
10
- if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
10
+ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"]
11
11
  task :default do
12
12
  sh "appraisal install && rake appraisal spec"
13
13
  end
@@ -38,7 +38,7 @@ Gem::Specification.new do |s|
38
38
  s.add_development_dependency "appraisal", "~> 2.0", ">= 2.5.0"
39
39
 
40
40
  s.add_development_dependency "rspec", "~> 3.0"
41
- s.add_development_dependency "rspec-its", "~> 1.0"
41
+ s.add_development_dependency "rspec-its", "~> 2.0"
42
42
 
43
43
  s.add_development_dependency "simplecov", ">= 0.21"
44
44
  s.add_development_dependency "simplecov-lcov", ">= 0.8"
@@ -52,4 +52,8 @@ Gem::Specification.new do |s|
52
52
  s.required_ruby_version = ">= 3.0.0"
53
53
 
54
54
  s.required_rubygems_version = ">= 1.4.0"
55
+
56
+ # requiring all owners to enable MFA on their account
57
+ # https://guides.rubygems.org/mfa-requirement-opt-in/
58
+ s.metadata["rubygems_mfa_required"] = "true"
55
59
  end
@@ -79,6 +79,16 @@ module ContractedValue
79
79
  )
80
80
  end
81
81
  end
82
+
83
+ class UnexpectedInputKeys < ArgumentError
84
+ def initialize(keys)
85
+ super(
86
+ <<~MSG
87
+ Unexpected key(s) #{keys.map{|key| ":#{key}" }.join(", ")} detected in input
88
+ MSG
89
+ )
90
+ end
91
+ end
82
92
  end
83
93
 
84
94
  module Private
@@ -112,6 +122,10 @@ module ContractedValue
112
122
  end
113
123
  end
114
124
 
125
+ def detect_unexpected_keys(hash)
126
+ hash.keys - attr_names
127
+ end
128
+
115
129
  protected
116
130
 
117
131
  def merge!(other_attr_set)
@@ -238,9 +252,8 @@ module ContractedValue
238
252
  )
239
253
  end
240
254
 
241
- @attr_values = {}
242
-
243
- self.class.send(:attribute_set).each_attribute do |attribute|
255
+ attribute_set = self.class.send(:attribute_set)
256
+ attribute_set.each_attribute do |attribute|
244
257
  attr_value = attribute.extract_value(input_attr_values_hash)
245
258
 
246
259
  sometimes_frozen_attr_value =
@@ -262,27 +275,48 @@ module ContractedValue
262
275
 
263
276
  # Using symbol since attribute names are limited in number
264
277
  # An alternative would be using frozen string
265
- @attr_values.store(
266
- attribute.name.to_sym,
278
+ instance_variable_set(
279
+ :"@#{attribute.name}",
267
280
  sometimes_frozen_attr_value,
268
281
  )
269
282
  end
270
283
 
284
+ if self.class.send(:should_detect_unexpected_keys)
285
+ unexpected_keys = attribute_set.detect_unexpected_keys(input_attr_values_hash)
286
+ if unexpected_keys.any?
287
+ raise Errors::UnexpectedInputKeys.new(
288
+ unexpected_keys,
289
+ )
290
+ end
291
+ end
292
+
271
293
  freeze
272
294
  end
273
295
  # rubocop:enable Metrics/AbcSize
274
296
  # rubocop:enable Metrics/CyclomaticComplexity
275
297
 
276
298
  def to_h
277
- @attr_values.clone
299
+ self.class.send(:attribute_set).
300
+ each_attribute.each_with_object({}) do |attribute, hash|
301
+ hash[attribute.name] = instance_variable_get(:"@#{attribute.name}")
302
+ end
278
303
  end
279
304
 
280
305
  # == Class interface == #
281
306
  class << self
282
- def inherited(klass)
307
+ def inherited(child_klass)
283
308
  super
284
309
 
285
- klass.instance_variable_set(:@attribute_set, AttributeSet.new)
310
+ child_klass.instance_variable_set(:@attribute_set, AttributeSet.new)
311
+ child_klass.instance_variable_set(
312
+ :@should_detect_unexpected_keys,
313
+ if child_klass.superclass.respond_to?(:should_detect_unexpected_keys, true)
314
+ child_klass.superclass.send(:should_detect_unexpected_keys)
315
+ else
316
+ false
317
+ end
318
+ )
319
+
286
320
  end
287
321
 
288
322
  private
@@ -306,7 +340,7 @@ module ContractedValue
306
340
  )
307
341
  @attribute_set = @attribute_set.add(attr)
308
342
 
309
- define_method(name_in_sym) { @attr_values[name_in_sym] }
343
+ attr_reader(name_in_sym)
310
344
  end
311
345
 
312
346
  # @api private
@@ -325,6 +359,14 @@ module ContractedValue
325
359
  # @attribute_set would be nil
326
360
  super_attribute_set.merge(@attribute_set || AttributeSet.new)
327
361
  end
362
+
363
+ # @api
364
+ def detect_unexpected_keys
365
+ @should_detect_unexpected_keys = true
366
+ end
367
+
368
+ # @api private
369
+ attr_reader :should_detect_unexpected_keys
328
370
  end
329
371
  # == Class interface == #
330
372
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ContractedValue
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -48,12 +48,13 @@ require "spec_helper"
48
48
  }.to_not raise_error
49
49
  end
50
50
 
51
- example "does not raise error when declaring 1 attribute with number name" do
51
+ example "does raise error when declaring 1 attribute with number name" do
52
52
  expect {
53
53
  value_class.class_eval do
54
54
  attribute(1)
55
55
  end
56
- }.to raise_error(::NoMethodError, /undefined method `to_sym'/)
56
+ }.to raise_error(::NoMethodError, /undefined method `to_sym'|undefined method 'to_sym'/)
57
+ # ruby 3.4+ uses different quotes
57
58
  end
58
59
  end
59
60
 
@@ -83,6 +84,12 @@ require "spec_helper"
83
84
  }
84
85
  end
85
86
 
87
+ let(:inputs_with_extra) do
88
+ default_inputs.merge(
89
+ attribute_3: 1,
90
+ )
91
+ end
92
+
86
93
  let(:non_hash) do
87
94
  []
88
95
  end
@@ -156,6 +163,30 @@ require "spec_helper"
156
163
  end
157
164
  end
158
165
  end
166
+
167
+ it "does not raise error when extra attribute is input" do
168
+ aggregate_failures do
169
+ expect {
170
+ value_class.new(
171
+ inputs_with_extra,
172
+ )
173
+ }.to_not raise_error
174
+ end
175
+ end
176
+
177
+ it "does not raise error when extra attribute is input" do
178
+ aggregate_failures do
179
+ value_class.class_eval do
180
+ detect_unexpected_keys
181
+ end
182
+
183
+ expect {
184
+ value_class.new(
185
+ inputs_with_extra,
186
+ )
187
+ }.to raise_error(::ContractedValue::Errors::UnexpectedInputKeys)
188
+ end
189
+ end
159
190
  end
160
191
 
161
192
  context "with class with some attributes declared with contract" do
@@ -621,6 +652,7 @@ require "spec_helper"
621
652
  end
622
653
  child_value_class.new(attribute_1: "")
623
654
  }.to raise_error(::ContractedValue::Errors::InvalidAttributeValue)
655
+ # Note that the error above is for the input not the attribute declaration
624
656
  end
625
657
 
626
658
  example "does not raise error when declaring existing attribute with different default_value" do
@@ -651,6 +683,66 @@ require "spec_helper"
651
683
 
652
684
  end
653
685
 
686
+ describe "for detect_unexpected_keys" do
687
+ let(:child_value_class) do
688
+ Class.new(parent_value_class)
689
+ end
690
+
691
+ context "when detect_unexpected_keys declared inside parent class" do
692
+ let(:parent_value_class) do
693
+ Class.new(described_class).tap do |klass|
694
+ klass.class_eval do
695
+ detect_unexpected_keys
696
+
697
+ # Too lazy to include parent attributes in all examples
698
+ attribute(:attribute_1)
699
+ end
700
+ end
701
+ end
702
+
703
+ it "does raise error on parent class object" do
704
+ expect {
705
+ parent_value_class.new(attribute_1: 1, attribute_2: 2)
706
+ }.to raise_error(::ContractedValue::Errors::UnexpectedInputKeys)
707
+ end
708
+
709
+ it "does raise error on child class object" do
710
+ expect {
711
+ child_value_class.new(attribute_1: 1, attribute_2: 2)
712
+ }.to raise_error(::ContractedValue::Errors::UnexpectedInputKeys)
713
+ end
714
+ end
715
+
716
+ context "when detect_unexpected_keys declared inside child class" do
717
+ let(:parent_value_class) do
718
+ Class.new(described_class).tap do |klass|
719
+ klass.class_eval do
720
+ # Too lazy to include parent attributes in all examples
721
+ attribute(:attribute_1)
722
+ end
723
+ end
724
+ end
725
+
726
+ before do
727
+ child_value_class.class_eval do
728
+ detect_unexpected_keys
729
+ end
730
+ end
731
+
732
+ it "does not raise error on parent class object" do
733
+ expect {
734
+ parent_value_class.new(attribute_1: 1, attribute_2: 2)
735
+ }.to_not raise_error
736
+ end
737
+
738
+ it "does raise error on child class object" do
739
+ expect {
740
+ child_value_class.new(attribute_1: 1, attribute_2: 2)
741
+ }.to raise_error(::ContractedValue::Errors::UnexpectedInputKeys)
742
+ end
743
+ end
744
+ end
745
+
654
746
  end
655
747
 
656
748
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contracted_value
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PikachuEXE
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-10-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: contracts
@@ -126,14 +125,14 @@ dependencies:
126
125
  requirements:
127
126
  - - "~>"
128
127
  - !ruby/object:Gem::Version
129
- version: '1.0'
128
+ version: '2.0'
130
129
  type: :development
131
130
  prerelease: false
132
131
  version_requirements: !ruby/object:Gem::Requirement
133
132
  requirements:
134
133
  - - "~>"
135
134
  - !ruby/object:Gem::Version
136
- version: '1.0'
135
+ version: '2.0'
137
136
  - !ruby/object:Gem::Dependency
138
137
  name: simplecov
139
138
  requirement: !ruby/object:Gem::Requirement
@@ -238,8 +237,8 @@ files:
238
237
  homepage: http://github.com/PikachuEXE/contracted_value
239
238
  licenses:
240
239
  - MIT
241
- metadata: {}
242
- post_install_message:
240
+ metadata:
241
+ rubygems_mfa_required: 'true'
243
242
  rdoc_options: []
244
243
  require_paths:
245
244
  - lib
@@ -254,8 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
253
  - !ruby/object:Gem::Version
255
254
  version: 1.4.0
256
255
  requirements: []
257
- rubygems_version: 3.4.20
258
- signing_key:
256
+ rubygems_version: 4.0.9
259
257
  specification_version: 4
260
258
  summary: Contracted immutable(by default) value objects
261
259
  test_files: