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,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Preloads
|
6
|
+
class CheckOptPreloadPath
|
7
|
+
class << self
|
8
|
+
def call(opts)
|
9
|
+
return unless opts.key?(:preload_path)
|
10
|
+
|
11
|
+
value = opts[:preload_path]
|
12
|
+
raise Error, "Invalid option :preload_path => #{value.inspect}. Can be provided only when :preload option provided" unless opts[:preload]
|
13
|
+
raise Error, "Invalid option :preload_path => #{value.inspect}. Can be provided only when :serializer option provided" unless opts[:serializer]
|
14
|
+
|
15
|
+
path = Array(value).map!(&:to_sym)
|
16
|
+
preloads = FormatUserPreloads.call(opts[:preload])
|
17
|
+
allowed_paths = paths(preloads)
|
18
|
+
raise Error, "Invalid option :preload_path => #{value.inspect}. Can be one of #{allowed_paths.inspect[1..-2]}" unless allowed_paths.include?(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def paths(preloads, path = [], result = [])
|
24
|
+
preloads.each do |key, nested_preloads|
|
25
|
+
path << key
|
26
|
+
result << path.dup
|
27
|
+
|
28
|
+
paths(nested_preloads, path, result)
|
29
|
+
path.pop
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
class Serega
|
7
|
+
module Plugins
|
8
|
+
#
|
9
|
+
# Plugin Presenter adds possibility to use declare Presenter for your objects inside serializer
|
10
|
+
#
|
11
|
+
# class User < Serega
|
12
|
+
# plugin :presenter
|
13
|
+
#
|
14
|
+
# attribute :name
|
15
|
+
#
|
16
|
+
# class Presenter
|
17
|
+
# def name
|
18
|
+
# [first_name, last_name].compact_blank.join(' ')
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
module Presenter
|
23
|
+
# @return [Symbol] plugin name
|
24
|
+
def self.plugin_name
|
25
|
+
:presenter
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Loads plugin
|
30
|
+
#
|
31
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
32
|
+
# @param _opts [Hash] Loaded plugins options
|
33
|
+
#
|
34
|
+
# @return [void]
|
35
|
+
#
|
36
|
+
def self.load_plugin(serializer_class, **_opts)
|
37
|
+
serializer_class.extend(ClassMethods)
|
38
|
+
serializer_class::ConvertItem.extend(ConvertItemClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Adds Presenter to current serializer
|
43
|
+
#
|
44
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
45
|
+
# @param _opts [Hash] Loaded plugins options
|
46
|
+
#
|
47
|
+
# @return [void]
|
48
|
+
#
|
49
|
+
def self.after_load_plugin(serializer_class, **_opts)
|
50
|
+
presenter_class = Class.new(Presenter)
|
51
|
+
presenter_class.serializer_class = serializer_class
|
52
|
+
serializer_class.const_set(:Presenter, presenter_class)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Presenter class
|
56
|
+
class Presenter < SimpleDelegator
|
57
|
+
# Presenter instance methods
|
58
|
+
module InstanceMethods
|
59
|
+
#
|
60
|
+
# Delegates all missing methods to serialized object.
|
61
|
+
#
|
62
|
+
# Creates delegator method after first #method_missing hit to improve
|
63
|
+
# performance of following serializations.
|
64
|
+
#
|
65
|
+
def method_missing(name, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing (base SimpleDelegator class has this method)
|
66
|
+
super.tap do
|
67
|
+
self.class.def_delegator :__getobj__, name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
extend Helpers::SerializerClassHelper
|
73
|
+
extend Forwardable
|
74
|
+
include InstanceMethods
|
75
|
+
end
|
76
|
+
|
77
|
+
# Overrides class methods of included class
|
78
|
+
module ClassMethods
|
79
|
+
private def inherited(subclass)
|
80
|
+
super
|
81
|
+
|
82
|
+
presenter_class = Class.new(self::Presenter)
|
83
|
+
presenter_class.serializer_class = subclass
|
84
|
+
subclass.const_set(:Presenter, presenter_class)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Overrides {Serega::ClassMethods#attribute} method, additionally adds method
|
88
|
+
# to Presenter to not hit {Serega::Plugins::Presenter::Presenter#method_missing}
|
89
|
+
# @see Serega::ClassMethods#attribute
|
90
|
+
def attribute(_name, **_opts, &_block)
|
91
|
+
super.tap do |attribute|
|
92
|
+
self::Presenter.def_delegator(:__getobj__, attribute.key) unless attribute.block
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Includes methods to override ConvertItem class
|
98
|
+
module ConvertItemClassMethods
|
99
|
+
#
|
100
|
+
# Replaces serialized object with Presenter.new(object)
|
101
|
+
#
|
102
|
+
def call(object, *)
|
103
|
+
object = serializer_class::Presenter.new(object)
|
104
|
+
super
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
register_plugin(Presenter.plugin_name, Presenter)
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Root
|
6
|
+
# @return [Symbol] Default response root key
|
7
|
+
ROOT_DEFAULT = :data
|
8
|
+
|
9
|
+
def self.plugin_name
|
10
|
+
:root
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load_plugin(serializer_class, **_opts)
|
14
|
+
serializer_class.extend(ClassMethods)
|
15
|
+
serializer_class::Convert.include(ConvertInstanceMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.after_load_plugin(serializer_class, **opts)
|
19
|
+
serializer_class.root(opts[:root] || ROOT_DEFAULT, one: opts[:root_one], many: opts[:root_many])
|
20
|
+
serializer_class.config[:serialize_keys] << :root
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
#
|
25
|
+
# Configures response root key
|
26
|
+
#
|
27
|
+
# @param root [String, Symbol] Specifies common root when serializing one or multiple objects
|
28
|
+
# @param root_one [String, Symbol] Specifies root when serializing one object
|
29
|
+
# @param root_many [String, Symbol] Specifies root when serializing multiple objects
|
30
|
+
#
|
31
|
+
# @return [Hash] Configured root names
|
32
|
+
#
|
33
|
+
def root(root = nil, one: nil, many: nil)
|
34
|
+
one ||= root
|
35
|
+
many ||= root
|
36
|
+
|
37
|
+
one = one.to_sym if one
|
38
|
+
many = many.to_sym if many
|
39
|
+
|
40
|
+
config[:root] = {one: one, many: many}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ConvertInstanceMethods
|
45
|
+
def to_h
|
46
|
+
hash = super
|
47
|
+
root = build_root(opts)
|
48
|
+
hash = {root => hash} if root
|
49
|
+
hash
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def build_root(opts)
|
55
|
+
return opts[:root] if opts.key?(:root)
|
56
|
+
|
57
|
+
root_config = self.class.serializer_class.config[:root]
|
58
|
+
many? ? root_config[:many] : root_config[:one]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
register_plugin(Root.plugin_name, Root)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module StringModifiers
|
6
|
+
class ParseStringModifiers
|
7
|
+
COMMA = ","
|
8
|
+
OPEN_BRACKET = "("
|
9
|
+
CLOSE_BRACKET = ")"
|
10
|
+
|
11
|
+
def self.call(fields)
|
12
|
+
return fields unless fields.is_a?(String)
|
13
|
+
|
14
|
+
new.parse(fields)
|
15
|
+
end
|
16
|
+
|
17
|
+
# user => { user: {} }
|
18
|
+
# user(id) => { user: { id: {} } }
|
19
|
+
# user(id,name) => { user: { id: {}, name: {} } }
|
20
|
+
# user,comments => { user: {}, comments: {} }
|
21
|
+
# user(comments(text)) => { user: { comments: { text: {} } } }
|
22
|
+
def parse(fields)
|
23
|
+
res = {}
|
24
|
+
attribute = +""
|
25
|
+
path_stack = nil
|
26
|
+
|
27
|
+
fields.each_char do |char|
|
28
|
+
case char
|
29
|
+
when COMMA
|
30
|
+
add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
|
31
|
+
when CLOSE_BRACKET
|
32
|
+
add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
|
33
|
+
path_stack&.pop
|
34
|
+
when OPEN_BRACKET
|
35
|
+
name = add_attribute(res, path_stack, attribute, {})
|
36
|
+
(path_stack ||= []).push(name) if name
|
37
|
+
else
|
38
|
+
attribute.insert(-1, char)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
|
43
|
+
|
44
|
+
res
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def add_attribute(res, path_stack, attribute, nested_attributes = FROZEN_EMPTY_HASH)
|
50
|
+
attribute.strip!
|
51
|
+
return if attribute.empty?
|
52
|
+
|
53
|
+
name = attribute.to_sym
|
54
|
+
attribute.clear
|
55
|
+
|
56
|
+
current_attrs = !path_stack || path_stack.empty? ? res : res.dig(*path_stack)
|
57
|
+
current_attrs[name] = nested_attributes
|
58
|
+
|
59
|
+
name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module StringModifiers
|
6
|
+
def self.plugin_name
|
7
|
+
:string_modifiers
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_plugin(serializer_class, **_opts)
|
11
|
+
serializer_class.include(InstanceMethods)
|
12
|
+
require_relative "./parse_string_modifiers"
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
private
|
17
|
+
|
18
|
+
def prepare_modifiers(opts)
|
19
|
+
opts = {
|
20
|
+
only: ParseStringModifiers.call(opts[:only]),
|
21
|
+
except: ParseStringModifiers.call(opts[:except]),
|
22
|
+
with: ParseStringModifiers.call(opts[:with])
|
23
|
+
}
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register_plugin(StringModifiers.plugin_name, StringModifiers)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module ValidateModifiers
|
6
|
+
class Validate
|
7
|
+
class << self
|
8
|
+
def call(serializer_class, fields)
|
9
|
+
return unless fields
|
10
|
+
|
11
|
+
validate(serializer_class, fields, [])
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def validate(serializer_class, fields, prev_names)
|
17
|
+
fields.each do |name, nested_fields|
|
18
|
+
attribute = serializer_class.attributes[name]
|
19
|
+
|
20
|
+
raise_error(name, prev_names) unless attribute
|
21
|
+
next if nested_fields.empty?
|
22
|
+
|
23
|
+
raise_nested_error(name, prev_names, nested_fields) unless attribute.relation?
|
24
|
+
nested_serializer = attribute.serializer
|
25
|
+
validate(nested_serializer, nested_fields, prev_names + [name])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def raise_error(name, prev_names)
|
30
|
+
field_name = field_name(name, prev_names)
|
31
|
+
|
32
|
+
raise Serega::Error, "Attribute #{field_name} not exists"
|
33
|
+
end
|
34
|
+
|
35
|
+
def raise_nested_error(name, prev_names, nested_fields)
|
36
|
+
field_name = field_name(name, prev_names)
|
37
|
+
first_nested = nested_fields.keys.first
|
38
|
+
|
39
|
+
raise Serega::Error, "Attribute #{field_name} is not a relation to add '#{first_nested}' attribute"
|
40
|
+
end
|
41
|
+
|
42
|
+
def field_name(name, prev_names)
|
43
|
+
res = "'#{name}'"
|
44
|
+
res += " ('#{prev_names.join(".")}.#{name}')" if prev_names.any?
|
45
|
+
res
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module ValidateModifiers
|
6
|
+
def self.plugin_name
|
7
|
+
:validate_modifiers
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_plugin(serializer_class, **_opts)
|
11
|
+
serializer_class.include(InstanceMethods)
|
12
|
+
require_relative "./validate"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.after_load_plugin(serializer_class, **opts)
|
16
|
+
serializer_class.config[:validate_modifiers] = {auto: opts.fetch(:auto, true)}
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
# Raises error if some modifiers are invalid
|
21
|
+
def validate_modifiers
|
22
|
+
@modifiers_validated ||= begin
|
23
|
+
Validate.call(self.class, opts[:only])
|
24
|
+
Validate.call(self.class, opts[:except])
|
25
|
+
Validate.call(self.class, opts[:with])
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def initialize(opts)
|
33
|
+
super
|
34
|
+
validate_modifiers if self.class.config[:validate_modifiers][:auto]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethods
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
register_plugin(ValidateModifiers.plugin_name, ValidateModifiers)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
# Module in which all Serega plugins should be stored
|
5
|
+
module Plugins
|
6
|
+
@plugins = {}
|
7
|
+
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Registers given plugin to be able to load it using symbol name.
|
11
|
+
#
|
12
|
+
# @example Register plugin
|
13
|
+
# Serega::Plugins.register_plugin(:plugin_name, PluginModule)
|
14
|
+
def register_plugin(name, mod)
|
15
|
+
@plugins[name] = mod
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Loads plugin code and returns plugin core module.
|
20
|
+
#
|
21
|
+
# @param name [Symbol, Module] plugin name or plugin itself
|
22
|
+
#
|
23
|
+
# @raise [Error] Raises Error when plugin was not found
|
24
|
+
#
|
25
|
+
# @example Find plugin when providing name
|
26
|
+
# Serega::Plugins.find_plugin(:presenter) # => Serega::Plugins::Presenter
|
27
|
+
#
|
28
|
+
# @example Find plugin when providing plugin itself
|
29
|
+
# Serega::Plugins.find_plugin(Presenter) # => Presenter
|
30
|
+
#
|
31
|
+
# @return [Class<Module>] Plugin core module
|
32
|
+
#
|
33
|
+
def find_plugin(name)
|
34
|
+
return name if name.is_a?(Module)
|
35
|
+
return @plugins[name] if @plugins.key?(name)
|
36
|
+
|
37
|
+
require_plugin(name)
|
38
|
+
|
39
|
+
@plugins[name] || raise(Error, "Plugin '#{name}' did not register itself correctly")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def require_plugin(name)
|
45
|
+
require "serega/plugins/#{name}/#{name}"
|
46
|
+
rescue LoadError
|
47
|
+
raise Error, "Plugin '#{name}' does not exist"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Utils
|
5
|
+
class AsJSON
|
6
|
+
DOUBLE_QUOTE = '"'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def call(data, to_json:)
|
10
|
+
case data
|
11
|
+
when Hash
|
12
|
+
data.each_with_object({}) do |(key, value), new_data|
|
13
|
+
new_key = key.to_s
|
14
|
+
new_value = call(value, to_json: to_json)
|
15
|
+
new_data[new_key] = new_value
|
16
|
+
end
|
17
|
+
when Array
|
18
|
+
data.map { |value| call(value, to_json: to_json) }
|
19
|
+
when NilClass, Integer, Float, String, TrueClass, FalseClass
|
20
|
+
data
|
21
|
+
when Symbol
|
22
|
+
data.to_s
|
23
|
+
else
|
24
|
+
res = to_json.call(data)
|
25
|
+
if res.start_with?(DOUBLE_QUOTE) && res.end_with?(DOUBLE_QUOTE)
|
26
|
+
res.delete_prefix!(DOUBLE_QUOTE)
|
27
|
+
res.delete_suffix!(DOUBLE_QUOTE)
|
28
|
+
end
|
29
|
+
res
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Utils
|
5
|
+
# Duplicates nested hashes and arrays
|
6
|
+
class EnumDeepDup
|
7
|
+
DUP = {
|
8
|
+
Hash => ->(data) { dup_hash_values(data) },
|
9
|
+
Array => ->(data) { dup_array_values(data) }
|
10
|
+
}.freeze
|
11
|
+
private_constant :DUP
|
12
|
+
|
13
|
+
class << self
|
14
|
+
#
|
15
|
+
# Deeply duplicate provided data
|
16
|
+
#
|
17
|
+
# @param data [Hash, Array] Data to duplicate
|
18
|
+
#
|
19
|
+
# @return [Hash, Array] Duplicated data
|
20
|
+
#
|
21
|
+
def call(data)
|
22
|
+
duplicate_data = data.dup
|
23
|
+
DUP.fetch(duplicate_data.class).call(duplicate_data)
|
24
|
+
duplicate_data
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def dup_hash_values(duplicate_data)
|
30
|
+
duplicate_data.each do |key, value|
|
31
|
+
duplicate_data[key] = call(value) if value.is_a?(Enumerable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def dup_array_values(duplicate_data)
|
36
|
+
duplicate_data.each_with_index do |value, index|
|
37
|
+
duplicate_data[index] = call(value) if value.is_a?(Enumerable)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Utils
|
5
|
+
class ToHash
|
6
|
+
module ClassMethods
|
7
|
+
def call(value)
|
8
|
+
case value
|
9
|
+
when Array then array_to_hash(value)
|
10
|
+
when Hash then hash_to_hash(value)
|
11
|
+
when NilClass, FalseClass then nil_to_hash(value)
|
12
|
+
when String then string_to_hash(value)
|
13
|
+
when Symbol then symbol_to_hash(value)
|
14
|
+
else raise Error, "Cant convert #{value.class} class object to hash"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def array_to_hash(values)
|
21
|
+
return Serega::FROZEN_EMPTY_HASH if values.empty?
|
22
|
+
|
23
|
+
values.each_with_object({}) do |value, obj|
|
24
|
+
obj.merge!(call(value))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def hash_to_hash(values)
|
29
|
+
return Serega::FROZEN_EMPTY_HASH if values.empty?
|
30
|
+
|
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
|
+
Serega::FROZEN_EMPTY_HASH
|
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 => Serega::FROZEN_EMPTY_HASH}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extend ClassMethods
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Utils
|
5
|
+
class ToJSON
|
6
|
+
class << self
|
7
|
+
def call(data)
|
8
|
+
json_adapter.dump(data)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def json_adapter
|
14
|
+
@json_adapter ||= begin
|
15
|
+
require "json"
|
16
|
+
::JSON
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
class Attribute
|
5
|
+
class CheckBlock
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# Checks :value option or a block provided with attribute
|
9
|
+
# Must have up to two arguments - object and context.
|
10
|
+
# It should not have any *rest or **key arguments
|
11
|
+
#
|
12
|
+
# @example without arguments
|
13
|
+
# attribute(:email) { CONSTANT_EMAIL }
|
14
|
+
#
|
15
|
+
# @example with one argument
|
16
|
+
# attribute(:email) { |obj| obj.confirmed_email }
|
17
|
+
#
|
18
|
+
# @example with two arguments
|
19
|
+
# attribute(:email) { |obj, context| context['is_current'] ? obj.email : nil }
|
20
|
+
#
|
21
|
+
# @param opts [Proc] Attribute opts, we will check :value option
|
22
|
+
# @param block [Proc] Block that returns serialized attribute value
|
23
|
+
#
|
24
|
+
# @raise [Error] Error that block has invalid arguments
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
#
|
28
|
+
def call(opts, block)
|
29
|
+
check_both_provided(opts, block)
|
30
|
+
|
31
|
+
if block
|
32
|
+
check_block(block)
|
33
|
+
elsif opts.key?(:value)
|
34
|
+
check_value(opts[:value])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def check_both_provided(opts, block)
|
41
|
+
if opts.key?(:value) && block
|
42
|
+
raise Error, both_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_block(block)
|
47
|
+
params = block.parameters
|
48
|
+
return if (params.count <= 2) && params.all? { |par| par[0] == :opt }
|
49
|
+
|
50
|
+
raise Error, block_error
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_value(value)
|
54
|
+
raise Error, value_error unless value.is_a?(Proc)
|
55
|
+
|
56
|
+
params = value.parameters
|
57
|
+
|
58
|
+
if value.lambda?
|
59
|
+
return if (params.count == 2) && params.all? { |par| par[0] == :req }
|
60
|
+
elsif (params.count <= 2) && params.all? { |par| par[0] == :opt }
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
raise Error, value_error
|
65
|
+
end
|
66
|
+
|
67
|
+
def block_error
|
68
|
+
"Block can have maximum two regular parameters (no **keyword or *array args)"
|
69
|
+
end
|
70
|
+
|
71
|
+
def value_error
|
72
|
+
"Option :value must be a Proc that is able to accept two parameters (no **keyword or *array args)"
|
73
|
+
end
|
74
|
+
|
75
|
+
def both_error
|
76
|
+
"Block and a :value option can not be provided together"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|