serega 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|