serega 0.11.1 → 0.12.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +62 -13
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +9 -4
  5. data/lib/serega/attribute_normalizer.rb +4 -13
  6. data/lib/serega/object_serializer.rb +11 -0
  7. data/lib/serega/plan.rb +20 -25
  8. data/lib/serega/plan_point.rb +13 -16
  9. data/lib/serega/plugins/batch/batch.rb +7 -245
  10. data/lib/serega/plugins/batch/lib/batch_config.rb +82 -0
  11. data/lib/serega/plugins/batch/lib/loader.rb +25 -7
  12. data/lib/serega/plugins/batch/lib/modules/attribute.rb +26 -0
  13. data/lib/serega/plugins/batch/lib/modules/attribute_normalizer.rb +65 -0
  14. data/lib/serega/plugins/batch/lib/modules/check_attribute_params.rb +22 -0
  15. data/lib/serega/plugins/batch/lib/modules/config.rb +23 -0
  16. data/lib/serega/plugins/batch/lib/modules/object_serializer.rb +46 -0
  17. data/lib/serega/plugins/batch/lib/modules/plan_point.rb +39 -0
  18. data/lib/serega/plugins/metadata/metadata.rb +5 -0
  19. data/lib/serega/plugins/preloads/lib/modules/attribute.rb +28 -0
  20. data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +99 -0
  21. data/lib/serega/plugins/preloads/lib/modules/check_attribute_params.rb +22 -0
  22. data/lib/serega/plugins/preloads/lib/modules/config.rb +19 -0
  23. data/lib/serega/plugins/preloads/lib/modules/plan_point.rb +41 -0
  24. data/lib/serega/plugins/preloads/lib/preload_paths.rb +46 -0
  25. data/lib/serega/plugins/preloads/lib/preloads_config.rb +62 -0
  26. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +20 -7
  27. data/lib/serega/plugins/preloads/preloads.rb +12 -210
  28. data/lib/serega/plugins/preloads/validations/check_opt_preload_path.rb +54 -15
  29. metadata +16 -3
  30. data/lib/serega/plugins/preloads/lib/main_preload_path.rb +0 -53
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # Batch plugin config
8
+ #
9
+ class BatchConfig
10
+ attr_reader :opts
11
+
12
+ def initialize(opts)
13
+ @opts = opts
14
+ end
15
+
16
+ #
17
+ # Defines batch loader
18
+ #
19
+ # @param loader_name [Symbol] Batch loader name, that is used when defining attribute with batch loader.
20
+ # @param block [Proc] Block that can accept 3 parameters - keys, context, plan_point
21
+ # and returns hash where ids are keys and values are batch loaded objects/
22
+ #
23
+ # @return [void]
24
+ #
25
+ def define(loader_name, &block)
26
+ unless block
27
+ raise SeregaError, "Block must be given to #define method"
28
+ end
29
+
30
+ params = block.parameters
31
+ if params.count > 3 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) }
32
+ raise SeregaError, "Block can have maximum 3 regular parameters"
33
+ end
34
+
35
+ loaders[loader_name] = block
36
+ end
37
+
38
+ # Shows defined loaders
39
+ # @return [Hash] defined loaders
40
+ def loaders
41
+ opts[:loaders]
42
+ end
43
+
44
+ #
45
+ # Finds previously defined batch loader by name
46
+ #
47
+ # @param loader_name [Symbol]
48
+ #
49
+ # @return [Proc] batch loader block
50
+ def fetch_loader(loader_name)
51
+ loaders[loader_name] || (raise SeregaError, "Batch loader with name `#{loader_name.inspect}` was not defined. Define example: config.batch.define(:#{loader_name}) { |keys, ctx, points| ... }")
52
+ end
53
+
54
+ # Shows option to auto hide attributes with :batch specified
55
+ # @return [Boolean, nil] option value
56
+ def auto_hide
57
+ opts[:auto_hide]
58
+ end
59
+
60
+ # @param value [Boolean] New :auto_hide option value
61
+ # @return [Boolean] New option value
62
+ def auto_hide=(value)
63
+ raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
64
+ opts[:auto_hide] = value
65
+ end
66
+
67
+ # Shows default key for :batch option
68
+ # @return [Symbol, nil] default key for :batch option
69
+ def default_key
70
+ opts[:default_key]
71
+ end
72
+
73
+ # @param value [Symbol] New :default_key option value
74
+ # @return [Boolean] New option value
75
+ def default_key=(value)
76
+ raise SeregaError, "Must be a Symbol, #{value.inspect} provided" unless value.is_a?(Symbol)
77
+ opts[:default_key] = value
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -60,6 +60,10 @@ class Serega
60
60
 
61
61
  private
62
62
 
63
+ def keys
64
+ @keys ||= {}
65
+ end
66
+
63
67
  def each_key
64
68
  keys.each do |key, containers|
65
69
  containers.each do |container|
@@ -74,16 +78,30 @@ class Serega
74
78
  def keys_values
75
79
  ids = keys.keys
76
80
 
77
- point.batch[:loader].call(ids, object_serializer.context, point).tap do |vals|
78
- next if vals.is_a?(Hash)
81
+ keys_values = load_keys_values(ids)
82
+ validate(keys_values)
79
83
 
80
- attribute_name = "#{point.class.serializer_class}.#{point.name}"
81
- raise SeregaError, "Batch loader for `#{attribute_name}` must return Hash, but #{vals.inspect} was returned"
82
- end
84
+ keys_values
83
85
  end
84
86
 
85
- def keys
86
- @keys ||= {}
87
+ def load_keys_values(ids)
88
+ point.batch[:loader].call(ids, object_serializer.context, point)
89
+ rescue => error
90
+ raise reraise_with_serialized_attribute_details(error, point)
91
+ end
92
+
93
+ def validate(keys_values)
94
+ return if keys_values.is_a?(Hash)
95
+
96
+ attribute_name = "#{point.class.serializer_class}.#{point.name}"
97
+ raise SeregaError, "Batch loader for `#{attribute_name}` must return Hash, but #{keys_values.inspect} was returned"
98
+ end
99
+
100
+ def reraise_with_serialized_attribute_details(error, point)
101
+ raise error.exception(<<~MESSAGE.strip)
102
+ #{error.message}
103
+ (when serializing '#{point.name}' attribute in #{self.class.serializer_class})
104
+ MESSAGE
87
105
  end
88
106
  end
89
107
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # Serega::SeregaAttribute additional/patched class methods
8
+ #
9
+ # @see Serega::SeregaAttribute
10
+ #
11
+ module AttributeInstanceMethods
12
+ #
13
+ # @return [nil, Hash] :batch option
14
+ #
15
+ attr_reader :batch
16
+
17
+ private
18
+
19
+ def set_normalized_vars(normalizer)
20
+ super
21
+ @batch = normalizer.batch
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # SeregaAttributeNormalizer additional/patched instance methods
8
+ #
9
+ # @see SeregaAttributeNormalizer::AttributeInstanceMethods
10
+ #
11
+ module AttributeNormalizerInstanceMethods
12
+ #
13
+ # Returns normalized attribute :batch option with prepared :key and
14
+ # :default options. Option :loader will be prepared at serialization
15
+ # time as loaders are usually defined after attributes.
16
+ #
17
+ # @return [Hash] attribute :batch normalized options
18
+ #
19
+ def batch
20
+ return @batch if instance_variable_defined?(:@batch)
21
+
22
+ @batch = prepare_batch
23
+ end
24
+
25
+ private
26
+
27
+ #
28
+ # Patch for original `prepare_hide` method
29
+ #
30
+ # Marks attribute hidden if auto_hide option was set and attribute has batch loader
31
+ #
32
+ def prepare_hide
33
+ res = super
34
+ return res unless res.nil?
35
+
36
+ if batch
37
+ self.class.serializer_class.config.batch.auto_hide || nil
38
+ end
39
+ end
40
+
41
+ def prepare_batch
42
+ batch = init_opts[:batch]
43
+ return unless batch
44
+
45
+ # take loader
46
+ loader = batch[:loader]
47
+
48
+ # take key
49
+ key = batch[:key] || self.class.serializer_class.config.batch.default_key
50
+ proc_key =
51
+ if key.is_a?(Symbol)
52
+ proc { |object| object.public_send(key) }
53
+ else
54
+ key
55
+ end
56
+
57
+ # take default value
58
+ default = batch.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
59
+
60
+ {loader: loader, key: proc_key, default: default}
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
8
+ #
9
+ # @see Serega::SeregaValidations::CheckAttributeParams
10
+ #
11
+ module CheckAttributeParamsInstanceMethods
12
+ private
13
+
14
+ def check_opts
15
+ super
16
+
17
+ CheckOptBatch.call(opts, block, self.class.serializer_class)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # Config class additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaConfig
10
+ #
11
+ module ConfigInstanceMethods
12
+ #
13
+ # Returns all batch loaders registered for current serializer
14
+ #
15
+ # @return [Serega::SeregaPlugins::Batch::BatchConfig] configuration for batch loaded attributes
16
+ #
17
+ def batch
18
+ @batch ||= BatchConfig.new(opts.fetch(:batch))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # SeregaObjectSerializer additional/patched class methods
8
+ #
9
+ # @see Serega::SeregaObjectSerializer
10
+ #
11
+ module SeregaObjectSerializerInstanceMethods
12
+ private
13
+
14
+ def attach_value(object, point, container)
15
+ batch = point.batch
16
+ return super unless batch
17
+
18
+ remember_key_for_batch_loading(batch, object, point, container)
19
+ end
20
+
21
+ def remember_key_for_batch_loading(batch, object, point, container)
22
+ key = batch[:key].call(object, context)
23
+ batch_loader(point).remember(key, container)
24
+ container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
25
+ end
26
+
27
+ def batch_loader(point)
28
+ batch_loaders = opts[:batch_loaders]
29
+ raise_batch_plugin_for_serializer_not_defined(point) unless batch_loaders
30
+ batch_loaders.get(point, self)
31
+ end
32
+
33
+ def raise_batch_plugin_for_serializer_not_defined(point)
34
+ root_plan = point.plan
35
+ root_plan = plan.parent_plan_point.plan while root_plan.parent_plan_point
36
+ current_serializer = root_plan.serializer_class
37
+ nested_serializer = self.class.serializer_class
38
+
39
+ raise SeregaError,
40
+ "Plugin :batch must be added to current serializer (#{current_serializer})" \
41
+ " to load attributes with :batch option in nested serializer (#{nested_serializer})"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Batch
6
+ #
7
+ # Serega::SeregaPlanPoint additional/patched class methods
8
+ #
9
+ # @see SeregaAttribute
10
+ #
11
+ module PlanPointInstanceMethods
12
+ #
13
+ # Returns attribute :batch option with prepared loader
14
+ # @return [Hash] attribute :batch option
15
+ #
16
+ attr_reader :batch
17
+
18
+ private
19
+
20
+ def set_normalized_vars
21
+ super
22
+ @batch = prepare_batch
23
+ end
24
+
25
+ def prepare_batch
26
+ batch = attribute.batch
27
+ if batch
28
+ loader = batch[:loader]
29
+ if loader.is_a?(Symbol)
30
+ batch_config = attribute.class.serializer_class.config.batch
31
+ batch[:loader] = batch_config.fetch_loader(loader)
32
+ end
33
+ end
34
+ batch
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -194,6 +194,11 @@ class Serega
194
194
  next unless metadata
195
195
 
196
196
  deep_merge_metadata(hash, metadata)
197
+ rescue => error
198
+ raise error.exception(<<~MESSAGE.strip)
199
+ #{error.message}
200
+ (when serializing meta_attribute #{meta_attribute.path.inspect} in #{self.class})
201
+ MESSAGE
197
202
  end
198
203
  end
199
204
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaAttribute additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaAttribute::AttributeInstanceMethods
10
+ #
11
+ module AttributeInstanceMethods
12
+ # @return [Hash, nil] normalized preloads of current attribute
13
+ attr_reader :preloads
14
+
15
+ # @return [Array] normalized preloads_path of current attribute
16
+ attr_reader :preloads_path
17
+
18
+ private
19
+
20
+ def set_normalized_vars(normalizer)
21
+ super
22
+ @preloads = normalizer.preloads
23
+ @preloads_path = normalizer.preloads_path
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaAttributeNormalizer additional/patched instance methods
8
+ #
9
+ # @see SeregaAttributeNormalizer::AttributeNormalizerInstanceMethods
10
+ #
11
+ module AttributeNormalizerInstanceMethods
12
+ # @return [Hash,nil] normalized attribute preloads
13
+ def preloads
14
+ return @preloads if instance_variable_defined?(:@preloads)
15
+
16
+ @preloads = prepare_preloads
17
+ end
18
+
19
+ # @return [Array, nil] normalized attribute preloads path
20
+ def preloads_path
21
+ return @preloads_path if instance_variable_defined?(:@preloads_path)
22
+
23
+ @preloads_path = prepare_preloads_path
24
+ end
25
+
26
+ private
27
+
28
+ #
29
+ # Patched in:
30
+ # - plugin :batch (extension :preloads - skips auto preloads when batch option provided)
31
+ #
32
+ def prepare_preloads
33
+ opts = init_opts
34
+ preloads_provided = opts.key?(:preload)
35
+ preloads =
36
+ if preloads_provided
37
+ opts[:preload]
38
+ elsif opts.key?(:serializer) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_serializer
39
+ key
40
+ elsif opts.key?(:delegate) && self.class.serializer_class.config.preloads.auto_preload_attributes_with_delegate
41
+ opts[:delegate].fetch(:to)
42
+ end
43
+
44
+ # Nil and empty hash differs as we can preload nested results to
45
+ # empty hash, but we will skip nested preloading if nil or false provided
46
+ return if preloads_provided && !preloads
47
+
48
+ FormatUserPreloads.call(preloads)
49
+ end
50
+
51
+ def prepare_preloads_path
52
+ path = init_opts.fetch(:preload_path) { default_preload_path(preloads) }
53
+
54
+ if path && path[0].is_a?(Array)
55
+ prepare_many_preload_paths(path)
56
+ else
57
+ prepare_one_preload_path(path)
58
+ end
59
+ end
60
+
61
+ def prepare_one_preload_path(path)
62
+ return unless path
63
+
64
+ case path
65
+ when Array
66
+ path.map(&:to_sym).freeze
67
+ else
68
+ [path.to_sym].freeze
69
+ end
70
+ end
71
+
72
+ def prepare_many_preload_paths(paths)
73
+ paths.map { |path| prepare_one_preload_path(path) }.freeze
74
+ end
75
+
76
+ def default_preload_path(preloads)
77
+ return FROZEN_EMPTY_ARRAY if !preloads || preloads.empty?
78
+
79
+ [preloads.keys.first]
80
+ end
81
+
82
+ #
83
+ # Patch for original `prepare_hide` method
84
+ # @see
85
+ #
86
+ # Marks attribute hidden if auto_hide_attribute_with_preloads option was set and attribute has preloads
87
+ #
88
+ def prepare_hide
89
+ res = super
90
+ return res unless res.nil?
91
+
92
+ if preloads && !preloads.empty?
93
+ self.class.serializer_class.config.preloads.auto_hide_attributes_with_preload || nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
8
+ #
9
+ # @see Serega::SeregaValidations::CheckAttributeParams
10
+ #
11
+ module CheckAttributeParamsInstanceMethods
12
+ private
13
+
14
+ def check_opts
15
+ super
16
+ CheckOptPreload.call(opts)
17
+ CheckOptPreloadPath.call(opts)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Config class additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaConfig
10
+ #
11
+ module ConfigInstanceMethods
12
+ # @return [Serega::SeregaPlugins::Preloads::PreloadsConfig] `preloads` plugin config
13
+ def preloads
14
+ @preloads ||= PreloadsConfig.new(opts.fetch(:preloads))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Serega::SeregaPlanPoint additional/patched instance methods
8
+ #
9
+ # @see Serega::SeregaPlanPoint::InstanceMethods
10
+ #
11
+ module PlanPointInstanceMethods
12
+ #
13
+ # @return [Hash] preloads for nested attributes
14
+ #
15
+ attr_reader :preloads
16
+
17
+ #
18
+ # @return [Array<Symbol>] preloads path for current attribute
19
+ #
20
+ attr_reader :preloads_path
21
+
22
+ private
23
+
24
+ def set_normalized_vars
25
+ super
26
+
27
+ @preloads = prepare_preloads
28
+ @preloads_path = prepare_preloads_path
29
+ end
30
+
31
+ def prepare_preloads
32
+ PreloadsConstructor.call(child_plan)
33
+ end
34
+
35
+ def prepare_preloads_path
36
+ attribute.preloads_path
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Utility that helps to transform preloads to array of paths
8
+ #
9
+ # Example:
10
+ #
11
+ # call({ a: { b: { c: {}, d: {} } }, e: {} })
12
+ #
13
+ # => [
14
+ # [:a],
15
+ # [:a, :b],
16
+ # [:a, :b, :c],
17
+ # [:a, :b, :d],
18
+ # [:e]
19
+ # ]
20
+ class PreloadPaths
21
+ class << self
22
+ #
23
+ # Transforms user provided preloads to array of paths
24
+ #
25
+ # @param value [Array,Hash,String,Symbol,nil,false] preloads
26
+ #
27
+ # @return [Hash] preloads transformed to hash
28
+ #
29
+ def call(preloads, path = [], result = [])
30
+ preloads = FormatUserPreloads.call(preloads)
31
+
32
+ preloads.each do |key, nested_preloads|
33
+ path << key
34
+ result << path.dup
35
+
36
+ call(nested_preloads, path, result)
37
+ path.pop
38
+ end
39
+
40
+ result
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module Preloads
6
+ #
7
+ # Config for `preloads` plugin
8
+ #
9
+ class PreloadsConfig
10
+ # @return [Hash] preloads plugin options
11
+ attr_reader :opts
12
+
13
+ #
14
+ # Initializes context_metadata config object
15
+ #
16
+ # @param opts [Hash] options
17
+ #
18
+ # @return [Serega::SeregaPlugins::Metadata::MetadataConfig]
19
+ #
20
+ def initialize(opts)
21
+ @opts = opts
22
+ end
23
+
24
+ # @!method auto_preload_attributes_with_delegate
25
+ # @return [Boolean, nil] option value
26
+ #
27
+ # @!method auto_preload_attributes_with_delegate=(value)
28
+ # @param value [Boolean] New option value
29
+ # @return [Boolean] New option value
30
+ #
31
+ # @!method auto_preload_attributes_with_serializer
32
+ # @return [Boolean, nil] option value
33
+ #
34
+ # @!method auto_preload_attributes_with_serializer=(value)
35
+ # @param value [Boolean] New option value
36
+ # @return [Boolean] New option value
37
+ #
38
+ # @!method auto_hide_attributes_with_preload
39
+ # @return [Boolean, nil] option value
40
+ #
41
+ # @!method auto_hide_attributes_with_preload=(value)
42
+ # @param value [Boolean] New option value
43
+ # @return [Boolean] New option value
44
+ #
45
+ %i[
46
+ auto_preload_attributes_with_delegate
47
+ auto_preload_attributes_with_serializer
48
+ auto_hide_attributes_with_preload
49
+ ].each do |method_name|
50
+ define_method(method_name) do
51
+ opts.fetch(method_name)
52
+ end
53
+
54
+ define_method("#{method_name}=") do |value|
55
+ raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
56
+ opts[method_name] = value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end