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,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
|