serega 0.39.0 → 0.40.1

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +164 -109
  4. data/VERSION +1 -1
  5. data/lib/serega/attribute.rb +2 -0
  6. data/lib/serega/attribute_normalizer.rb +61 -3
  7. data/lib/serega/attribute_value_resolvers/batch.rb +3 -0
  8. data/lib/serega/attribute_value_resolvers/const.rb +3 -0
  9. data/lib/serega/attribute_value_resolvers/delegate.rb +4 -0
  10. data/lib/serega/attribute_value_resolvers/hash_access.rb +122 -0
  11. data/lib/serega/attribute_value_resolvers/keyword.rb +3 -0
  12. data/lib/serega/config.rb +60 -1
  13. data/lib/serega/data_builder.rb +2 -0
  14. data/lib/serega/engine/level.rb +2 -0
  15. data/lib/serega/engine/level_queue.rb +2 -0
  16. data/lib/serega/engine/loader.rb +3 -0
  17. data/lib/serega/helpers/serializer_class_helper.rb +2 -0
  18. data/lib/serega/object_serializer.rb +2 -0
  19. data/lib/serega/plan.rb +3 -0
  20. data/lib/serega/plan_point.rb +2 -0
  21. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +22 -16
  22. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +6 -0
  23. data/lib/serega/plugins/camel_case/camel_case.rb +13 -13
  24. data/lib/serega/plugins/context_metadata/context_metadata.rb +8 -1
  25. data/lib/serega/plugins/depth_limit/depth_limit.rb +15 -12
  26. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +12 -10
  27. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +1 -0
  28. data/lib/serega/plugins/formatters/formatters.rb +18 -10
  29. data/lib/serega/plugins/if/if.rb +29 -32
  30. data/lib/serega/plugins/if/validations/check_opt_if.rb +1 -0
  31. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +1 -0
  32. data/lib/serega/plugins/if/validations/check_opt_unless.rb +1 -0
  33. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +1 -0
  34. data/lib/serega/plugins/metadata/meta_attribute.rb +2 -0
  35. data/lib/serega/plugins/metadata/metadata.rb +14 -12
  36. data/lib/serega/plugins/metadata/validations/check_block.rb +1 -0
  37. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +1 -0
  38. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +1 -0
  39. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +1 -0
  40. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +1 -0
  41. data/lib/serega/plugins/metadata/validations/check_opts.rb +1 -0
  42. data/lib/serega/plugins/metadata/validations/check_path.rb +1 -0
  43. data/lib/serega/plugins/presenter/presenter.rb +14 -7
  44. data/lib/serega/plugins/root/root.rb +10 -6
  45. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +1 -0
  46. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +21 -0
  47. data/lib/serega/plugins.rb +2 -0
  48. data/lib/serega/utils/collection_detector.rb +2 -0
  49. data/lib/serega/utils/enum_deep_dup.rb +2 -0
  50. data/lib/serega/utils/enum_deep_freeze.rb +2 -0
  51. data/lib/serega/utils/method_signature.rb +2 -0
  52. data/lib/serega/utils/serialized_attribute_error.rb +2 -0
  53. data/lib/serega/utils/symbol_name.rb +2 -0
  54. data/lib/serega/utils/to_hash.rb +2 -0
  55. data/lib/serega/validations/attribute/check_block.rb +3 -0
  56. data/lib/serega/validations/attribute/check_name.rb +3 -0
  57. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -0
  58. data/lib/serega/validations/attribute/check_opt_batch.rb +3 -0
  59. data/lib/serega/validations/attribute/check_opt_const.rb +3 -0
  60. data/lib/serega/validations/attribute/check_opt_delegate.rb +45 -1
  61. data/lib/serega/validations/attribute/check_opt_hash_access.rb +81 -0
  62. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -0
  63. data/lib/serega/validations/attribute/check_opt_many.rb +3 -0
  64. data/lib/serega/validations/attribute/check_opt_method.rb +3 -0
  65. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -0
  66. data/lib/serega/validations/attribute/check_opt_serializer.rb +3 -0
  67. data/lib/serega/validations/attribute/check_opt_value.rb +3 -0
  68. data/lib/serega/validations/check_attribute_params.rb +4 -0
  69. data/lib/serega/validations/check_batch_loader_params.rb +3 -0
  70. data/lib/serega/validations/check_initiate_params.rb +3 -0
  71. data/lib/serega/validations/check_serialize_params.rb +3 -0
  72. data/lib/serega/validations/initiate/check_modifiers.rb +3 -0
  73. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -0
  74. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -0
  75. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -0
  76. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -0
  77. data/lib/serega.rb +8 -0
  78. metadata +15 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a223242ae8bce14755fa2cbb444ff78728d95fccb0ce3077c212706e39e4ddcc
4
- data.tar.gz: b8e1d3bdf11a57091cfd22eb31bb017979382df4b2166b05f746d064dc486f6e
3
+ metadata.gz: edecae0c35540be687e394092e3b0a158fa1c2aeff4710ff5ce8e17effba1eb2
4
+ data.tar.gz: 7e63d238aad501f31be388824bb6b594e9f7988e17c49e4fc478d9353993c63b
5
5
  SHA512:
6
- metadata.gz: d815a4a17625533c34659451851c33806bd4d1e0bf862945b2118d52315726184322c55d989378cf8ac7f9b4d04a392dfa344af196491b984ddc1d094d15c063
7
- data.tar.gz: ecf2be1cff661bfe8e99103748276969f72442b04b8308dd136d137dc88f28dfb543ad73a5e0880a31eb6f6c4a6a51e6ccab307a92f867551ff4b19b56d38f73
6
+ metadata.gz: 506607362308145ffcc0a34474d76d8b0a0edb7fbb570deb1fb973c71c396aab65a1b7b408069d31c8a5e15f8c8146316f835194c53e4a68315fe62eb4992421
7
+ data.tar.gz: 6d5b5ebe921a36b197b4829721af89eb92f5e47522101d126cffc8c72e30ad2679a6f9084df2685cf2250d772c5b0974b76861260b2eb19c6f53660fba16f5d4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Andrey Glushkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -4,7 +4,8 @@
4
4
  [![GitHub Actions][build-badge]][build]
5
5
 
6
6
  The Serega Ruby Serializer provides easy and powerful DSL to describe your
7
- objects and serialize them to Hash or JSON.
7
+ objects and serialize them to Hash, ready for further conversion to JSON or
8
+ other formats.
8
9
 
9
10
  ---
10
11
 
@@ -26,6 +27,7 @@ It has some great features:
26
27
  keeping the code dry
27
28
  - Conditional attributes - ([if][if] plugin)
28
29
  - Auto camelCase keys - [camel_case][camel_case] plugin
30
+ - Serializing Hash records - [hash_access][hash_access] attribute option
29
31
 
30
32
  ## Installation
31
33
 
@@ -74,8 +76,9 @@ class UserSerializer < Serega
74
76
  # Regular attribute
75
77
  attribute :first_name
76
78
 
77
- # Option :method specifies the method that must be called on the
78
- # serialized object
79
+ # Option :method specifies the name used to read the attribute's value —
80
+ # a method called on the serialized object by default, or a Hash key when
81
+ # combined with :hash_access
79
82
  attribute :first_name, method: :old_first_name
80
83
 
81
84
  # Attribute blocks below require a base serializer for nested serializers.
@@ -494,6 +497,12 @@ class AppSerializer < Serega
494
497
  # proc { |object, batches:| batches[:counter][object.id] }
495
498
  config.batch_id_option = :id
496
499
 
500
+ # Defaults for the `hash_access:` attribute option — `default_mode` is
501
+ # what `hash_access: true` (and a Hash form omitting :mode) resolves to.
502
+ # See "Serializing Hash records".
503
+ config.hash_access.default_mode = :symbol # the default
504
+ config.hash_access.default_allow_missing_key = false # the default
505
+
497
506
  # Parent class for nested serializers defined with attribute blocks.
498
507
  # Usually a settings-only serializer, e.g. `config.base_serializer = self`
499
508
  # in an application base serializer class. There is no default — an
@@ -664,6 +673,69 @@ end
664
673
 
665
674
  ---
666
675
 
676
+ ## Serializing Hash records
677
+
678
+ (objects should implement `#[]`, `#fetch`, and `#key?` - Hash already has them)
679
+
680
+ The `hash_access: <mode>` attribute option makes an attribute read its value from
681
+ Hash keys instead of calling a method.
682
+
683
+ Allowed `<mode>`:
684
+
685
+ - `:symbol` reads record[:name]
686
+ - `:string` reads record["name"]
687
+ - `true` shorthand for `config.hash_access.default_mode` (`:symbol` by default)
688
+
689
+ By default missing keys will raise `KeyError`.
690
+
691
+ `config.hash_access.default_allow_missing_key = true` will change `KeyError`
692
+ to return `nil` value instead. `config.hash_access.default_mode = <mode>`
693
+ changes what `true` (and a Hash form omitting `:mode`) resolves to.
694
+
695
+ ```ruby
696
+ class UserSerializer < Serega
697
+ config.hash_access.default_mode = :symbol # the default
698
+ config.hash_access.default_allow_missing_key = true # false by default
699
+
700
+ attribute :name, hash_access: true # reads record[:name] (config.hash_access.default_mode)
701
+ attribute :name, hash_access: :symbol # reads record[:name]
702
+ attribute :name, hash_access: :string # reads record["name"]
703
+
704
+ # Use long form if you need to override `allow_missing_key`
705
+ attribute :name, hash_access: { allow_missing_key: false }
706
+ attribute :name, hash_access: { mode: :symbol, allow_missing_key: false }
707
+ attribute :name, hash_access: { mode: :string, allow_missing_key: false }
708
+ end
709
+ ```
710
+
711
+ Delegated attributes configure hash access **per step**:
712
+
713
+ - `to_hash_access` configures reading `<mode>` for intermediate object
714
+ - `hash_access` configures reading `<mode>` for final key
715
+
716
+ ```ruby
717
+ # reads `record.address.city` (no hash access)
718
+ attribute :city, delegate: { to: :address } # record.address.city
719
+
720
+ # reads record.address[:city]
721
+ attribute :city, delegate: { to: :address, hash_access: :symbol }
722
+
723
+ # reads record[:address].city
724
+ attribute :city, delegate: { to: :address, to_hash_access: :symbol }
725
+
726
+ # reads record[:address][:city]
727
+ attribute :city, delegate: { to: :address, to_hash_access: :symbol, hash_access: :symbol }
728
+
729
+ # reads record["address"] and if exists returns record["address"][:city]
730
+ attribute :city,
731
+ delegate: {
732
+ to: :address,
733
+ allow_nil: true,
734
+ to_hash_access: { mode: :string, allow_missing_key: true },
735
+ hash_access: { mode: :symbol, allow_missing_key: false }
736
+ }
737
+ ```
738
+
667
739
  ## Plugins
668
740
 
669
741
  ### Plugin :activerecord_preloads
@@ -678,31 +750,38 @@ handler; to use a different ORM, register your own instead.
678
750
  ```ruby
679
751
  class AppSerializer < Serega
680
752
  config.auto_preload = true
681
- config.hide_by_default = false
682
-
683
753
  plugin :activerecord_preloads
684
754
  end
685
755
 
756
+ class AlbumSerializer < AppSerializer
757
+ # no preloads
758
+ attribute :title
759
+
760
+ # preloads :downloads, as manually specified
761
+ attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
762
+ end
763
+
686
764
  class UserSerializer < AppSerializer
765
+ # no preloads
687
766
  attribute :username
767
+
768
+ # preloads :user_stats, as auto_preload is enabled for :delegate attributes
688
769
  attribute :comments_count, delegate: { to: :user_stats }
689
- attribute :albums, serializer: AlbumSerializer
690
- end
691
770
 
692
- class AlbumSerializer < AppSerializer
693
- attribute :title
694
- attribute :downloads_count, preload: :downloads,
695
- value: proc { |album| album.downloads.count }
771
+ # preloads :albums, as auto_preload is enabled for :serializer attributes
772
+ attribute :albums, serializer: AlbumSerializer
696
773
  end
697
774
 
698
- UserSerializer.to_h(user)
699
- # preloads :user_stats and :albums on the user; the AlbumSerializer level
700
- # preloads :downloads on the albums
775
+ UserSerializer.to_h(users)
776
+ # 1 query to load :user_stats for all users
777
+ # + 1 query to load :albums for all users
778
+ # + 1 query to load :downloads for all albums
779
+ # = 3 queries total, regardless of how many users/albums are serialized
701
780
  ```
702
781
 
703
782
  ### Plugin :root
704
783
 
705
- Allows to add root key to your serialized data
784
+ Adds a root key to serialized data.
706
785
 
707
786
  Accepts options:
708
787
 
@@ -715,56 +794,47 @@ Adds additional config options:
715
794
  - config.root.one
716
795
  - config.root.many
717
796
  - config.root.one=
718
- - config.root_many=
797
+ - config.root.many=
719
798
 
720
799
  The default root is `:data`.
721
800
 
722
- The root key can be changed per serialization.
723
-
724
801
  ```ruby
725
- # @example Change root per serialization:
802
+ class UserSerializer < Serega
803
+ plugin :root # default root is :data
804
+ end
726
805
 
727
- class UserSerializer < Serega
728
- plugin :root
729
- end
806
+ class UserSerializer < Serega
807
+ plugin :root, root: :users
808
+ end
730
809
 
731
- UserSerializer.to_h(nil) # => {:data=>nil}
732
- UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
733
- UserSerializer.to_h(nil, root: nil) # => nil
734
- ```
810
+ class UserSerializer < Serega
811
+ plugin :root, root_one: :user, root_many: :people
812
+ end
735
813
 
736
- The root key can be removed for all responses by providing the `root: nil`
737
- plugin option.
814
+ class UserSerializer < Serega
815
+ plugin :root, root: nil # no root key by default
816
+ end
817
+ ```
738
818
 
739
- In this case, no root key will be added. But it still can be added manually.
819
+ The root key can also be changed per serialization, or removed entirely by
820
+ providing `root: nil` (it can still be added back per serialization).
740
821
 
741
822
  ```ruby
742
- #@example Define :root plugin with different options
743
-
744
- class UserSerializer < Serega
745
- plugin :root # default root is :data
746
- end
747
-
748
- class UserSerializer < Serega
749
- plugin :root, root: :users
750
- end
751
-
752
- class UserSerializer < Serega
753
- plugin :root, root_one: :user, root_many: :people
754
- end
823
+ class UserSerializer < Serega
824
+ plugin :root
825
+ end
755
826
 
756
- class UserSerializer < Serega
757
- plugin :root, root: nil # no root key by default
758
- end
827
+ UserSerializer.to_h(nil) # => {:data=>nil}
828
+ UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
829
+ UserSerializer.to_h(nil, root: nil) # => nil
759
830
  ```
760
831
 
761
832
  ### Plugin :metadata
762
833
 
763
834
  Depends on: [`:root`][root] plugin, that must be loaded first
764
835
 
765
- Adds ability to describe metadata and adds it to serialized response
766
-
767
- Adds class-level `.meta_attribute` method. It accepts:
836
+ Adds metadata to the serialized response via the class-level `meta_attribute`
837
+ method, which accepts:
768
838
 
769
839
  - `*path` [Array of Symbols] - nested hash keys.
770
840
  - `**options` [Hash]
@@ -804,7 +874,8 @@ AppSerializer.to_h(nil)
804
874
 
805
875
  Depends on: [`:root`][root] plugin, that must be loaded first
806
876
 
807
- Allows to provide metadata and attach it to serialized response.
877
+ Adds metadata supplied per serialization call (as opposed to `:metadata`,
878
+ which is defined statically on the serializer).
808
879
 
809
880
  Accepts option `:context_metadata_key` with the name of the root metadata keyword.
810
881
  By default, it has the `:meta` value.
@@ -828,41 +899,39 @@ UserSerializer.to_h(nil, meta: { version: '1.0.1' })
828
899
 
829
900
  ### Plugin :formatters
830
901
 
831
- Allows to define `formatters` and apply them to attribute values.
832
-
833
- Config option `config.formatters.add` can be used to add formatters.
902
+ Defines named value formatters once and applies them to any attribute.
834
903
 
835
- Attribute option `:format` can be used with the name of formatter or with
836
- callable instance.
904
+ Use `config.formatters.add()` to register formatters. The `:format`
905
+ attribute option then accepts a formatter name or a callable directly.
837
906
 
838
- Formatters can accept up to 2 parameters (formatted object, context)
907
+ Formatters receive up to 2 parameters: the value and the context.
839
908
 
840
909
  ```ruby
841
910
  class AppSerializer < Serega
842
911
  plugin :formatters, formatters: {
843
- iso8601: ->(value) { time.iso8601.round(6) },
912
+ iso8601: ->(value) { value.iso8601 },
844
913
  on_off: ->(value) { value ? 'ON' : 'OFF' },
845
- money: ->(value, ctx) { value / 10**ctx[:digits) }
914
+ money: ->(value) { value.round(2) },
846
915
  date: DateTypeFormatter # callable
847
916
  }
848
917
  end
849
918
 
850
919
  class UserSerializer < Serega
851
- # Additionally, we can add formatters via config in subclasses
920
+ # Additionally we can add formatters via config in subclasses
852
921
  config.formatters.add(
853
- iso8601: ->(value) { time.iso8601.round(6) },
922
+ iso8601: ->(value) { value.iso8601 },
854
923
  on_off: ->(value) { value ? 'ON' : 'OFF' },
855
924
  money: ->(value) { value.round(2) }
856
925
  )
857
926
 
858
927
  # Using predefined formatter
859
928
  attribute :commission, format: :money
860
- attribute :is_logined, format: :on_off
929
+ attribute :is_logged_in, format: :on_off
861
930
  attribute :created_at, format: :iso8601
862
931
  attribute :updated_at, format: :iso8601
863
932
 
864
933
  # Using `callable` formatter
865
- attribute :score_percent, format: PercentFormmatter # callable class
934
+ attribute :score_percent, format: PercentFormatter # callable class
866
935
  attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
867
936
  end
868
937
  ```
@@ -924,12 +993,11 @@ callables keep receiving the raw objects.
924
993
 
925
994
  ### Plugin :string_modifiers
926
995
 
927
- Allows to specify modifiers as strings.
996
+ Allows `:only`, `:except` and `:with` to be given as a single comma-separated
997
+ string, with nested attributes in parentheses. Useful for accepting a field
998
+ list straight from a query parameter.
928
999
 
929
- Serialized attributes must be split with `,` and nested attributes must be
930
- defined inside brackets `()`.
931
-
932
- Modifiers can still be provided the old way using nested hashes or arrays.
1000
+ Modifiers can still be provided the old way, as nested hashes or arrays.
933
1001
 
934
1002
  ```ruby
935
1003
  PostSerializer.plugin :string_modifiers
@@ -937,25 +1005,19 @@ PostSerializer.new(only: "id,user(id,username)").to_h(post)
937
1005
  PostSerializer.new(except: "user(username,email)").to_h(post)
938
1006
  PostSerializer.new(with: "user(email)").to_h(post)
939
1007
 
940
- # Modifiers can still be provided the old way using nested hashes or arrays.
941
1008
  PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
942
1009
  ```
943
1010
 
944
1011
  ### Plugin :if
945
1012
 
946
- Plugin adds `:if, :unless, :if_value, :unless_value` options to
947
- attributes so we can remove attributes from the response in various ways.
948
-
949
- Use `:if` and `:unless` when you want to hide attributes before finding
950
- attribute value, and use `:if_value` and `:unless_value` to hide attributes
951
- after getting the final value.
1013
+ Adds `:if`, `:unless`, `:if_value`, `:unless_value` attribute options to
1014
+ conditionally remove attributes from the response.
952
1015
 
953
- Options `:if` and `:unless` accept currently serialized object and context as
954
- parameters. Options `:if_value` and `:unless_value` accept already found
955
- serialized value and context as parameters.
956
-
957
- Options `:if_value` and `:unless_value` cannot be used with the `:serializer` option.
958
- Use `:if` and `:unless` in this case.
1016
+ `:if`/`:unless` receive the serialized object and context, and are checked
1017
+ before the attribute value is found. `:if_value`/`:unless_value` receive the
1018
+ already-found value and context, checked after. The latter two cannot be
1019
+ used with the `:serializer` option, since a relationship has no "serialized
1020
+ value" of its own use `:if`/`:unless` instead.
959
1021
 
960
1022
  See also a `:hide` option that is available without any plugins to hide
961
1023
  attribute without conditions.
@@ -987,24 +1049,18 @@ Look at [select serialized fields](#selecting-fields) for `:hide` usage examples
987
1049
 
988
1050
  ### Plugin :camel_case
989
1051
 
990
- By default, when we add an attribute like `attribute :first_name` it means:
991
-
992
- - adding a `:first_name` key to the resulting hash
993
- - adding a `#first_name` method call result as value
994
-
995
- But it's often desired to respond with *camelCased* keys.
996
- By default, this can be achieved by specifying the attribute name and method directly
997
- for each attribute: `attribute :firstName, method: first_name`
1052
+ Without this plugin, responding with *camelCased* keys means specifying the
1053
+ attribute name and method directly for each attribute:
1054
+ `attribute :firstName, method: first_name`
998
1055
 
999
- This plugin transforms all attribute names automatically.
1000
- We use a simple regular expression to replace `_x` with `X` for the whole string.
1001
- We make this transformation only once when the attribute is defined.
1056
+ This plugin camelCases every attribute name automatically, replacing `_x`
1057
+ with `X` throughout the string. The transformation runs once, when the
1058
+ attribute is defined, not on every serialization.
1002
1059
 
1003
- You can provide custom transformation when adding the plugin,
1004
- for example `plugin :camel_case, transform: ->(name) { name.camelize }`
1060
+ Provide a custom transformation when adding the plugin, for example
1061
+ `plugin :camel_case, transform: ->(name) { name.camelize }`
1005
1062
 
1006
- For any attribute camelCase-behavior can be skipped when
1007
- the `camel_case: false` attribute option provided.
1063
+ Skip camelCase for a single attribute with `camel_case: false`.
1008
1064
 
1009
1065
  This plugin transforms only attribute keys, without affecting the `root`,
1010
1066
  `metadata` and `context_metadata` plugins keys.
@@ -1069,27 +1125,25 @@ end
1069
1125
 
1070
1126
  ### Plugin :explicit_many_option
1071
1127
 
1072
- The plugin requires adding a `:many` option when adding relationships
1073
- (attributes with the `:serializer` option or a block defining a nested
1074
- serializer).
1075
-
1076
- Adding this plugin makes it clearer to find if some relationship is an array or
1077
- a single object.
1128
+ Requires the `:many` option on every relationship attribute (an attribute
1129
+ with the `:serializer` option or a block defining a nested serializer), so
1130
+ it's always explicit whether it returns one object or many.
1078
1131
 
1079
1132
  ```ruby
1080
- class BaseSerializer < Serega
1081
- plugin :explicit_many_option
1082
- end
1133
+ class BaseSerializer < Serega
1134
+ plugin :explicit_many_option
1135
+ config.base_serializer = self
1136
+ end
1137
+
1138
+ class PostSerializer < BaseSerializer
1139
+ attribute :text
1083
1140
 
1084
- class UserSerializer < BaseSerializer
1141
+ attribute :user, many: false do
1085
1142
  attribute :name
1086
1143
  end
1087
1144
 
1088
- class PostSerializer < BaseSerializer
1089
- attribute :text
1090
- attribute :user, serializer: UserSerializer, many: false
1091
- attribute :comments, serializer: PostSerializer, many: true
1092
- end
1145
+ attribute :comments, serializer: PostSerializer, many: true
1146
+ end
1093
1147
  ```
1094
1148
 
1095
1149
  ## Errors
@@ -1128,6 +1182,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
1128
1182
  [context_metadata]: #plugin-context_metadata
1129
1183
  [depth_limit]: #plugin-depth_limit
1130
1184
  [formatters]: #plugin-formatters
1185
+ [hash_access]: #serializing-hash-records
1131
1186
  [metadata]: #plugin-metadata
1132
1187
  [preloads]: #preloads
1133
1188
  [presenter]: #plugin-presenter
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.39.0
1
+ 0.40.1
@@ -4,10 +4,12 @@ class Serega
4
4
  #
5
5
  # Stores serialized attribute data
6
6
  #
7
+ # @private
7
8
  class SeregaAttribute
8
9
  #
9
10
  # Attribute instance methods
10
11
  #
12
+ # @private
11
13
  module AttributeInstanceMethods
12
14
  # Attribute initial params
13
15
  # @return [Hash] Attribute initial params
@@ -4,10 +4,12 @@ class Serega
4
4
  #
5
5
  # Prepares provided attribute options
6
6
  #
7
+ # @private
7
8
  class SeregaAttributeNormalizer
8
9
  #
9
10
  # AttributeNormalizer instance methods
10
11
  #
12
+ # @private
11
13
  module AttributeNormalizerInstanceMethods
12
14
  # Attribute initial params
13
15
  # @return [Hash] Attribute initial params
@@ -254,7 +256,10 @@ class Serega
254
256
  end
255
257
 
256
258
  def prepare_keyword_block
257
- AttributeValueResolvers::KeywordResolver.get(method_name)
259
+ mode, allow_nil = hash_access
260
+ return AttributeValueResolvers::KeywordResolver.get(method_name) unless mode
261
+
262
+ AttributeValueResolvers::HashAccessResolver.get(method_name, mode, allow_nil)
258
263
  end
259
264
 
260
265
  def prepare_batch_loader_block
@@ -291,15 +296,68 @@ class Serega
291
296
  init_opts.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
292
297
  end
293
298
 
299
+ # `delegate: {to_hash_access: ...}` configures hash access for the
300
+ # intermediate (:to) read, `delegate: {hash_access: ...}` for the
301
+ # final (:method) read. A step without its sub-option keeps a plain
302
+ # method read.
294
303
  def prepare_delegate_block
295
304
  delegate = init_opts[:delegate]
296
305
  return unless delegate
297
306
 
298
307
  key_method_name = delegate[:method] || method_name
299
308
  delegate_to = delegate[:to]
309
+ delegate_allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
310
+
311
+ to_access = delegate[:to_hash_access]
312
+ final_access = delegate[:hash_access]
313
+ unless to_access || final_access
314
+ return AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, delegate_allow_nil)
315
+ end
316
+
317
+ to_step = delegate_to_step(delegate_to, to_access)
318
+ final_step = delegate_final_step(key_method_name, final_access)
319
+ AttributeValueResolvers::HashAccessDelegateResolver.get(to_step, final_step, delegate_allow_nil)
320
+ end
321
+
322
+ def delegate_to_step(delegate_to, access)
323
+ return AttributeValueResolvers::Keyword.new(delegate_to) unless access
324
+
325
+ mode, allow_missing_key = parse_hash_access(access)
326
+ AttributeValueResolvers::HashAccessKeyword.new(delegate_to, mode, allow_missing_key)
327
+ end
328
+
329
+ def delegate_final_step(key_method_name, access)
330
+ return AttributeValueResolvers::Keyword.new(key_method_name) unless access
300
331
 
301
- allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
302
- AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, allow_nil)
332
+ mode, allow_missing_key = parse_hash_access(access)
333
+ AttributeValueResolvers::HashAccessKeyword.new(key_method_name, mode, allow_missing_key)
334
+ end
335
+
336
+ # Resolves the :hash_access option of plain attributes
337
+ # @return [Array(Symbol, Boolean), nil] mode and allow_missing_key pair,
338
+ # or nil when hash access is not enabled for this attribute
339
+ def hash_access
340
+ option = init_opts[:hash_access]
341
+ return unless option
342
+
343
+ parse_hash_access(option)
344
+ end
345
+
346
+ # Resolves a :hash_access value (`true`, a Symbol mode or a
347
+ # `{mode:, allow_missing_key:}` Hash) into a mode and allow_missing_key
348
+ # pair. `true` and a Hash omitting :mode resolve to
349
+ # `config.hash_access.default_mode`.
350
+ def parse_hash_access(option)
351
+ defaults = config.hash_access
352
+
353
+ case option
354
+ when true then [defaults.default_mode, defaults.default_allow_missing_key]
355
+ when Symbol then [option, defaults.default_allow_missing_key]
356
+ else
357
+ mode = option.fetch(:mode) { defaults.default_mode }
358
+ allow_missing_key = option.fetch(:allow_missing_key) { defaults.default_allow_missing_key }
359
+ [mode, allow_missing_key]
360
+ end
303
361
  end
304
362
 
305
363
  # Prepares preloads for this attribute.
@@ -4,10 +4,12 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
+ # @private
7
8
  module AttributeValueResolvers
8
9
  #
9
10
  # Builds value resolver class for attributes with :batch option
10
11
  #
12
+ # @private
11
13
  class BatchResolver
12
14
  #
13
15
  # Generates callable block to find attribute value when attribute with :batch
@@ -55,6 +57,7 @@ class Serega
55
57
  #
56
58
  # Builds value resolver class for attributes with :batch option
57
59
  #
60
+ # @private
58
61
  class Batch
59
62
  def initialize(loader_name, id_method)
60
63
  @loader_name = loader_name
@@ -4,10 +4,12 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
+ # @private
7
8
  module AttributeValueResolvers
8
9
  #
9
10
  # Builds value resolver class for attributes with :const option
10
11
  #
12
+ # @private
11
13
  class ConstResolver
12
14
  #
13
15
  # Creates resolver that returns constant value
@@ -23,6 +25,7 @@ class Serega
23
25
  #
24
26
  # Value resolver class for attributes with :const option
25
27
  #
28
+ # @private
26
29
  class Const
27
30
  def initialize(const_value)
28
31
  @const_value = const_value
@@ -4,10 +4,12 @@ class Serega
4
4
  #
5
5
  # Attribute value resolvers
6
6
  #
7
+ # @private
7
8
  module AttributeValueResolvers
8
9
  #
9
10
  # Builds value resolver class for attributes with :delegate option
10
11
  #
12
+ # @private
11
13
  class DelegateResolver
12
14
  #
13
15
  # Creates resolver that delegates method call to another object
@@ -25,6 +27,7 @@ class Serega
25
27
  #
26
28
  # Value resolver class for attributes with :delegate (with :allow_nil) option
27
29
  #
30
+ # @private
28
31
  class DelegateAllowNil
29
32
  def initialize(delegate_to, method_name)
30
33
  @delegate_to = delegate_to
@@ -49,6 +52,7 @@ class Serega
49
52
  #
50
53
  # Value resolver class for attributes with :delegate (without :allow_nil) option
51
54
  #
55
+ # @private
52
56
  class Delegate
53
57
  def initialize(delegate_to, method_name)
54
58
  @delegate_to = delegate_to