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
data/lib/serega/errors.rb CHANGED
@@ -1,23 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
+ # Raised on invalid definitions and options, and on changes of a locked
5
+ # serializer. Parent class of the other Serega errors.
4
6
  #
5
- # Base exception class
6
- #
7
+ # See [Errors](https://github.com/andreyruby/serega/blob/master/docs/errors.md).
7
8
  class SeregaError < StandardError; end
8
9
 
9
- # Raised when serializer is initiated using not existing attribute
10
+ # Raised when `only:`, `except:` or `with:` names an unknown field
11
+ #
12
+ # ```ruby
13
+ # UserSerializer.to_h(user, only: [:nickname, {friends: :age}])
14
+ # # => Serega::AttributeNotExist: Not existing attributes: nickname, friends(age)
15
+ # ```
10
16
  #
11
- # @example
12
- # Serega.new(only: 'FOO')
13
- # # => Attribute 'FOO' not exists (Serega::AttributeNotExist)
17
+ # See [Unknown fields](https://github.com/andreyruby/serega/blob/master/docs/selecting-fields.md#unknown-fields).
14
18
  class AttributeNotExist < SeregaError
15
- attr_reader :serializer, :attributes
19
+ # Serializer that has no such fields
20
+ attr_reader :serializer
21
+
22
+ # Unknown fields, for example `["nickname", "friends(age)"]`
23
+ attr_reader :attributes
16
24
 
17
- def initialize(message = nil, serializer = nil, attributes = nil)
25
+ def initialize(message = nil, serializer = nil, attributes = nil) # :nodoc:
18
26
  super(message)
19
27
  @serializer = serializer
20
28
  @attributes = attributes
21
29
  end
22
30
  end
31
+
32
+ # Raised when fields nest deeper than `config.depth_limit` of the
33
+ # serializer that starts the serialization
34
+ #
35
+ # ```ruby
36
+ # UserSerializer.to_h(user, with: {friends: :friends})
37
+ # # => Serega::DepthLimitError: Depth limit was exceeded
38
+ # ```
39
+ #
40
+ # See [Depth limit](https://github.com/andreyruby/serega/blob/master/docs/configuration.md#depth-limit).
41
+ class DepthLimitError < SeregaError
42
+ # Path that exceeds the limit, for example
43
+ # `UserSerializer (depth limit: 2) -> friends -> friends`
44
+ attr_reader :details
45
+
46
+ def initialize(message = nil, details = nil) # :nodoc:
47
+ super(message)
48
+ @details = details
49
+ end
50
+ end
23
51
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # Formats attribute values. A formatter is one of:
6
+ # - a callable that gets the value, and the context when it has a second
7
+ # parameter or the :ctx keyword;
8
+ # - a method name, called on the value;
9
+ # - an Array of a method name and its arguments, for example `[:round, 2]`;
10
+ # - a Hash with one of them as `:use` and the `:allow_nil` option.
11
+ #
12
+ # With `allow_nil: true` a nil value stays nil and is not formatted.
13
+ #
14
+ class SeregaFormatter # :nodoc:
15
+ # Callable that formats a value, nil for a method formatter
16
+ attr_reader :callable
17
+
18
+ # Arguments of the method, nil for a callable formatter
19
+ attr_reader :args
20
+
21
+ def initialize(formatter)
22
+ formatter = {use: formatter} unless formatter.is_a?(Hash)
23
+ use = formatter[:use]
24
+ @allow_nil = formatter.fetch(:allow_nil, false)
25
+
26
+ if use.respond_to?(:call)
27
+ @method_name = nil
28
+ @args = nil
29
+ @callable = use
30
+ @signature = SeregaUtils::MethodSignature.call(use, pos_limit: 2, keyword_args: [:ctx])
31
+ else
32
+ method_name, *args = use
33
+ @method_name = method_name
34
+ @args = args
35
+ @callable = nil
36
+ @signature = nil
37
+ end
38
+ end
39
+
40
+ # Formats the value
41
+ def call(value, context)
42
+ return if value.nil? && @allow_nil
43
+ return value.public_send(@method_name, *@args) if @method_name
44
+
45
+ case @signature
46
+ when "1" then @callable.call(value)
47
+ when "1_ctx" then @callable.call(value, ctx: context)
48
+ else @callable.call(value, context) # "2"
49
+ end
50
+ end
51
+
52
+ # Ruby code that formats the value of `value_code`. A method is called
53
+ # directly, for example `(source.created_at).iso8601(3)`. Literal
54
+ # arguments are written in the code, other arguments are read from the
55
+ # Array that `args_code` reads. A callable is read with `callable_code`.
56
+ def code(value_code, callable_code, args_code)
57
+ return method_call_code(value_code, args_code) if @method_name
58
+ return unless @signature == "1"
59
+ return "#{callable_code}.call(#{value_code})" unless @allow_nil
60
+
61
+ "(value = #{value_code}).nil? ? nil : #{callable_code}.call(value)"
62
+ end
63
+
64
+ private
65
+
66
+ def method_call_code(value_code, args_code)
67
+ operator = @allow_nil ? "&." : "."
68
+ arguments = @args.each_with_index.map { |arg, index| argument_code(arg, index, args_code) }
69
+
70
+ unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
71
+ send_arguments = [@method_name.inspect, *arguments]
72
+ return "(#{value_code})#{operator}public_send(#{send_arguments.join(", ")})"
73
+ end
74
+
75
+ return "(#{value_code})#{operator}#{@method_name}" if arguments.empty?
76
+
77
+ "(#{value_code})#{operator}#{@method_name}(#{arguments.join(", ")})"
78
+ end
79
+
80
+ # Literal arguments are written in the code, other arguments are read
81
+ # from the Array of arguments
82
+ def argument_code(arg, index, args_code)
83
+ literal?(arg) ? literal_code(arg) : "#{args_code}[#{index}]"
84
+ end
85
+
86
+ def literal?(arg)
87
+ case arg
88
+ when Integer, Symbol, String, true, false, nil then true
89
+ when Float then arg.finite?
90
+ else false
91
+ end
92
+ end
93
+
94
+ def literal_code(arg)
95
+ arg.is_a?(String) ? "#{arg.inspect}.freeze" : arg.inspect
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ class SeregaPlan # :nodoc:
5
+ #
6
+ # Serialization plans of one serializer, cached by modifiers.
7
+ # Validates initiate options before building a plan.
8
+ #
9
+ class Cache # :nodoc:
10
+ # Cached plan, whether its modifiers were validated, and the modifiers
11
+ # as provided
12
+ CachedPlan = Struct.new(:plan, :validated, :only, :with, :except)
13
+ private_constant :CachedPlan
14
+
15
+ # Initializes empty plans cache of the serializer
16
+ def initialize(serializer_class)
17
+ @serializer_class = serializer_class
18
+ @default_plan = nil
19
+ @plans = {}
20
+ end
21
+
22
+ # Returns serialization plan for provided modifiers.
23
+ # The plan without modifiers is always cached; plans with modifiers are
24
+ # cached by modifiers as provided when `max_cached_plans_per_serializer_count`
25
+ # is positive. Modifiers are parsed and validated when a plan is built.
26
+ def fetch(only, with, except, check_initiate_params: false)
27
+ return default_plan if only.nil? && with.nil? && except.nil?
28
+
29
+ max_size = @serializer_class.config.max_cached_plans_per_serializer_count
30
+ return build(only, with, except, check_initiate_params) if max_size.zero?
31
+
32
+ cached_plan(only, with, except, max_size, check_initiate_params)
33
+ end
34
+
35
+ private
36
+
37
+ def default_plan
38
+ @default_plan ||= SeregaPlan.new(@serializer_class, nil, FROZEN_EMPTY_HASH)
39
+ end
40
+
41
+ # Keeps plans by the hash of the hash values of the modifiers, and
42
+ # compares each modifier with the cached one: an Array of the modifiers
43
+ # as the key hashes and compares nested arrays inside a recursion guard.
44
+ # Modifiers with the hash of other cached modifiers replace them.
45
+ def cached_plan(only, with, except, max_size, check_initiate_params)
46
+ key = [only.hash, with.hash, except.hash].hash # rubocop:disable Security/CompoundHash -- `[only, with, except].hash` hashes nested arrays inside a recursion guard
47
+ cached = @plans[key]
48
+ return reuse(cached, only, with, except, check_initiate_params) if cached && same_modifiers?(cached, only, with, except)
49
+
50
+ plan = build(only, with, except, check_initiate_params)
51
+ @plans[key] = CachedPlan.new(plan, check_initiate_params, *frozen_copy([only, with, except]))
52
+ @plans.shift if @plans.length > max_size
53
+ plan
54
+ end
55
+
56
+ def same_modifiers?(cached, only, with, except)
57
+ only.eql?(cached.only) && with.eql?(cached.with) && except.eql?(cached.except)
58
+ end
59
+
60
+ def reuse(cached, only, with, except, check_initiate_params)
61
+ if check_initiate_params && !cached.validated
62
+ validate(parse_modifiers(only, with, except))
63
+ cached.validated = true
64
+ end
65
+
66
+ cached.plan
67
+ end
68
+
69
+ def build(only, with, except, check_initiate_params)
70
+ modifiers = parse_modifiers(only, with, except)
71
+ return default_plan if modifiers[:only].empty? && modifiers[:with].empty? && modifiers[:except].empty?
72
+
73
+ validate(modifiers) if check_initiate_params
74
+ SeregaPlan.new(@serializer_class, nil, modifiers)
75
+ end
76
+
77
+ def parse_modifiers(only, with, except)
78
+ {only: parse_modifier(only), with: parse_modifier(with), except: parse_modifier(except)}
79
+ end
80
+
81
+ def parse_modifier(value)
82
+ return SeregaUtils::ParseStringModifiers.parse(value) if value.is_a?(String)
83
+
84
+ SeregaUtils::ToHash.call(value)
85
+ end
86
+
87
+ def frozen_copy(modifiers)
88
+ SeregaUtils::EnumDeepFreeze.call(SeregaUtils::EnumDeepDup.call(modifiers))
89
+ end
90
+
91
+ def validate(modifiers)
92
+ SeregaValidations::Initiate::CheckModifiers.new.call(@serializer_class, modifiers[:only], modifiers[:with], modifiers[:except])
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ class SeregaPlan # :nodoc:
5
+ #
6
+ # Shareable plans cache of a shareable serializer, keeps the plans of each
7
+ # Ractor apart
8
+ #
9
+ class PerRactorCache # :nodoc:
10
+ def initialize(serializer_class)
11
+ @serializer_class = serializer_class
12
+ @key = SeregaUtils::RactorLocal.key(serializer_class, :plan_cache)
13
+ freeze
14
+ end
15
+
16
+ # Returns serialization plan from the plans cache of the current Ractor
17
+ def fetch(only, with, except, check_initiate_params: false)
18
+ plan_cache = SeregaUtils::RactorLocal.fetch(@key) { SeregaPlan::Cache.new(@serializer_class) }
19
+ plan_cache.fetch(only, with, except, check_initiate_params: check_initiate_params)
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/serega/plan.rb CHANGED
@@ -5,152 +5,125 @@ class Serega
5
5
  # Constructs plan - list of serialized attributes.
6
6
  # We will traverse this plan to construct serialized response.
7
7
  #
8
- # @private
9
- class SeregaPlan
10
- #
11
- # SeregaPlan class methods
12
- #
13
- # @private
14
- module ClassMethods
15
- #
16
- # Constructs plan of attributes that should be serialized.
17
- #
18
- # @param opts Serialization parameters
19
- # @option opts [Hash] :only The only attributes to serialize
20
- # @option opts [Hash] :except Attributes to hide
21
- # @option opts [Hash] :with Attributes (usually marked `hide: true`) to serialize additionally
22
- #
23
- # @return [SeregaPlan] Serialization plan
24
- #
25
- def call(opts)
26
- max_cache_size = serializer_class.config.max_cached_plans_per_serializer_count
27
- return new(nil, opts) if max_cache_size.zero?
28
-
29
- cached_plan_for(opts, max_cache_size)
30
- end
31
-
32
- #
33
- # Returns (and caches) the Data class for the given set of field names.
34
- # Uses the Array as cache key so the same Data class is reused across
35
- # all plan instances with identical fields.
36
- #
37
- # @param point_names [Array<Symbol>] Attribute names for the Data members
38
- # @return [Class] Subclass of Data
39
- #
40
- def data_class_for(point_names)
41
- (@data_classes ||= {})[point_names] ||= Data.define(*point_names)
42
- end
43
-
44
- private
45
-
46
- def cached_plan_for(opts, max_cache_size)
47
- @cache ||= {}
48
- cache_key = construct_cache_key(opts)
49
-
50
- plan = @cache[cache_key] ||= new(nil, opts)
51
- @cache.shift if @cache.length > max_cache_size
52
- plan
53
- end
8
+ class SeregaPlan # :nodoc:
9
+ # Serializer of the plan
10
+ attr_reader :serializer_class
11
+
12
+ # Parent plan point
13
+ attr_reader :parent_plan_point
14
+
15
+ # Serialization points
16
+ attr_reader :points
17
+
18
+ # Serialization points with child plans
19
+ attr_reader :relation_points
20
+
21
+ # Whether the root objects of a class are collections
22
+ attr_reader :collection_classes
23
+
24
+ # Presenter class that wraps the sources in the Ractor of the plan, `nil`
25
+ # without presenter methods
26
+ attr_reader :presenter
27
+
28
+ # Instantiate new serialization plan. Raises DepthLimitError when the plan
29
+ # nests deeper than the depth limit of the root serializer.
30
+ def initialize(serializer_class, parent_plan_point, modifiers)
31
+ check_depth_limit(parent_plan_point) if parent_plan_point
32
+ serializer_class.lock
33
+ @serializer_class = serializer_class
34
+ @parent_plan_point = parent_plan_point
35
+ @presenter = serializer_class.local_presenter
36
+ @points = attributes_points(modifiers)
37
+ @relation_points = points.select(&:child_plan).freeze
38
+ @preload_points = points.select(&:preloads).freeze
39
+ @batch_points = points.any? { |point| !point.batch_loaders.empty? }
40
+ @flat = relation_points.empty? && preload_points.empty? && !@batch_points && !@presenter
41
+ @result_builders = {}
42
+ @collection_classes = {}.compare_by_identity
43
+ direct = parent_plan_point.nil? && @flat && !serializer_class.prepare_initial_objects_signature
44
+ @direct_hash_builder = direct ? result_builder(:hash) : nil
45
+ # :nocov:
46
+ rescue *RACTOR_ISOLATION_ERRORS
47
+ raise if serializer_class.shareable?
48
+
49
+ raise SeregaError, "#{serializer_class} can not serialize in other Ractors. Call `make_shareable!` at the end of its definition"
50
+ # :nocov:
51
+ end
54
52
 
55
- def construct_cache_key(opts, cache_key = nil)
56
- return nil if opts.empty?
53
+ # Serialization points with preloads
54
+ attr_reader :preload_points
57
55
 
58
- cache_key ||= +""
56
+ # Hash result builder of a flat root plan whose serializer prepares no
57
+ # initial objects. `Serega#call` builds one source with it directly.
58
+ attr_reader :direct_hash_builder
59
59
 
60
- opts.each do |key, nested_opts|
61
- cache_key.insert(-1, SeregaUtils::SymbolName.call(key))
62
- cache_key.insert(-1, "-")
63
- construct_cache_key(nested_opts, cache_key)
64
- end
60
+ # Whether any point uses batch loaders
61
+ def batch_points?
62
+ @batch_points
63
+ end
65
64
 
66
- cache_key
67
- end
65
+ # A flat plan has no relations, preloads, batch loaders and presenter.
66
+ # Its sources need no source groups.
67
+ def flat?
68
+ @flat
68
69
  end
69
70
 
70
- #
71
- # SeregaPlan instance methods
72
- #
73
- # @private
74
- module InstanceMethods
75
- # Parent plan point
76
- # @return [SeregaPlanPoint, nil]
77
- attr_reader :parent_plan_point
78
-
79
- # Serialization points
80
- # @return [Array<SeregaPlanPoint>] points to serialize
81
- attr_reader :points
82
-
83
- # Named serialization points
84
- # @return [Hash] Named points
85
- attr_reader :points_hash
86
-
87
- #
88
- # Instantiate new serialization plan
89
- #
90
- # Patched by
91
- # - depth_limit plugin, which checks depth limit is not exceeded when adding new plan
92
- #
93
- # @param parent_plan_point [SeregaPlanPoint, nil] Parent plan_point
94
- # @param modifiers [Hash] Serialization parameters
95
- # @option modifiers [Hash] :only The only attributes to serialize
96
- # @option modifiers [Hash] :except Attributes to hide
97
- # @option modifiers [Hash] :with Hidden attributes to serialize additionally
98
- #
99
- # @return [SeregaPlan] Serialization plan
100
- #
101
- def initialize(parent_plan_point, modifiers)
102
- @parent_plan_point = parent_plan_point
103
- @points = attributes_points(modifiers)
104
- @points_hash = points.to_h { |point| [point.name, point] }
105
- end
71
+ # Result builder of this plan in the serialization mode
72
+ def result_builder(mode)
73
+ @result_builders[mode] ||= SeregaResultBuilder.new(serializer_class, mode, points)
74
+ end
106
75
 
107
- #
108
- # Serializer class of current plan
109
- #
110
- def serializer_class
111
- self.class.serializer_class
112
- end
76
+ private
113
77
 
114
- # Returns the Data class whose members match this plan's serialized fields.
115
- # Delegates to the class-level cache so identical field sets share one Data class.
116
- #
117
- # @return [Class] Subclass of Data
118
- #
119
- def data_class
120
- @data_class ||= self.class.data_class_for(point_names)
78
+ def check_depth_limit(parent_plan_point)
79
+ depth = 2
80
+ root_plan = parent_plan_point.plan
81
+ while (point = root_plan.parent_plan_point)
82
+ depth += 1
83
+ root_plan = point.plan
121
84
  end
122
85
 
123
- private
86
+ root_serializer = root_plan.serializer_class
87
+ limit = root_serializer.config.depth_limit
88
+ return if limit.nil? || depth <= limit
124
89
 
125
- def attributes_points(modifiers)
126
- only = modifiers[:only] || FROZEN_EMPTY_HASH
127
- except = modifiers[:except] || FROZEN_EMPTY_HASH
128
- with = modifiers[:with] || FROZEN_EMPTY_HASH
129
- points = []
90
+ names = [parent_plan_point.name]
91
+ point = parent_plan_point
92
+ names.unshift(point.name) while (point = point.plan.parent_plan_point)
93
+ raise DepthLimitError.new("Depth limit was exceeded", "#{root_serializer} (depth limit: #{limit}) -> #{names.join(" -> ")}")
94
+ end
130
95
 
131
- serializer_class.attributes.each_value do |attribute|
132
- next unless attribute.visible?(only: only, except: except, with: with)
96
+ def attributes_points(modifiers)
97
+ only = modifiers[:only] || FROZEN_EMPTY_HASH
98
+ except = modifiers[:except] || FROZEN_EMPTY_HASH
99
+ with = modifiers[:with] || FROZEN_EMPTY_HASH
100
+ only_limits = only_limits?(only, except, with)
101
+ points = []
133
102
 
134
- child_fields =
135
- if attribute.relation?
136
- name = attribute.name
137
- {only: only[name], with: with[name], except: except[name]}
138
- end
103
+ serializer_class.attributes.each_value do |attribute|
104
+ next unless attribute.visible?(only, except, with, only_limits)
139
105
 
140
- point = serializer_class::SeregaPlanPoint.new(self, attribute, child_fields)
141
- points << point.freeze
142
- end
106
+ child_fields =
107
+ if attribute.relation?
108
+ name = attribute.name
109
+ {only: only[name], with: with[name], except: except[name]}
110
+ end
143
111
 
144
- points.freeze
112
+ point = SeregaPlanPoint.new(self, attribute, child_fields)
113
+ points << point.freeze
145
114
  end
146
115
 
147
- def point_names
148
- @point_names ||= points.map(&:name)
149
- end
116
+ points.freeze
150
117
  end
151
118
 
152
- extend ClassMethods
153
- include InstanceMethods
154
- extend SeregaHelpers::SerializerClassHelper
119
+ # `only` hides the fields it does not name when it names a field of
120
+ # this level without nested fields, or when `except` hides no field and
121
+ # `with` adds no field of this level
122
+ def only_limits?(only, except, with)
123
+ return false if only.empty?
124
+ return true if only.each_value.any?(&:empty?)
125
+
126
+ with.empty? && except.each_value.none?(&:empty?)
127
+ end
155
128
  end
156
129
  end