serega 0.42.0 → 0.50.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module If
6
- #
7
- # Validator for attribute :if_value option
8
- #
9
- # @private
10
- class CheckOptIfValue
11
- class << self
12
- #
13
- # Checks attribute :if_value option that must be [nil, Symbol, Proc, #call]
14
- #
15
- # @param opts [Hash] Attribute options
16
- #
17
- # @raise [SeregaError] Attribute validation error
18
- #
19
- # @return [void]
20
- #
21
- def call(opts)
22
- return unless opts.key?(:if_value)
23
-
24
- check_usage_with_other_params(opts)
25
- check_type(opts[:if_value])
26
- end
27
-
28
- private
29
-
30
- def check_usage_with_other_params(opts)
31
- raise SeregaError, "Option :if_value can not be used together with option :serializer" if opts.key?(:serializer)
32
- end
33
-
34
- def check_type(value)
35
- return if value.is_a?(Symbol)
36
- raise SeregaError, must_be_callable unless value.respond_to?(:call)
37
-
38
- signature = SeregaUtils::MethodSignature.call(value, pos_limit: 2, keyword_args: [:ctx])
39
- raise SeregaError, signature_error unless valid_signature?(signature)
40
- end
41
-
42
- def must_be_callable
43
- "Invalid attribute option :if_value. It must be a Symbol, a Proc or respond to :call"
44
- end
45
-
46
- def valid_signature?(signature)
47
- case signature
48
- when "0" # no parameters
49
- true
50
- when "1" # (value)
51
- true
52
- when "2" # (value, context)
53
- true
54
- when "1_ctx" # (value, :ctx)
55
- true
56
- when "2_ctx" # (value, context, :ctx)
57
- true
58
- else
59
- false
60
- end
61
- end
62
-
63
- def signature_error
64
- <<~ERROR.strip
65
- Invalid attribute option :if_value parameters, valid parameters signatures:
66
- - () # no parameters
67
- - (value) # one positional parameter
68
- - (value, context) # two positional parameters
69
- - (value, :ctx) # one positional parameter and :ctx keyword
70
- - (value, context, :ctx) # two positional parameters and :ctx keyword
71
- ERROR
72
- end
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module If
6
- #
7
- # Validator for attribute :unless option
8
- #
9
- # @private
10
- class CheckOptUnless
11
- class << self
12
- #
13
- # Checks attribute :unless option that must be [nil, Symbol, Proc, #call]
14
- #
15
- # @param opts [Hash] Attribute options
16
- #
17
- # @raise [SeregaError] Attribute validation error
18
- #
19
- # @return [void]
20
- #
21
- def call(opts)
22
- return unless opts.key?(:unless)
23
-
24
- check_type(opts[:unless])
25
- end
26
-
27
- private
28
-
29
- def check_type(value)
30
- return if value.is_a?(Symbol)
31
- raise SeregaError, must_be_callable unless value.respond_to?(:call)
32
-
33
- signature = SeregaUtils::MethodSignature.call(value, pos_limit: 2, keyword_args: [:ctx])
34
- raise SeregaError, signature_error unless valid_signature?(signature)
35
- end
36
-
37
- def must_be_callable
38
- "Invalid attribute option :unless. It must be a Symbol, a Proc or respond to :call"
39
- end
40
-
41
- def valid_signature?(signature)
42
- case signature
43
- when "0" # no parameters
44
- true
45
- when "1" # (object)
46
- true
47
- when "2" # (object, context)
48
- true
49
- when "1_ctx" # (object, :ctx)
50
- true
51
- when "2_ctx" # (object, context, :ctx)
52
- true
53
- else
54
- false
55
- end
56
- end
57
-
58
- def signature_error
59
- <<~ERROR.strip
60
- Invalid attribute option :unless parameters, valid parameters signatures:
61
- - () # no parameters
62
- - (object) # one positional parameter
63
- - (object, context) # two positional parameters
64
- - (object, :ctx) # one positional parameter and :ctx keyword
65
- - (object, context, :ctx) # two positional parameters and :ctx keyword
66
- ERROR
67
- end
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module If
6
- #
7
- # Validator for attribute :unless_value option
8
- #
9
- # @private
10
- class CheckOptUnlessValue
11
- class << self
12
- #
13
- # Checks attribute :unless_value option that must be [nil, Symbol, Proc, #call]
14
- #
15
- # @param opts [Hash] Attribute options
16
- #
17
- # @raise [SeregaError] Attribute validation error
18
- #
19
- # @return [void]
20
- #
21
- def call(opts)
22
- return unless opts.key?(:unless_value)
23
-
24
- check_usage_with_other_params(opts)
25
- check_type(opts[:unless_value])
26
- end
27
-
28
- private
29
-
30
- def check_usage_with_other_params(opts)
31
- raise SeregaError, "Option :unless_value can not be used together with option :serializer" if opts.key?(:serializer)
32
- end
33
-
34
- def check_type(value)
35
- return if value.is_a?(Symbol)
36
- raise SeregaError, must_be_callable unless value.respond_to?(:call)
37
-
38
- signature = SeregaUtils::MethodSignature.call(value, pos_limit: 2, keyword_args: [:ctx])
39
- raise SeregaError, signature_error unless valid_signature?(signature)
40
- end
41
-
42
- def must_be_callable
43
- "Invalid attribute option :unless_value. It must be a Symbol, a Proc or respond to :call"
44
- end
45
-
46
- def valid_signature?(signature)
47
- case signature
48
- when "0" # no parameters
49
- true
50
- when "1" # (value)
51
- true
52
- when "2" # (value, context)
53
- true
54
- when "1_ctx" # (value, :ctx)
55
- true
56
- when "2_ctx" # (value, context, :ctx)
57
- true
58
- else
59
- false
60
- end
61
- end
62
-
63
- def signature_error
64
- <<~ERROR.strip
65
- Invalid attribute option :unless_value parameters, valid parameters signatures:
66
- - () # no parameters
67
- - (value) # one positional parameter
68
- - (value, context) # two positional parameters
69
- - (value, :ctx) # one positional parameter and :ctx keyword
70
- - (value, context, :ctx) # two positional parameters and :ctx keyword
71
- ERROR
72
- end
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute block parameter
9
- #
10
- # @private
11
- class CheckBlock
12
- class << self
13
- #
14
- # Checks block provided with attribute
15
- # Block must have up to two arguments - object(s) and context.
16
- #
17
- # @example without arguments
18
- # metadata(:version) { CONSTANT_VERSION }
19
- #
20
- # @example with one argument
21
- # metadata(:paging) { |scope| { { page: scope.page, per_page: scope.per_page, total_count: scope.total_count } }
22
- #
23
- # @example with two arguments
24
- # metadata(:paging) { |scope, context| { { ... } if context[:pagy] }
25
- #
26
- # @param block [Proc] Block that returns serialized meta attribute value
27
- #
28
- # @raise [SeregaError] SeregaError that block has invalid arguments
29
- #
30
- # @return [void]
31
- #
32
- def call(block)
33
- signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [])
34
- raise SeregaError, block_error unless %w[0 1 2].include?(signature)
35
- end
36
-
37
- private
38
-
39
- def block_error
40
- "Block can have maximum two parameters (object(s), context)"
41
- end
42
- end
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # MetaAttribute `:const` option validator
9
- #
10
- # @private
11
- class CheckOptConst
12
- class << self
13
- #
14
- # Checks meta_attribute :const option
15
- #
16
- # @param opts [Hash] MetaAttribute options
17
- #
18
- # @raise [SeregaError] validation error
19
- #
20
- # @return [void]
21
- #
22
- def call(opts, block = nil)
23
- return unless opts.key?(:const)
24
-
25
- check_usage_with_other_params(opts, block)
26
- end
27
-
28
- private
29
-
30
- def check_usage_with_other_params(opts, block)
31
- raise SeregaError, "Option :const can not be used together with option :value" if opts.key?(:value)
32
- raise SeregaError, "Option :const can not be used together with block" if block
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute :hide_empty option
9
- #
10
- # @private
11
- class CheckOptHideEmpty
12
- class << self
13
- #
14
- # Checks attribute :hide_empty option
15
- #
16
- # @param opts [Hash] Attribute options
17
- #
18
- # @raise [SeregaError] SeregaError that option has invalid value
19
- #
20
- # @return [void]
21
- #
22
- def call(opts)
23
- return unless opts.key?(:hide_empty)
24
-
25
- value = opts[:hide_empty]
26
- return if value == true
27
-
28
- raise SeregaError, "Invalid option :hide_empty => #{value.inspect}. Must be true"
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute :hide_nil option
9
- #
10
- # @private
11
- class CheckOptHideNil
12
- class << self
13
- #
14
- # Checks attribute :hide_nil option
15
- #
16
- # @param opts [Hash] Attribute options
17
- #
18
- # @raise [SeregaError] SeregaError that option has invalid value
19
- #
20
- # @return [void]
21
- #
22
- def call(opts)
23
- return unless opts.key?(:hide_nil)
24
-
25
- value = opts[:hide_nil]
26
- return if value == true
27
-
28
- raise SeregaError, "Invalid option :hide_nil => #{value.inspect}. Must be true"
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute :value option
9
- #
10
- # @private
11
- class CheckOptValue
12
- class << self
13
- #
14
- # Checks attribute :value option
15
- #
16
- # @param opts [Hash] Attribute options
17
- #
18
- # @raise [SeregaError] SeregaError that option has invalid value
19
- #
20
- # @return [void]
21
- #
22
- def call(opts, block = nil)
23
- return unless opts.key?(:value)
24
-
25
- check_usage_with_other_params(opts, block)
26
-
27
- check_value(opts[:value])
28
- end
29
-
30
- private
31
-
32
- def check_usage_with_other_params(opts, block)
33
- raise SeregaError, "Option :value can not be used together with option :const" if opts.key?(:const)
34
- raise SeregaError, "Option :value can not be used together with block" if block
35
- end
36
-
37
- def check_value(value)
38
- check_value_type(value)
39
-
40
- signature = SeregaUtils::MethodSignature.call(value, pos_limit: 2, keyword_args: [])
41
-
42
- raise SeregaError, params_count_error unless %w[0 1 2].include?(signature)
43
- end
44
-
45
- def check_value_type(value)
46
- raise SeregaError, type_error if !value.is_a?(Proc) && !value.respond_to?(:call)
47
- end
48
-
49
- def type_error
50
- "Option :value value must be a Proc or respond to #call"
51
- end
52
-
53
- def params_count_error
54
- "Option :value value can have maximum 2 parameters (object(s), context)"
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute options
9
- #
10
- # @private
11
- class CheckOpts
12
- class << self
13
- #
14
- # Validates meta_attribute options
15
- # Checks used options are allowed and then checks options values.
16
- #
17
- # @param opts [Hash] Attribute options
18
- # @param block [Proc] Attribute block
19
- # @param allowed_keys [Array<Symbol>] Allowed options keys
20
- #
21
- # @raise [SeregaError] when attribute has invalid options
22
- #
23
- # @return [void]
24
- #
25
- def call(opts, block, allowed_keys)
26
- check_allowed_options_keys(opts, allowed_keys)
27
- check_each_opt(opts, block)
28
- check_any_value_provided(opts, block)
29
- end
30
-
31
- private
32
-
33
- def check_allowed_options_keys(opts, allowed_keys)
34
- opts.each_key do |key|
35
- next if allowed_keys.include?(key.to_sym)
36
-
37
- allowed = allowed_keys.map(&:inspect).join(", ")
38
- raise SeregaError, "Invalid option #{key.inspect}. Allowed options are: #{allowed}"
39
- end
40
- end
41
-
42
- def check_each_opt(opts, block)
43
- CheckOptHideEmpty.call(opts)
44
- CheckOptHideNil.call(opts)
45
- CheckOptValue.call(opts, block)
46
- CheckOptConst.call(opts, block)
47
- end
48
-
49
- def check_any_value_provided(opts, block)
50
- return if opts.key?(:const) || opts.key?(:value) || block
51
-
52
- raise SeregaError, "Please provide block argument or add :value or :const option"
53
- end
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- module Metadata
6
- class MetaAttribute
7
- #
8
- # Validator for meta_attribute :path parameter
9
- #
10
- # @private
11
- class CheckPath
12
- # Regexp for valid path
13
- FORMAT = /\A[\w~-]+\z/
14
-
15
- private_constant :FORMAT
16
-
17
- class << self
18
- #
19
- # Checks allowed characters.
20
- # Allowed characters: "a-z", "A-Z", "0-9", "_", "-", "~".
21
- #
22
- # @param path [Array<String, Symbol>] Metadata attribute path names
23
- #
24
- # @raise [SeregaError] when metadata attribute name has invalid format
25
- # @return [void]
26
- #
27
- def call(path)
28
- path.each { |attr_name| check_name(attr_name) }
29
- end
30
-
31
- private
32
-
33
- def check_name(name)
34
- name = name.to_s
35
-
36
- raise SeregaError, message(name) unless FORMAT.match?(name)
37
- end
38
-
39
- def message(name)
40
- <<~MESSAGE.tr("\n", "")
41
- Invalid metadata path #{name.inspect}.
42
- Allowed characters: "a-z", "A-Z", "0-9", "_", "-", "~"
43
- MESSAGE
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- module SeregaPlugins
5
- #
6
- # Plugin :string_modifiers
7
- #
8
- # Allows to specify modifiers as strings.
9
- #
10
- # Serialized attributes must be split with `,` and nested attributes can be defined inside brackets `(`, `)`.
11
- #
12
- # @example
13
- # PostSerializer.plugin :string_modifiers
14
- # PostSerializer.new(only: "id,user(id,username)").to_h(post)
15
- # PostSerializer.new(except: "user(username,email)").to_h(post)
16
- # PostSerializer.new(with: "user(email)").to_h(post)
17
- #
18
- # # Modifiers can still be provided old way with nested hashes or arrays.
19
- # PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
20
- #
21
- module StringModifiers
22
- #
23
- # Modifiers parser
24
- #
25
- # @private
26
- class ParseStringModifiers
27
- COMMA = ","
28
- COMMA_CODEPOINT = COMMA.ord
29
- LPAREN = "("
30
- LPAREN_CODEPOINT = LPAREN.ord
31
- RPAREN = ")"
32
- RPAREN_CODEPOINT = RPAREN.ord
33
- private_constant :COMMA, :LPAREN, :RPAREN, :COMMA_CODEPOINT, :LPAREN_CODEPOINT, :RPAREN_CODEPOINT
34
-
35
- class << self
36
- #
37
- # Parses string modifiers
38
- #
39
- # @param fields [String]
40
- #
41
- # @return [Hash] parsed modifiers in form of nested hash
42
- #
43
- # @example
44
- # parse("user") => { user: {} }
45
- # parse("user(id)") => { user: { id: {} } }
46
- # parse("user(id,name)") => { user: { id: {}, name: {} } }
47
- # parse("user,comments") => { user: {}, comments: {} }
48
- # parse("user(comments(text))") => { user: { comments: { text: {} } } }
49
- def parse(fields)
50
- result = {}
51
- attribute_storage = result
52
- path_stack = (fields.include?(LPAREN) || fields.include?(RPAREN)) ? [] : nil
53
-
54
- start_index = 0
55
- end_index = 0
56
- fields.each_codepoint do |codepoint|
57
- case codepoint
58
- when COMMA_CODEPOINT
59
- attribute = extract_attribute(fields, start_index, end_index)
60
- add_attribute(attribute_storage, attribute, FROZEN_EMPTY_HASH) if attribute
61
- start_index = end_index + 1
62
- when LPAREN_CODEPOINT
63
- attribute = extract_attribute(fields, start_index, end_index)
64
- if attribute
65
- attribute_storage = add_attribute(attribute_storage, attribute, {})
66
- path_stack.push(attribute)
67
- end
68
- start_index = end_index + 1
69
- when RPAREN_CODEPOINT
70
- attribute = extract_attribute(fields, start_index, end_index)
71
- add_attribute(attribute_storage, attribute, FROZEN_EMPTY_HASH) if attribute
72
- path_stack.pop
73
- attribute_storage = dig?(result, path_stack)
74
- start_index = end_index + 1
75
- end
76
-
77
- end_index += 1
78
- end
79
-
80
- attribute = extract_attribute(fields, start_index, end_index)
81
- add_attribute(attribute_storage, attribute, FROZEN_EMPTY_HASH) if attribute
82
-
83
- result
84
- end
85
-
86
- private
87
-
88
- def extract_attribute(fields, start_index, end_index)
89
- attribute = fields[start_index, end_index - start_index]
90
- attribute.strip!
91
- attribute.empty? ? nil : attribute.to_sym
92
- end
93
-
94
- def add_attribute(storage, attribute, nested_attributes = FROZEN_EMPTY_HASH)
95
- storage[attribute] = nested_attributes
96
- end
97
-
98
- def dig?(hash, path)
99
- return hash if !path || path.empty?
100
-
101
- path.each do |point|
102
- hash = hash[point]
103
- end
104
-
105
- hash
106
- end
107
- end
108
- end
109
- end
110
- end
111
- end