hashie 4.1.0 → 5.1.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.
data/README.md CHANGED
@@ -2,11 +2,53 @@
2
2
 
3
3
  [![Join the chat at https://gitter.im/hashie/hashie](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hashie/hashie?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
4
  [![Gem Version](http://img.shields.io/gem/v/hashie.svg)](http://badge.fury.io/rb/hashie)
5
- [![Build Status](http://img.shields.io/travis/hashie/hashie.svg)](https://travis-ci.org/hashie/hashie)
6
- [![Test Coverage](https://api.codeclimate.com/v1/badges/7a0b42c8a22c945571fd/test_coverage)](https://codeclimate.com/github/hashie/hashie/test_coverage)
7
- [![Maintainability](https://api.codeclimate.com/v1/badges/7a0b42c8a22c945571fd/maintainability)](https://codeclimate.com/github/hashie/hashie/maintainability)
8
-
9
- Hashie is a growing collection of tools that extend Hashes and make them more useful.
5
+ [![Build Status](https://github.com/hashie/hashie/actions/workflows/test.yml/badge.svg)](https://github.com/hashie/hashie/actions/workflows/test.yml)
6
+
7
+ [![eierlegende Wollmilchsau](./mascot.svg)](#mascot) Hashie is a growing collection of tools that extend Hashes and make them more useful.
8
+
9
+ # Table of Contents
10
+
11
+ - [Installation](#installation)
12
+ - [Stable Release](#stable-release)
13
+ - [Hash Extensions](#hash-extensions)
14
+ - [Logging](#logging)
15
+ - [Coercion](#coercion)
16
+ - [Coercing Collections](#coercing-collections)
17
+ - [Coercing Hashes](#coercing-hashes)
18
+ - [Coercing Core Types](#coercing-core-types)
19
+ - [Coercion Proc](#coercion-proc)
20
+ - [A note on circular coercion](#a-note-on-circular-coercion)
21
+ - [KeyConversion](#keyconversion)
22
+ - [MergeInitializer](#mergeinitializer)
23
+ - [MethodAccess](#methodaccess)
24
+ - [MethodAccessWithOverride](#methodaccesswithoverride)
25
+ - [MethodOverridingInitializer](#methodoverridinginitializer)
26
+ - [IndifferentAccess](#indifferentaccess)
27
+ - [IgnoreUndeclared](#ignoreundeclared)
28
+ - [DeepMerge](#deepmerge)
29
+ - [DeepFetch](#deepfetch)
30
+ - [DeepFind](#deepfind)
31
+ - [DeepLocate](#deeplocate)
32
+ - [StrictKeyAccess](#strictkeyaccess)
33
+ - [Mash](#mash)
34
+ - [KeepOriginalKeys](#keeporiginalkeys)
35
+ - [PermissiveRespondTo](#permissiverespondto)
36
+ - [SafeAssignment](#safeassignment)
37
+ - [SymbolizeKeys](#symbolizekeys)
38
+ - [DefineAccessors](#defineaccessors)
39
+ - [Dash](#dash)
40
+ - [Potential Gotchas](#potential-gotchas)
41
+ - [PropertyTranslation](#propertytranslation)
42
+ - [Mash and Rails 4 Strong Parameters](#mash-and-rails-4-strong-parameters)
43
+ - [Coercion](#coercion-1)
44
+ - [PredefinedValues](#predefinedvalues)
45
+ - [Trash](#trash)
46
+ - [Clash](#clash)
47
+ - [Rash](#rash)
48
+ - [Auto-Optimized](#auto-optimized)
49
+ - [Mascot](#mascot)
50
+ - [Contributing](#contributing)
51
+ - [Copyright](#copyright)
10
52
 
11
53
  ## Installation
12
54
 
@@ -18,7 +60,7 @@ $ gem install hashie
18
60
 
19
61
  ## Stable Release
20
62
 
21
- You're reading the documentation for the stable release of Hashie, 4.1.0.(https://github.com/hashie/hashie/blob/v4.1.0/README.md).
63
+ You're reading the documentation for the stable release of Hashie, 5.1.0.
22
64
 
23
65
  ## Hash Extensions
24
66
 
@@ -190,13 +232,13 @@ end
190
232
 
191
233
  ### KeyConversion
192
234
 
193
- The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or [`Hashie::Extensions::SymbolizeKeys`](#mash-extension-symbolizekeys).
235
+ The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or `Hashie::Extensions::SymbolizeKeys`.
194
236
 
195
237
  Hashie also has a utility method for converting keys on a Hash without a mixin:
196
238
 
197
239
  ```ruby
198
- Hashie.symbolize_keys! hash # => Symbolizes keys of hash.
199
- Hashie.symbolize_keys hash # => Returns a copy of hash with keys symbolized.
240
+ Hashie.symbolize_keys! hash # => Symbolizes all string keys of hash.
241
+ Hashie.symbolize_keys hash # => Returns a copy of hash with string keys symbolized.
200
242
  Hashie.stringify_keys! hash # => Stringifies keys of hash.
201
243
  Hashie.stringify_keys hash # => Returns a copy of hash with keys stringified.
202
244
  ```
@@ -269,8 +311,6 @@ This extension can be mixed in to your Hash subclass to allow you to use Strings
269
311
 
270
312
  In addition, IndifferentAccess will also inject itself into sub-hashes so they behave the same.
271
313
 
272
- Example:
273
-
274
314
  ```ruby
275
315
  class MyHash < Hash
276
316
  include Hashie::Extensions::MergeInitializer
@@ -290,6 +330,18 @@ myhash['fishes'][:food] = 'flakes'
290
330
  myhash['fishes']['food'] # => "flakes"
291
331
  ```
292
332
 
333
+ To get back a normal, not-indifferent Hash, you can use `#to_hash` on the indifferent hash. It exports the keys as strings, not symbols:
334
+
335
+ ```ruby
336
+ myhash = MyHash.new
337
+ myhash["foo"] = "bar"
338
+ myhash[:foo] #=> "bar"
339
+
340
+ normal_hash = myhash.to_hash
341
+ myhash["foo"] #=> "bar"
342
+ myhash[:foo] #=> nil
343
+ ```
344
+
293
345
  ### IgnoreUndeclared
294
346
 
295
347
  This extension can be mixed in to silently ignore undeclared properties on initialization instead of raising an error. This is useful when using a Trash to capture a subset of a larger hash.
@@ -445,8 +497,6 @@ books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
445
497
 
446
498
  This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key.
447
499
 
448
- ### Example:
449
-
450
500
  ```ruby
451
501
  class StrictKeyAccessHash < Hash
452
502
  include Hashie::Extensions::StrictKeyAccess
@@ -464,8 +514,6 @@ end
464
514
 
465
515
  Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality.
466
516
 
467
- ### Example:
468
-
469
517
  ```ruby
470
518
  mash = Hashie::Mash.new
471
519
  mash.name? # => false
@@ -488,12 +536,10 @@ mash.inspect # => <Hashie::Mash>
488
536
 
489
537
  **Note:** The `?` method will return false if a key has been set to false or nil. In order to check if a key has been set at all, use the `mash.key?('some_key')` method instead.
490
538
 
491
- ### How does Mash handle conflicts with pre-existing methods?
539
+ _How does Mash handle conflicts with pre-existing methods?_
492
540
 
493
541
  Please note that a Mash will not override methods through the use of the property-like syntax. This can lead to confusion if you expect to be able to access a Mash value through the property-like syntax for a key that conflicts with a method name. However, it protects users of your library from the unexpected behavior of those methods being overridden behind the scenes.
494
542
 
495
- #### Example:
496
-
497
543
  ```ruby
498
544
  mash = Hashie::Mash.new
499
545
  mash.name = "My Mash"
@@ -564,12 +610,10 @@ Hashie::Mash.quiet.new(zip: '90210', compact: true) # no errors logged
564
610
  Hashie::Mash.quiet(:zip).new(zip: '90210', compact: true) # error logged for compact
565
611
  ```
566
612
 
567
- ### How does the wrapping of Mash sub-Hashes work?
613
+ _How does the wrapping of Mash sub-Hashes work?_
568
614
 
569
615
  Mash duplicates any sub-Hashes that you add to it and wraps them in a Mash. This allows for infinite chaining of nested Hashes within a Mash without modifying the object(s) that are passed into the Mash. When you subclass Mash, the subclass wraps any sub-Hashes in its own class. This preserves any extensions that you mixed into the Mash subclass and allows them to work within the sub-Hashes, in addition to the main containing Mash.
570
616
 
571
- #### Example:
572
-
573
617
  ```ruby
574
618
  mash = Hashie::Mash.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" })
575
619
  mash.dependencies.class #=> Hashie::Mash
@@ -579,11 +623,20 @@ my_gem = MyGem.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0"
579
623
  my_gem.dependencies.class #=> MyGem
580
624
  ```
581
625
 
582
- ### What else can Mash do?
626
+ _How does Mash handle key types which cannot be symbolized?_
583
627
 
584
- Mash allows you also to transform any files into a Mash objects.
628
+ Mash preserves keys which cannot be converted *directly* to both a string and a symbol, such as numeric keys. Since Mash is conceived to provide pseudo-object functionality, handling keys which cannot represent a method call falls outside its scope of value.
629
+
630
+ ```ruby
631
+ Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num')
632
+ # => {"1"=>"one sym", 1=>"one num"}
633
+ ```
634
+
635
+ The symbol key `:'1'` is converted the string `'1'` to support indifferent access and consequently its value `'one sym'` will override the previously set `'one string'`. However, the subsequent key of `1` cannot directly convert to a symbol and therefore **not** converted to the string `'1'` that would otherwise override the previously set value of `'one sym'`.
585
636
 
586
- #### Example:
637
+ _What else can Mash do?_
638
+
639
+ Mash allows you also to transform any files into a Mash objects.
587
640
 
588
641
  ```yml
589
642
  #/etc/config/settings/twitter.yml
@@ -639,9 +692,9 @@ Specify `permitted_symbols`, `permitted_classes` and `aliases` options as needed
639
692
  Mash.load('data/user.csv', permitted_classes: [Symbol], permitted_symbols: [], aliases: false)
640
693
  ```
641
694
 
642
- ### Mash Extension: KeepOriginalKeys
695
+ ### KeepOriginalKeys
643
696
 
644
- This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.
697
+ This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts symbol keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.
645
698
 
646
699
  ```ruby
647
700
  class KeepingMash < ::Hashie::Mash
@@ -658,7 +711,7 @@ mash['string_key'] #=> 'string'
658
711
  mash[:string_key] #=> 'string'
659
712
  ```
660
713
 
661
- ### Mash Extension: PermissiveRespondTo
714
+ ### PermissiveRespondTo
662
715
 
663
716
  By default, Mash only states that it responds to built-in methods, affixed methods (e.g. setters, underbangs, etc.), and keys that it currently contains. That means it won't state that it responds to a getter for an unset key, as in the following example:
664
717
 
@@ -682,12 +735,10 @@ mash.respond_to? :b #=> true
682
735
 
683
736
  This comes at the cost of approximately 20% performance for initialization and setters and 19KB of permanent memory growth for each such class that you create.
684
737
 
685
- ### Mash Extension: SafeAssignment
738
+ ### SafeAssignment
686
739
 
687
740
  This extension can be mixed into a Mash to guard the attempted overwriting of methods by property setters. When mixed in, the Mash will raise an `ArgumentError` if you attempt to write a property with the same name as an existing method.
688
741
 
689
- #### Example:
690
-
691
742
  ```ruby
692
743
  class SafeMash < ::Hashie::Mash
693
744
  include Hashie::Extensions::Mash::SafeAssignment
@@ -698,9 +749,9 @@ safe_mash.zip = 'Test' # => ArgumentError
698
749
  safe_mash[:zip] = 'test' # => still ArgumentError
699
750
  ```
700
751
 
701
- ### Mash Extension: SymbolizeKeys
752
+ ### SymbolizeKeys
702
753
 
703
- This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all keys to symbols. It can be useful to use with keywords argument, which required symbol keys.
754
+ This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all string keys to symbols. It can be useful to use with keywords argument, which required symbol keys.
704
755
 
705
756
  ```ruby
706
757
  class SymbolizedMash < ::Hashie::Mash
@@ -731,7 +782,7 @@ end
731
782
 
732
783
  However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input.
733
784
 
734
- ### Mash Extension: DefineAccessors
785
+ ### DefineAccessors
735
786
 
736
787
  This extension can be mixed into a Mash so it makes it behave like `OpenStruct`. It reduces the overhead of `method_missing?` magic by lazily defining field accessors when they're requested.
737
788
 
@@ -758,12 +809,10 @@ mash = ::Hashie::Mash.new.with_accessors!
758
809
 
759
810
  ## Dash
760
811
 
761
- Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property.
812
+ Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property. A property with a proc value will be evaluated lazily upon retrieval.
762
813
 
763
814
  You can also conditionally require certain properties by passing a Proc or Symbol. If a Proc is provided, it will be run in the context of the Dash instance. If a Symbol is provided, the value returned for the property or method of the same name will be evaluated. The property will be required if the result of the conditional is truthy.
764
815
 
765
- ### Example:
766
-
767
816
  ```ruby
768
817
  class Person < Hashie::Dash
769
818
  property :name, required: true
@@ -772,6 +821,7 @@ class Person < Hashie::Dash
772
821
  property :phone, required: -> { email.nil? }, message: 'is required if email is not set.'
773
822
  property :pants, required: :weekday?, message: 'are only required on weekdays.'
774
823
  property :occupation, default: 'Rubyist'
824
+ property :genome
775
825
 
776
826
  def weekday?
777
827
  [ Time.now.saturday?, Time.now.sunday? ].none?
@@ -796,12 +846,12 @@ p.occupation # => 'Evil'
796
846
  p.name # => 'Trudy'
797
847
  p.update_attributes!(occupation: nil)
798
848
  p.occupation # => 'Rubyist'
849
+ p.genome = -> { Genome.sequence } # Some expensive operation
850
+ p.genome # => 'GATTACA'
799
851
  ```
800
852
 
801
853
  Properties defined as symbols are not the same thing as properties defined as strings.
802
854
 
803
- ### Example:
804
-
805
855
  ```ruby
806
856
  class Tricky < Hashie::Dash
807
857
  property :trick
@@ -825,7 +875,28 @@ p = Tricky.new('trick' => 'two')
825
875
  p.trick # => NoMethodError
826
876
  ```
827
877
 
828
- ### Potential gotchas
878
+ If you would like to update a Dash and use any default values set in the case of a `nil` value, use `#update_attributes!`.
879
+
880
+ ```ruby
881
+ class WithDefaults < Hashie::Dash
882
+ property :description, default: 'none'
883
+ end
884
+
885
+ dash = WithDefaults.new
886
+ dash.description #=> 'none'
887
+
888
+ dash.description = 'You committed one of the classic blunders!'
889
+ dash.description #=> 'You committed one of the classic blunders!'
890
+
891
+ dash.description = nil
892
+ dash.description #=> nil
893
+
894
+ dash.description = 'Only slightly less known is ...'
895
+ dash.update_attributes!(description: nil)
896
+ dash.description #=> 'none'
897
+ ```
898
+
899
+ ### Potential Gotchas
829
900
 
830
901
  Because Dashes are subclasses of the built-in Ruby Hash class, the double-splat operator takes the Dash as-is without any conversion. This can lead to strange behavior when you use the double-splat operator on a Dash as the first part of a keyword list or built Hash. For example:
831
902
 
@@ -857,13 +928,11 @@ qux.is_a?(Hash) #=> true
857
928
  qux[:quux] #=> "corge"
858
929
  ```
859
930
 
860
- ### Dash Extension: PropertyTranslation
931
+ ### PropertyTranslation
861
932
 
862
933
  The `Hashie::Extensions::Dash::PropertyTranslation` mixin extends a Dash with
863
934
  the ability to remap keys from a source hash.
864
935
 
865
- ### Example from inconsistent APIs
866
-
867
936
  Property translation is useful when you need to read data from another
868
937
  application -- such as a Java API -- where the keys are named differently from
869
938
  Ruby conventions.
@@ -883,8 +952,6 @@ person[:first_name] #=> 'Michael'
883
952
  person[:last_name] #=> 'Bleigh
884
953
  ```
885
954
 
886
- ### Example using translation lambdas
887
-
888
955
  You can also use a lambda to translate the value. This is particularly useful
889
956
  when you want to ensure the type of data you're wrapping.
890
957
 
@@ -905,7 +972,7 @@ model.created_at.class #=> Time
905
972
 
906
973
  To enable compatibility with Rails 4 use the [hashie-forbidden_attributes](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes) gem.
907
974
 
908
- ### Dash Extension: Coercion.
975
+ ### Coercion
909
976
 
910
977
  If you want to use `Hashie::Extensions::Coercion` together with `Dash` then
911
978
  you may probably want to use `Hashie::Extensions::Dash::Coercion` instead.
@@ -935,6 +1002,20 @@ class UserHash < Hashie::Dash
935
1002
  end
936
1003
  ```
937
1004
 
1005
+ ### PredefinedValues
1006
+
1007
+ The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with
1008
+ the ability to accept predefined values on a property.
1009
+
1010
+ ```ruby
1011
+ class UserHash < Hashie::Dash
1012
+ include Hashie::Extensions::Dash::PredefinedValues
1013
+
1014
+ property :gender, values: %i[male female prefer_not_to_say]
1015
+ property :age, values: (0..150)
1016
+ end
1017
+ ```
1018
+
938
1019
  ## Trash
939
1020
 
940
1021
  A Trash is a Dash that allows you to translate keys on initialization. It mixes
@@ -976,8 +1057,6 @@ Clash is a Chainable Lazy Hash that allows you to easily construct complex hashe
976
1057
 
977
1058
  Essentially, a Clash is a generalized way to provide much of the same kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes provide.
978
1059
 
979
- ### Example:
980
-
981
1060
  ```ruby
982
1061
  c = Hashie::Clash.new
983
1062
  c.where(abc: 'def').order(:created_at)
@@ -1003,8 +1082,6 @@ A good use case for the Rash is an URL router for a web framework, where URLs ne
1003
1082
 
1004
1083
  If the Rash's value is a `proc`, the `proc` will be automatically called with the regexp's MatchData (matched groups) as a block argument.
1005
1084
 
1006
- ### Example:
1007
-
1008
1085
  ```ruby
1009
1086
 
1010
1087
  # Mapping names to appropriate greetings
@@ -1021,18 +1098,21 @@ mapper["I like traffic lights"] # => "Who DOESN'T like traffic lights?!"
1021
1098
  mapper["Get off my lawn!"] # => "Forget your lawn, old man!"
1022
1099
  ```
1023
1100
 
1024
- ### Auto-optimized
1101
+ ### Auto-Optimized
1025
1102
 
1026
1103
  **Note:** The Rash is automatically optimized every 500 accesses (which means that it sorts the list of Regexps, putting the most frequently matched ones at the beginning).
1027
1104
 
1028
1105
  If this value is too low or too high for your needs, you can tune it by setting: `rash.optimize_every = n`.
1029
1106
 
1107
+ ## Mascot
1108
+ [![eierlegende Wollmilchsau](./mascot.svg)](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau) Meet Hashie's "official" mascot, the [eierlegende Wollmilchsau](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau)!
1109
+
1030
1110
  ## Contributing
1031
1111
 
1032
1112
  See [CONTRIBUTING.md](CONTRIBUTING.md)
1033
1113
 
1034
1114
  ## Copyright
1035
1115
 
1036
- Copyright (c) 2009-2014 Intridea, Inc. (http://intridea.com/) and [contributors](https://github.com/hashie/hashie/graphs/contributors).
1116
+ Copyright (c) 2009-2020 [Intridea, Inc.](http://intridea.com), and [contributors](https://github.com/hashie/hashie/graphs/contributors).
1037
1117
 
1038
1118
  MIT License. See [LICENSE](LICENSE) for details.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
  Bundler.setup
@@ -16,6 +18,7 @@ RuboCop::RakeTask.new(:rubocop)
16
18
  require_relative 'spec/support/integration_specs'
17
19
  task :integration_specs do
18
20
  next if ENV['CI']
21
+
19
22
  status_codes = []
20
23
  handler = lambda do |status_code|
21
24
  status_codes << status_code unless status_code.zero?
data/UPGRADING.md CHANGED
@@ -1,6 +1,44 @@
1
1
  Upgrading Hashie
2
2
  ================
3
3
 
4
+ ### Upgrading to 5.0.0
5
+
6
+ #### Mash initialization key conversion
7
+
8
+ Mash initialization now only converts to string keys which can be represented as symbols.
9
+
10
+ ```ruby
11
+ Hashie::Mash.new(
12
+ {foo: "bar"} => "baz",
13
+ "1" => "one string",
14
+ :"1" => "one sym",
15
+ 1 => "one num"
16
+ )
17
+
18
+ # Before
19
+ {"{:foo=>\"bar\"}"=>"baz", "1"=>"one num"}
20
+
21
+ # After
22
+ {{:foo=>"bar"}=>"baz", "1"=>"one sym", 1=>"one num"}
23
+ ```
24
+
25
+ #### Mash#dig with numeric keys
26
+
27
+ `Hashie::Mash#dig` no longer considers numeric keys for indifferent access.
28
+
29
+ ```ruby
30
+ my_mash = Hashie::Mash.new("1" => "a") # => {"1"=>"a"}
31
+
32
+ my_mash.dig("1") # => "a"
33
+ my_mash.dig(:"1") # => "a"
34
+
35
+ # Before
36
+ my_mash.dig(1) # => "a"
37
+
38
+ # After
39
+ my_mash.dig(1) # => nil
40
+ ```
41
+
4
42
  ### Upgrading to 4.0.0
5
43
 
6
44
  #### Non-destructive Hash methods called on Mash
data/hashie.gemspec CHANGED
@@ -24,5 +24,9 @@ Gem::Specification.new do |gem|
24
24
  }
25
25
  end
26
26
 
27
+ gem.required_ruby_version = '>= 2.7'
28
+
29
+ gem.add_dependency 'logger' # for Ruby >= 3.4
30
+
27
31
  gem.add_development_dependency 'bundler'
28
32
  end
data/lib/hashie/dash.rb CHANGED
@@ -10,7 +10,7 @@ module Hashie
10
10
  # lightweight data object that needs even fewer options and
11
11
  # resources than something like a DataMapper resource.
12
12
  #
13
- # It is preferrable to a Struct because of the in-class
13
+ # It is preferable to a Struct because of the in-class
14
14
  # API for defining properties as well as per-property defaults.
15
15
  class Dash < Hash
16
16
  include Hashie::Extensions::PrettyInspect
@@ -104,19 +104,6 @@ module Hashie
104
104
  def initialize(attributes = {}, &block)
105
105
  super(&block)
106
106
 
107
- self.class.defaults.each_pair do |prop, value|
108
- self[prop] = begin
109
- val = value.dup
110
- if val.is_a?(Proc)
111
- val.arity == 1 ? val.call(self) : val.call
112
- else
113
- val
114
- end
115
- rescue TypeError
116
- value
117
- end
118
- end
119
-
120
107
  initialize_attributes(attributes)
121
108
  assert_required_attributes_set!
122
109
  end
@@ -169,17 +156,30 @@ module Hashie
169
156
  self
170
157
  end
171
158
 
159
+ def to_h
160
+ defaults = ::Hash[self.class.properties.map { |prop| [prop, self.class.defaults[prop]] }]
161
+
162
+ defaults.merge(self)
163
+ end
164
+ alias to_hash to_h
165
+
172
166
  def update_attributes!(attributes)
173
167
  update_attributes(attributes)
174
168
 
175
169
  self.class.defaults.each_pair do |prop, value|
176
- next unless self[prop].nil?
170
+ next unless fetch(prop, nil).nil?
177
171
  self[prop] = begin
178
- value.dup
172
+ val = value.dup
173
+ if val.is_a?(Proc)
174
+ val.arity == 1 ? val.call(self) : val.call
175
+ else
176
+ val
177
+ end
179
178
  rescue TypeError
180
179
  value
181
180
  end
182
181
  end
182
+
183
183
  assert_required_attributes_set!
184
184
  end
185
185
 
@@ -189,7 +189,7 @@ module Hashie
189
189
  return unless attributes
190
190
 
191
191
  cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
192
- update_attributes(cleaned_attributes)
192
+ update_attributes!(cleaned_attributes)
193
193
  end
194
194
 
195
195
  def update_attributes(attributes)
@@ -17,14 +17,7 @@ module Hashie
17
17
  }.freeze
18
18
 
19
19
  ABSTRACT_CORE_TYPES =
20
- if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
21
- { Numeric => [Integer, Float, Complex, Rational] }
22
- else
23
- {
24
- Integer => [Fixnum, Bignum],
25
- Numeric => [Fixnum, Bignum, Float, Complex, Rational]
26
- }
27
- end
20
+ { Numeric => [Integer, Float, Complex, Rational] }
28
21
 
29
22
  def self.included(base)
30
23
  base.send :include, InstanceMethods
@@ -19,6 +19,15 @@ module Hashie
19
19
  end
20
20
  private_class_method :requires_class_methods?
21
21
 
22
+ def to_h
23
+ defaults = ::Hash[self.class.properties.map do |prop|
24
+ [Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]]
25
+ end]
26
+
27
+ defaults.merge(self)
28
+ end
29
+ alias to_hash to_h
30
+
22
31
  module ClassMethods
23
32
  # Check to see if the specified property has already been
24
33
  # defined.
@@ -0,0 +1,88 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Dash
4
+ # Extends a Dash with the ability to accept only predefined values on a property.
5
+ #
6
+ # == Example
7
+ #
8
+ # class PersonHash < Hashie::Dash
9
+ # include Hashie::Extensions::Dash::PredefinedValues
10
+ #
11
+ # property :gender, values: [:male, :female, :prefer_not_to_say]
12
+ # property :age, values: (0..150) # a Range
13
+ # end
14
+ #
15
+ # person = PersonHash.new(gender: :male, age: -1)
16
+ # # => ArgumentError: The value '-1' is not accepted for property 'age'
17
+ module PredefinedValues
18
+ def self.included(base)
19
+ base.instance_variable_set(:@values_for_properties, {})
20
+ base.extend(ClassMethods)
21
+ base.include(InstanceMethods)
22
+ end
23
+
24
+ module ClassMethods
25
+ attr_reader :values_for_properties
26
+
27
+ def inherited(klass)
28
+ super
29
+ klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
30
+ end
31
+
32
+ def property(property_name, options = {})
33
+ super
34
+
35
+ return unless (predefined_values = options[:values])
36
+
37
+ assert_predefined_values!(predefined_values)
38
+ set_predefined_values(property_name, predefined_values)
39
+ end
40
+
41
+ private
42
+
43
+ def assert_predefined_values!(predefined_values)
44
+ return if supported_type?(predefined_values)
45
+
46
+ raise ArgumentError, %(`values` accepts an Array or a Range.)
47
+ end
48
+
49
+ def supported_type?(predefined_values)
50
+ [::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) }
51
+ end
52
+
53
+ def set_predefined_values(property_name, predefined_values)
54
+ @values_for_properties[property_name] = predefined_values
55
+ end
56
+ end
57
+
58
+ module InstanceMethods
59
+ def initialize(*)
60
+ super
61
+
62
+ assert_property_values!
63
+ end
64
+
65
+ private
66
+
67
+ def assert_property_values!
68
+ self.class.values_for_properties.each_key do |property|
69
+ value = send(property)
70
+
71
+ if value && !values_for_properties(property).include?(value)
72
+ fail_property_value_error!(property)
73
+ end
74
+ end
75
+ end
76
+
77
+ def fail_property_value_error!(property)
78
+ raise ArgumentError, "Invalid value for property '#{property}'"
79
+ end
80
+
81
+ def values_for_properties(property)
82
+ self.class.values_for_properties[property]
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -153,7 +153,12 @@ module Hashie
153
153
  def []=(property, value)
154
154
  if self.class.translation_exists? property
155
155
  send("#{property}=", value)
156
- super(property, value) if self.class.properties.include?(property)
156
+
157
+ if self.class.transformation_exists? property
158
+ super property, self.class.transformed_property(property, value)
159
+ elsif self.class.properties.include?(property)
160
+ super(property, value)
161
+ end
157
162
  elsif self.class.transformation_exists? property
158
163
  super property, self.class.transformed_property(property, value)
159
164
  elsif property_exists? property
@@ -1,7 +1,7 @@
1
1
  module Hashie
2
2
  module Extensions
3
3
  module DeepLocate
4
- # The module level implementation of #deep_locate, incase you do not want
4
+ # The module level implementation of #deep_locate, in case you do not want
5
5
  # to include/extend the base datastructure. For further examples please
6
6
  # see #deep_locate.
7
7
  #