serega 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/lib/serega/attribute.rb +130 -0
- data/lib/serega/config.rb +48 -0
- data/lib/serega/convert.rb +45 -0
- data/lib/serega/convert_item.rb +37 -0
- data/lib/serega/helpers/serializer_class_helper.rb +11 -0
- data/lib/serega/map.rb +49 -0
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +61 -0
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +95 -0
- data/lib/serega/plugins/context_metadata/context_metadata.rb +74 -0
- data/lib/serega/plugins/formatters/formatters.rb +49 -0
- data/lib/serega/plugins/hide_nil/hide_nil.rb +80 -0
- data/lib/serega/plugins/metadata/meta_attribute.rb +74 -0
- data/lib/serega/plugins/metadata/metadata.rb +123 -0
- data/lib/serega/plugins/metadata/validations/check_block.rb +45 -0
- data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +31 -0
- data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +31 -0
- data/lib/serega/plugins/metadata/validations/check_opts.rb +41 -0
- data/lib/serega/plugins/metadata/validations/check_path.rb +53 -0
- data/lib/serega/plugins/preloads/lib/enum_deep_freeze.rb +17 -0
- data/lib/serega/plugins/preloads/lib/format_user_preloads.rb +53 -0
- data/lib/serega/plugins/preloads/lib/main_preload_path.rb +40 -0
- data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +60 -0
- data/lib/serega/plugins/preloads/preloads.rb +100 -0
- data/lib/serega/plugins/preloads/validations/check_opt_preload_path.rb +38 -0
- data/lib/serega/plugins/presenter/presenter.rb +111 -0
- data/lib/serega/plugins/root/root.rb +65 -0
- data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +64 -0
- data/lib/serega/plugins/string_modifiers/string_modifiers.rb +32 -0
- data/lib/serega/plugins/validate_modifiers/validate.rb +51 -0
- data/lib/serega/plugins/validate_modifiers/validate_modifiers.rb +44 -0
- data/lib/serega/plugins.rb +51 -0
- data/lib/serega/utils/as_json.rb +35 -0
- data/lib/serega/utils/enum_deep_dup.rb +43 -0
- data/lib/serega/utils/to_hash.rb +52 -0
- data/lib/serega/utils/to_json.rb +22 -0
- data/lib/serega/validations/attribute/check_block.rb +81 -0
- data/lib/serega/validations/attribute/check_name.rb +45 -0
- data/lib/serega/validations/attribute/check_opt_hide.rb +20 -0
- data/lib/serega/validations/attribute/check_opt_many.rb +20 -0
- data/lib/serega/validations/attribute/check_opt_method.rb +25 -0
- data/lib/serega/validations/attribute/check_opt_serializer.rb +36 -0
- data/lib/serega/validations/attribute/check_opts.rb +36 -0
- data/lib/serega/validations/check_allowed_keys.rb +13 -0
- data/lib/serega/validations/check_opt_is_bool.rb +14 -0
- data/lib/serega/validations/check_opt_is_hash.rb +14 -0
- data/lib/serega/version.rb +5 -0
- data/lib/serega.rb +265 -0
- metadata +94 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
#
|
7
|
+
# Stores Attribute data
|
8
|
+
#
|
9
|
+
class MetaAttribute
|
10
|
+
#
|
11
|
+
# Stores Attribute instance methods
|
12
|
+
#
|
13
|
+
module InstanceMethods
|
14
|
+
# @return [Symbol] Meta attribute name
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# @return [Symbol] Meta attribute full path
|
18
|
+
attr_reader :path
|
19
|
+
|
20
|
+
# @return [Proc] Meta attribute options
|
21
|
+
attr_reader :opts
|
22
|
+
|
23
|
+
# @return [Proc] Meta attribute originally added block
|
24
|
+
attr_reader :block
|
25
|
+
|
26
|
+
#
|
27
|
+
# Initializes new meta attribute
|
28
|
+
#
|
29
|
+
# @param path [Array<Symbol, String>] Path for metadata of attribute
|
30
|
+
#
|
31
|
+
# @param opts [Hash] metadata attribute options
|
32
|
+
#
|
33
|
+
# @param block [Proc] Proc that receives object(s) and context and finds value
|
34
|
+
#
|
35
|
+
def initialize(path:, opts:, block:)
|
36
|
+
check(path, opts, block)
|
37
|
+
|
38
|
+
@name = path.join(".").to_sym
|
39
|
+
@path = Utils::EnumDeepDup.call(path)
|
40
|
+
@opts = Utils::EnumDeepDup.call(opts)
|
41
|
+
@block = block
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Finds attribute value
|
46
|
+
#
|
47
|
+
# @param object [Object] Serialized object(s)
|
48
|
+
# @param context [Hash, nil] Serialization context
|
49
|
+
#
|
50
|
+
# @return [Object] Serialized meta attribute value
|
51
|
+
#
|
52
|
+
def value(object, context)
|
53
|
+
block.call(object, context)
|
54
|
+
end
|
55
|
+
|
56
|
+
def hide?(value)
|
57
|
+
(opts[:hide_nil] && value.nil?) || (opts[:hide_empty] && value.empty?)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def check(path, opts, block)
|
63
|
+
CheckPath.call(path)
|
64
|
+
CheckOpts.call(opts, self.class.serializer_class.config[:metadata][:attribute_keys])
|
65
|
+
CheckBlock.call(block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
extend Serega::Helpers::SerializerClassHelper
|
70
|
+
include InstanceMethods
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
def self.plugin_name
|
7
|
+
:metadata
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.before_load_plugin(serializer_class, **opts)
|
11
|
+
serializer_class.plugin(:root, **opts) unless serializer_class.plugin_used?(:root)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_plugin(serializer_class, **_opts)
|
15
|
+
serializer_class.extend(ClassMethods)
|
16
|
+
serializer_class::Convert.include(ConvertInstanceMethods)
|
17
|
+
|
18
|
+
require_relative "./meta_attribute"
|
19
|
+
require_relative "./validations/check_block"
|
20
|
+
require_relative "./validations/check_opt_hide_nil"
|
21
|
+
require_relative "./validations/check_opt_hide_empty"
|
22
|
+
require_relative "./validations/check_opts"
|
23
|
+
require_relative "./validations/check_path"
|
24
|
+
|
25
|
+
meta_attribute_class = Class.new(MetaAttribute)
|
26
|
+
meta_attribute_class.serializer_class = serializer_class
|
27
|
+
serializer_class.const_set(:MetaAttribute, meta_attribute_class)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.after_load_plugin(serializer_class, **_opts)
|
31
|
+
serializer_class.config[plugin_name] = {attribute_keys: %i[path hide_nil hide_empty]}
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
private def inherited(subclass)
|
36
|
+
super
|
37
|
+
|
38
|
+
meta_attribute_class = Class.new(self::MetaAttribute)
|
39
|
+
meta_attribute_class.serializer_class = subclass
|
40
|
+
subclass.const_set(:MetaAttribute, meta_attribute_class)
|
41
|
+
|
42
|
+
# Assign same metadata attributes
|
43
|
+
meta_attributes.each_value do |attr|
|
44
|
+
subclass.meta_attribute(*attr.path, **attr.opts, &attr.block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# List of added metadata attributes
|
50
|
+
#
|
51
|
+
# @return [Array] Added metadata attributes
|
52
|
+
#
|
53
|
+
def meta_attributes
|
54
|
+
@meta_attributes ||= {}
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Adds metadata to response
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# class AppSerializer < Serega
|
62
|
+
#
|
63
|
+
# meta_attribute(:version) { '1.2.3' }
|
64
|
+
#
|
65
|
+
# meta_attribute(:meta, :paging, hide_nil: true, hide_empty: true) do |scope, ctx|
|
66
|
+
# { page: scope.page, per_page: scope.per_page, total_count: scope.total_count }
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# @param *path [Array<String, Symbol>] Metadata attribute path keys
|
71
|
+
# @param **opts [Hash] Metadata attribute options
|
72
|
+
# @param &block [Proc] Metadata attribute value
|
73
|
+
#
|
74
|
+
# @return [MetadataAttribute] Added metadata attribute
|
75
|
+
#
|
76
|
+
def meta_attribute(*path, **opts, &block)
|
77
|
+
attribute = self::MetaAttribute.new(path: path, opts: opts, block: block)
|
78
|
+
meta_attributes[attribute.name] = attribute
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module ConvertInstanceMethods
|
83
|
+
def to_h
|
84
|
+
hash = super
|
85
|
+
add_metadata(hash)
|
86
|
+
hash
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def add_metadata(hash)
|
92
|
+
self.class.serializer_class.meta_attributes.each_value do |meta_attribute|
|
93
|
+
metadata = meta_attribute_value(meta_attribute)
|
94
|
+
next unless metadata
|
95
|
+
|
96
|
+
deep_merge_metadata(hash, metadata)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def meta_attribute_value(meta_attribute)
|
101
|
+
value = meta_attribute.value(object, opts[:context])
|
102
|
+
return if meta_attribute.hide?(value)
|
103
|
+
|
104
|
+
# Example:
|
105
|
+
# [:foo, :bar].reverse_each.inject(:bazz) { |val, key| { key => val } } # => { foo: { bar: :bazz } }
|
106
|
+
meta_attribute.path.reverse_each.inject(value) { |val, key| {key => val} }
|
107
|
+
end
|
108
|
+
|
109
|
+
def deep_merge_metadata(hash, metadata)
|
110
|
+
hash.merge!(metadata) do |_key, this_val, other_val|
|
111
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
112
|
+
deep_merge_metadata(this_val, other_val)
|
113
|
+
else
|
114
|
+
other_val
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
register_plugin(Metadata.plugin_name, Metadata)
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
class MetaAttribute
|
7
|
+
class CheckBlock
|
8
|
+
ALLOWED_PARAM_TYPES = %i[opt req]
|
9
|
+
private_constant :ALLOWED_PARAM_TYPES
|
10
|
+
|
11
|
+
class << self
|
12
|
+
#
|
13
|
+
# Checks block provided with attribute
|
14
|
+
# Block must have up to two arguments - object and context.
|
15
|
+
# It should not have any *rest or **key arguments
|
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[:with_paging] }
|
25
|
+
#
|
26
|
+
# @param block [Proc] Block that returns serialized meta attribute value
|
27
|
+
#
|
28
|
+
# @raise [Error] Error that block has invalid arguments
|
29
|
+
#
|
30
|
+
# @return [void]
|
31
|
+
#
|
32
|
+
def call(block)
|
33
|
+
raise Error, "Block must be provided when defining meta attribute" unless block
|
34
|
+
|
35
|
+
params = block.parameters
|
36
|
+
return if (params.count <= 2) && params.all? { |par| ALLOWED_PARAM_TYPES.include?(par[0]) }
|
37
|
+
|
38
|
+
raise Error, "Block can have maximum 2 regular parameters (no **keyword or *array args)"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
class MetaAttribute
|
7
|
+
class CheckOptHideEmpty
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Checks attribute :after_hide_if option
|
11
|
+
#
|
12
|
+
# @param opts [Hash] Attribute options
|
13
|
+
#
|
14
|
+
# @raise [Error] Error that option has invalid value
|
15
|
+
#
|
16
|
+
# @return [void]
|
17
|
+
#
|
18
|
+
def call(opts)
|
19
|
+
return unless opts.key?(:hide_empty)
|
20
|
+
|
21
|
+
value = opts[:hide_empty]
|
22
|
+
return if value == true
|
23
|
+
|
24
|
+
raise Error, "Invalid option :hide_empty => #{value.inspect}. Must be true"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
class MetaAttribute
|
7
|
+
class CheckOptHideNil
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Checks attribute :after_hide_if option
|
11
|
+
#
|
12
|
+
# @param opts [Hash] Attribute options
|
13
|
+
#
|
14
|
+
# @raise [Error] Error that option has invalid value
|
15
|
+
#
|
16
|
+
# @return [void]
|
17
|
+
#
|
18
|
+
def call(opts)
|
19
|
+
return unless opts.key?(:hide_nil)
|
20
|
+
|
21
|
+
value = opts[:hide_nil]
|
22
|
+
return if value == true
|
23
|
+
|
24
|
+
raise Error, "Invalid option :hide_nil => #{value.inspect}. Must be true"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
class MetaAttribute
|
7
|
+
class CheckOpts
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Validates attribute options
|
11
|
+
# Checks used options are allowed and then checks options values.
|
12
|
+
#
|
13
|
+
# @param opts [Hash] Attribute options
|
14
|
+
# @param attribute_keys [Array<Symbol>] Allowed options keys
|
15
|
+
#
|
16
|
+
# @raise [Error] when attribute has invalid options
|
17
|
+
#
|
18
|
+
# @return [void]
|
19
|
+
#
|
20
|
+
def call(opts, attribute_keys)
|
21
|
+
opts.each_key do |key|
|
22
|
+
next if attribute_keys.include?(key.to_sym)
|
23
|
+
|
24
|
+
raise Error, "Invalid option #{key.inspect}. Allowed options are: #{attribute_keys.map(&:inspect).join(", ")}"
|
25
|
+
end
|
26
|
+
|
27
|
+
check_each_opt(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def check_each_opt(opts)
|
33
|
+
CheckOptHideEmpty.call(opts)
|
34
|
+
CheckOptHideNil.call(opts)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Metadata
|
6
|
+
class MetaAttribute
|
7
|
+
class CheckPath
|
8
|
+
FORMAT_ONE_CHAR = /\A[a-zA-Z0-9]\z/
|
9
|
+
FORMAT_MANY_CHARS = /\A[a-zA-Z0-9][a-zA-Z0-9_-]*?[a-zA-Z0-9]\z/ # allow '-' and '_' in the middle
|
10
|
+
|
11
|
+
private_constant :FORMAT_ONE_CHAR, :FORMAT_MANY_CHARS
|
12
|
+
|
13
|
+
class << self
|
14
|
+
#
|
15
|
+
# Checks allowed characters in specified metadata path parts.
|
16
|
+
# Globally allowed characters: "a-z", "A-Z", "0-9".
|
17
|
+
# Minus and low line "-", "_" also allowed except as the first or last character.
|
18
|
+
#
|
19
|
+
# @param path [Array<String, Symbol>] Metadata attribute path names
|
20
|
+
#
|
21
|
+
# @raise [Error] when metadata attribute name has invalid format
|
22
|
+
# @return [void]
|
23
|
+
#
|
24
|
+
def call(path)
|
25
|
+
path.each { |attr_name| check_name(attr_name) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def check_name(name)
|
31
|
+
name = name.to_s
|
32
|
+
|
33
|
+
valid =
|
34
|
+
case name.size
|
35
|
+
when 0 then false
|
36
|
+
when 1 then name.match?(FORMAT_ONE_CHAR)
|
37
|
+
else name.match?(FORMAT_MANY_CHARS)
|
38
|
+
end
|
39
|
+
|
40
|
+
return if valid
|
41
|
+
|
42
|
+
raise Error, message(name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def message(name)
|
46
|
+
%(Invalid metadata path #{name.inspect}, globally allowed characters: "a-z", "A-Z", "0-9". Minus and low line "-", "_" also allowed except as the first or last character)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
# Freezes nested enumerable data
|
7
|
+
class EnumDeepFreeze
|
8
|
+
class << self
|
9
|
+
def call(data)
|
10
|
+
data.each_entry { |entry| call(entry) } if data.is_a?(Enumerable)
|
11
|
+
data.freeze
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
# Transforms user provided preloads to hash
|
7
|
+
class FormatUserPreloads
|
8
|
+
METHODS = {
|
9
|
+
Array => :array_to_hash,
|
10
|
+
FalseClass => :nil_to_hash,
|
11
|
+
Hash => :hash_to_hash,
|
12
|
+
NilClass => :nil_to_hash,
|
13
|
+
String => :string_to_hash,
|
14
|
+
Symbol => :symbol_to_hash
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def call(value)
|
19
|
+
send(METHODS.fetch(value.class), value)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def array_to_hash(values)
|
25
|
+
values.each_with_object({}) do |value, obj|
|
26
|
+
obj.merge!(call(value))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash_to_hash(values)
|
31
|
+
values.each_with_object({}) do |(key, value), obj|
|
32
|
+
obj[key.to_sym] = call(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def nil_to_hash(_value)
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
|
40
|
+
def string_to_hash(value)
|
41
|
+
symbol_to_hash(value.to_sym)
|
42
|
+
end
|
43
|
+
|
44
|
+
def symbol_to_hash(value)
|
45
|
+
{value => {}}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extend ClassMethods
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
class MainPreloadPath
|
7
|
+
module ClassMethods
|
8
|
+
# @param preloads [Hash] Formatted user provided preloads hash
|
9
|
+
def call(preloads)
|
10
|
+
return FROZEN_EMPTY_ARRAY if preloads.empty?
|
11
|
+
|
12
|
+
main_path(preloads)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Generates path (Array) to the last included resource.
|
18
|
+
# We need to know this path to include nested associations.
|
19
|
+
#
|
20
|
+
# main_path(a: { b: { c: {} }, d: {} }) # => [:a, :d]
|
21
|
+
#
|
22
|
+
def main_path(hash, path = [])
|
23
|
+
current_level = path.size
|
24
|
+
|
25
|
+
hash.each do |key, data|
|
26
|
+
path.pop(path.size - current_level)
|
27
|
+
path << key
|
28
|
+
|
29
|
+
main_path(data, path)
|
30
|
+
end
|
31
|
+
|
32
|
+
path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
extend ClassMethods
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
#
|
7
|
+
# Finds relations to preload for provided serializer
|
8
|
+
#
|
9
|
+
class PreloadsConstructor
|
10
|
+
module ClassMethods
|
11
|
+
#
|
12
|
+
# Constructs preloads hash for given serializer
|
13
|
+
#
|
14
|
+
# @param serializer [Serega] Instance of Serega serializer
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
#
|
18
|
+
def call(map)
|
19
|
+
preloads = {}
|
20
|
+
append_many(preloads, map)
|
21
|
+
preloads
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def append_many(preloads, map)
|
27
|
+
map.each do |attribute, nested_map|
|
28
|
+
current_preloads = attribute.preloads
|
29
|
+
next unless current_preloads
|
30
|
+
|
31
|
+
has_nested = nested_map.any?
|
32
|
+
current_preloads = Utils::EnumDeepDup.call(current_preloads) if has_nested
|
33
|
+
append_current(preloads, current_preloads)
|
34
|
+
next unless has_nested
|
35
|
+
|
36
|
+
nested_preloads = nested(preloads, attribute.preloads_path)
|
37
|
+
append_many(nested_preloads, nested_map)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def append_current(preloads, current_preloads)
|
42
|
+
merge(preloads, current_preloads) unless current_preloads.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def merge(preloads, current_preloads)
|
46
|
+
preloads.merge!(current_preloads) do |_key, value_one, value_two|
|
47
|
+
merge(value_one, value_two)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def nested(preloads, path)
|
52
|
+
!path || path.empty? ? preloads : preloads.dig(*path)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
extend ClassMethods
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# Plugin adds `.preloads` method to find relations that must be preloaded
|
7
|
+
#
|
8
|
+
module Preloads
|
9
|
+
# @return [Symbol] plugin name
|
10
|
+
def self.plugin_name
|
11
|
+
:preloads
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Includes plugin modules to current serializer
|
16
|
+
#
|
17
|
+
# @param serializer_class [Class] current serializer class
|
18
|
+
# @param _opts [Hash] plugin opts
|
19
|
+
#
|
20
|
+
# @return [void]
|
21
|
+
#
|
22
|
+
def self.load_plugin(serializer_class, **_opts)
|
23
|
+
serializer_class.include(InstanceMethods)
|
24
|
+
serializer_class::Attribute.include(AttributeMethods)
|
25
|
+
|
26
|
+
serializer_class::Attribute::CheckOpts.extend(CheckOptsClassMethods)
|
27
|
+
|
28
|
+
require_relative "./lib/enum_deep_freeze"
|
29
|
+
require_relative "./lib/format_user_preloads"
|
30
|
+
require_relative "./lib/main_preload_path"
|
31
|
+
require_relative "./lib/preloads_constructor"
|
32
|
+
require_relative "./validations/check_opt_preload_path"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.after_load_plugin(serializer_class, **opts)
|
36
|
+
config = serializer_class.config
|
37
|
+
config[:attribute_keys] += [:preload, :preload_path]
|
38
|
+
config[:preloads] = {auto_preload_relations: opts.fetch(:auto_preload_relations, true)}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adds #preloads instance method
|
42
|
+
module InstanceMethods
|
43
|
+
# @return [Hash] relations that can be preloaded to omit N+1
|
44
|
+
def preloads
|
45
|
+
@preloads ||= PreloadsConstructor.call(map)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds #preloads and #preloads_path Attribute instance method
|
50
|
+
module AttributeMethods
|
51
|
+
def preloads
|
52
|
+
return @preloads if defined?(@preloads)
|
53
|
+
|
54
|
+
@preloads = get_preloads
|
55
|
+
end
|
56
|
+
|
57
|
+
def preloads_path
|
58
|
+
return @preloads_path if defined?(@preloads_path)
|
59
|
+
|
60
|
+
@preloads_path = get_preloads_path
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def get_preloads
|
66
|
+
preloads_provided = opts.key?(:preload)
|
67
|
+
preloads =
|
68
|
+
if preloads_provided
|
69
|
+
opts[:preload]
|
70
|
+
elsif relation? && self.class.serializer_class.config[:preloads][:auto_preload_relations]
|
71
|
+
key
|
72
|
+
end
|
73
|
+
|
74
|
+
# Nil and empty hash differs as we can preload nested results to
|
75
|
+
# empty hash, but we will skip nested preloading if nil or false provided
|
76
|
+
return if preloads_provided && !preloads
|
77
|
+
|
78
|
+
FormatUserPreloads.call(preloads)
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_preloads_path
|
82
|
+
path = Array(opts[:preload_path]).map!(&:to_sym)
|
83
|
+
path = MainPreloadPath.call(preloads) if path.empty?
|
84
|
+
EnumDeepFreeze.call(path)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module CheckOptsClassMethods
|
89
|
+
private
|
90
|
+
|
91
|
+
def check_each_opt(opts)
|
92
|
+
super
|
93
|
+
CheckOptPreloadPath.call(opts)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
register_plugin(Preloads.plugin_name, Preloads)
|
99
|
+
end
|
100
|
+
end
|