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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ac327fb1d98a08b76fcdfdc68166265bea134f298892f0a2f9b1a25c7d1e6063
|
4
|
+
data.tar.gz: 9843b54690c3ab5e4c4ad0c651e5e7629915eb502a0913520cec9153e6ebfa9f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7260b7ff2a939f93510565e50a19e6ad3c4fe6899c8ddb927938714bf422d0faedab7e62c638f18e98af13b6f9403cd30d4d72088dd6209f083ad4338f4d64da
|
7
|
+
data.tar.gz: b54726235c873c143fc3458837f1ecce05ffa26229d37e6b7420b304b8f1bff3bdde9e4a73fcd91c0e19664918cd2080d708e105398d4d30f15e6b440a7d737f
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
#
|
5
|
+
# Stores Attribute data
|
6
|
+
#
|
7
|
+
class Attribute
|
8
|
+
#
|
9
|
+
# Stores Attribute instance methods
|
10
|
+
#
|
11
|
+
module AttributeInstanceMethods
|
12
|
+
# @return [Symbol] Attribute name
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
# @return [Hash] Attribute options
|
16
|
+
attr_reader :opts
|
17
|
+
|
18
|
+
# @return [Proc] Attribute originally added block
|
19
|
+
attr_reader :block
|
20
|
+
|
21
|
+
#
|
22
|
+
# Initializes new attribute
|
23
|
+
#
|
24
|
+
# @param name [Symbol, String] Name of attribute
|
25
|
+
#
|
26
|
+
# @param opts [Hash] Attribute options
|
27
|
+
# @option opts [Symbol] :key Object instance method name to get attribute value
|
28
|
+
# @option opts [Boolean] :exposed Configures if we should serialize this attribute by default.
|
29
|
+
# (by default is true for regular attributes and false for relationships)
|
30
|
+
# @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable)
|
31
|
+
# @option opts [Serega, Proc] :serializer Relationship serializer class. Use `proc { MySerializer }` if serializers have cross references.
|
32
|
+
#
|
33
|
+
# @param block [Proc] Proc that receives object and context and finds attribute value
|
34
|
+
#
|
35
|
+
def initialize(name:, opts: {}, block: nil)
|
36
|
+
check(name, opts, block)
|
37
|
+
|
38
|
+
@name = name.to_sym
|
39
|
+
@opts = Utils::EnumDeepDup.call(opts)
|
40
|
+
@block = opts[:value] || block
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Symbol] Object method name to will be used to get attribute value unless block provided
|
44
|
+
def key
|
45
|
+
@key ||= opts.key?(:key) ? opts[:key].to_sym : name
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Boolean, nil] Attribute initial :hide option value
|
49
|
+
def hide
|
50
|
+
opts[:hide]
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Boolean, nil] Attribute initialization :many option
|
54
|
+
def many
|
55
|
+
opts[:many]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Boolean] Checks if attribute is relationship (if :serializer option exists)
|
59
|
+
def relation?
|
60
|
+
!opts[:serializer].nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Serega, nil] Attribute serializer if exists
|
64
|
+
def serializer
|
65
|
+
return @serializer if instance_variable_defined?(:@serializer)
|
66
|
+
|
67
|
+
serializer = opts[:serializer]
|
68
|
+
@serializer =
|
69
|
+
case serializer
|
70
|
+
when String then Object.const_get(serializer, false)
|
71
|
+
when Proc then serializer.call
|
72
|
+
else serializer
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Proc] Proc to find attribute value
|
77
|
+
def value_block
|
78
|
+
return @value_block if instance_variable_defined?(:@value_block)
|
79
|
+
|
80
|
+
@value_block = block || keyword_block
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Finds attribute value
|
85
|
+
#
|
86
|
+
# @param object [Object] Serialized object
|
87
|
+
# @param context [Hash, nil] Serialization context
|
88
|
+
#
|
89
|
+
# @return [Object] Serialized attribute value
|
90
|
+
#
|
91
|
+
def value(object, context)
|
92
|
+
value_block.call(object, context)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Checks if attribute must be added to serialized response
|
97
|
+
#
|
98
|
+
# @param except [Hash] manually hidden attributes
|
99
|
+
# @param only [Hash] manually enforced exposed attributes, other attributes are enforced to be hidden
|
100
|
+
# @param with [Hash] manually enforced exposed attributes
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
#
|
104
|
+
def visible?(except:, only:, with:)
|
105
|
+
return false if except.member?(name) && except[name].empty?
|
106
|
+
return true if only.member?(name)
|
107
|
+
return true if with.member?(name)
|
108
|
+
return false unless only.empty?
|
109
|
+
|
110
|
+
!hide
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def keyword_block
|
116
|
+
key_method_name = key
|
117
|
+
proc { |object| object.public_send(key_method_name) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def check(name, opts, block)
|
121
|
+
CheckName.call(name)
|
122
|
+
CheckOpts.call(opts, self.class.serializer_class.config[:attribute_keys])
|
123
|
+
CheckBlock.call(opts, block)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
extend Serega::Helpers::SerializerClassHelper
|
128
|
+
include AttributeInstanceMethods
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
class Serega
|
6
|
+
#
|
7
|
+
# Core class that stores serializer configuration
|
8
|
+
#
|
9
|
+
class Config
|
10
|
+
module ConfigInstanceMethods
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
# @return [Hash] Current config data
|
14
|
+
attr_reader :opts
|
15
|
+
|
16
|
+
#
|
17
|
+
# Initializes new config instance and deeply duplicates all provided options to
|
18
|
+
# remove possibility of accidental overwriting of parent/nested configs.
|
19
|
+
#
|
20
|
+
# @param opts [Hash] Initial config options
|
21
|
+
#
|
22
|
+
def initialize(opts = {})
|
23
|
+
@opts = Utils::EnumDeepDup.call(opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# @!method [](name)
|
28
|
+
# Get config option, delegates to opts#[]
|
29
|
+
# @param name [Symbol] option name
|
30
|
+
# @return [Object]
|
31
|
+
#
|
32
|
+
# @!method []=(name)
|
33
|
+
# Set config option, delegates to opts#[]=
|
34
|
+
# @param name [Symbol] option name
|
35
|
+
# @return [Object]
|
36
|
+
#
|
37
|
+
# @!method fetch(name)
|
38
|
+
# Fetch config option, delegates to opts#fetch
|
39
|
+
# @param name [Symbol] option name
|
40
|
+
# @return [Object]
|
41
|
+
#
|
42
|
+
def_delegators :opts, :[], :[]=, :fetch, :keys, :has_key?
|
43
|
+
end
|
44
|
+
|
45
|
+
include ConfigInstanceMethods
|
46
|
+
extend Serega::Helpers::SerializerClassHelper
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
class Convert
|
5
|
+
module ConvertClassMethods
|
6
|
+
def call(object, **opts)
|
7
|
+
new(object, **opts).to_h
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ConvertInstanceMethods
|
12
|
+
attr_reader :object, :opts
|
13
|
+
|
14
|
+
def initialize(object, **opts)
|
15
|
+
@object = object
|
16
|
+
@opts = opts
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
many? ? many(object) : one(object) || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def many(objects)
|
26
|
+
objects.map { |obj| one(obj) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def one(object)
|
30
|
+
self.class.serializer_class::ConvertItem.call(object, opts[:context], opts[:map])
|
31
|
+
end
|
32
|
+
|
33
|
+
def many?
|
34
|
+
many = opts[:many]
|
35
|
+
return many unless many.nil?
|
36
|
+
|
37
|
+
object.is_a?(Enumerable)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
extend Serega::Helpers::SerializerClassHelper
|
42
|
+
extend ConvertClassMethods
|
43
|
+
include ConvertInstanceMethods
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
class ConvertItem
|
5
|
+
module ConvertItemClassMethods
|
6
|
+
def call(object, context, map)
|
7
|
+
return unless object
|
8
|
+
|
9
|
+
map.each_with_object({}) do |(attribute, nested_attributes), hash|
|
10
|
+
value = attribute.value(object, context)
|
11
|
+
attach_value(value, hash, attribute, nested_attributes, context)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def attach_value(value, hash, attribute, nested_attributes, context)
|
18
|
+
hash[attribute.name] =
|
19
|
+
if nested_attributes.empty?
|
20
|
+
attribute.relation? ? FROZEN_EMPTY_HASH : value
|
21
|
+
elsif many?(attribute, value)
|
22
|
+
value.map { |val| call(val, context, nested_attributes) }
|
23
|
+
else
|
24
|
+
call(value, context, nested_attributes)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def many?(attribute, object)
|
29
|
+
is_many = attribute.many
|
30
|
+
is_many.nil? ? object.is_a?(Enumerable) : is_many
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
extend Serega::Helpers::SerializerClassHelper
|
35
|
+
extend ConvertItemClassMethods
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Helpers
|
5
|
+
# Stores link to current serializer class
|
6
|
+
module SerializerClassHelper
|
7
|
+
# @return [Class<Serega>] Serializer class that current class is namespaced under.
|
8
|
+
attr_accessor :serializer_class
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/serega/map.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
class Map
|
5
|
+
module ClassMethods
|
6
|
+
def call(opts)
|
7
|
+
@cache ||= {}
|
8
|
+
cache_key = opts.to_s
|
9
|
+
|
10
|
+
@cache[cache_key] ||= begin
|
11
|
+
modifiers = {
|
12
|
+
only: opts&.[](:only) || FROZEN_EMPTY_HASH,
|
13
|
+
except: opts&.[](:except) || FROZEN_EMPTY_HASH,
|
14
|
+
with: opts&.[](:with) || FROZEN_EMPTY_HASH
|
15
|
+
}
|
16
|
+
|
17
|
+
construct_map(serializer_class, **modifiers).tap do
|
18
|
+
@cache.shift if @cache.length >= serializer_class.config[:max_cached_map_per_serializer_count]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def construct_map(serializer_class, only:, except:, with:)
|
26
|
+
serializer_class.attributes.each_with_object([]) do |(name, attribute), map|
|
27
|
+
next unless attribute.visible?(only: only, except: except, with: with)
|
28
|
+
|
29
|
+
nested_map =
|
30
|
+
if attribute.relation?
|
31
|
+
construct_map(
|
32
|
+
attribute.serializer,
|
33
|
+
only: only[name] || FROZEN_EMPTY_HASH,
|
34
|
+
with: with[name] || FROZEN_EMPTY_HASH,
|
35
|
+
except: except[name] || FROZEN_EMPTY_HASH
|
36
|
+
)
|
37
|
+
else
|
38
|
+
FROZEN_EMPTY_ARRAY
|
39
|
+
end
|
40
|
+
|
41
|
+
map << [attribute, nested_map]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
extend ClassMethods
|
47
|
+
extend Serega::Helpers::SerializerClassHelper
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# Plugin that checks used plugins and loads correct Preloader for selected response type
|
7
|
+
# @see Serega::Plugins::JsonApiActiverecordPreloader
|
8
|
+
# @see Serega::Plugins::SimpleApiActiverecordPreloader
|
9
|
+
#
|
10
|
+
module ActiverecordPreloads
|
11
|
+
# @return [Symbol] plugin name
|
12
|
+
def self.plugin_name
|
13
|
+
:activerecord_preloads
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.before_load_plugin(serializer_class, **opts)
|
17
|
+
serializer_class.plugin(:preloads, **opts) unless serializer_class.plugin_used?(:preloads)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Loads plugin code and additional plugins
|
22
|
+
#
|
23
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
24
|
+
# @param opts [Hash] loaded plugins opts
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
#
|
28
|
+
def self.load_plugin(serializer_class, **opts)
|
29
|
+
require_relative "./lib/preloader"
|
30
|
+
|
31
|
+
serializer_class.include(InstanceMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides Serega classes instance methods
|
35
|
+
module InstanceMethods
|
36
|
+
#
|
37
|
+
# Override original #to_h method
|
38
|
+
# @see Serega#to_h
|
39
|
+
#
|
40
|
+
def to_h(object, **opts)
|
41
|
+
object = add_preloads(object)
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def add_preloads(obj)
|
48
|
+
return obj if obj.nil? || (obj.is_a?(Array) && obj.empty?)
|
49
|
+
|
50
|
+
# preloads() method comes from :preloads plugin
|
51
|
+
preloads = preloads()
|
52
|
+
return obj if preloads.empty?
|
53
|
+
|
54
|
+
Preloader.preload(obj, preloads)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module ActiverecordPreloads
|
6
|
+
class Preloader
|
7
|
+
module ClassMethods
|
8
|
+
def preload(object, preloads)
|
9
|
+
preload_handler = handlers.find { |handler| handler.fit?(object) }
|
10
|
+
raise Error, "Can't preload #{preloads.inspect} to #{object.inspect}" unless preload_handler
|
11
|
+
|
12
|
+
preload_handler.preload(object, preloads)
|
13
|
+
end
|
14
|
+
|
15
|
+
def handlers
|
16
|
+
@handlers ||= [ActiverecordRelation, ActiverecordObject, ActiverecordArray].freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
class Loader
|
24
|
+
# :nocov: We can check only one version of activerecord
|
25
|
+
def self.call(records, associations)
|
26
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
27
|
+
ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
|
28
|
+
else
|
29
|
+
ActiveRecord::Associations::Preloader.new.preload(records, associations)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
# :nocov:
|
33
|
+
end
|
34
|
+
|
35
|
+
class ActiverecordObject
|
36
|
+
module ClassMethods
|
37
|
+
def fit?(object)
|
38
|
+
object.is_a?(ActiveRecord::Base)
|
39
|
+
end
|
40
|
+
|
41
|
+
def preload(object, preloads)
|
42
|
+
Loader.call([object], preloads)
|
43
|
+
object
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
extend ClassMethods
|
48
|
+
end
|
49
|
+
|
50
|
+
class ActiverecordRelation
|
51
|
+
module ClassMethods
|
52
|
+
def fit?(objects)
|
53
|
+
objects.is_a?(ActiveRecord::Relation)
|
54
|
+
end
|
55
|
+
|
56
|
+
def preload(objects, preloads)
|
57
|
+
if objects.loaded?
|
58
|
+
array_objects = objects.to_a
|
59
|
+
Loader.call(array_objects, preloads)
|
60
|
+
objects
|
61
|
+
else
|
62
|
+
objects.preload(preloads).load
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
extend ClassMethods
|
68
|
+
end
|
69
|
+
|
70
|
+
class ActiverecordArray
|
71
|
+
module ClassMethods
|
72
|
+
def fit?(objects)
|
73
|
+
objects.is_a?(Array) &&
|
74
|
+
ActiverecordObject.fit?(objects.first) &&
|
75
|
+
same_kind?(objects)
|
76
|
+
end
|
77
|
+
|
78
|
+
def preload(objects, preloads)
|
79
|
+
Loader.call(objects, preloads)
|
80
|
+
objects
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def same_kind?(objects)
|
86
|
+
first_object_class = objects.first.class
|
87
|
+
objects.all? { |object| object.instance_of?(first_object_class) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
extend ClassMethods
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module ContextMetadata
|
6
|
+
DEFAULT_CONTEXT_METADATA_KEY = :meta
|
7
|
+
|
8
|
+
def self.plugin_name
|
9
|
+
:context_metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.before_load_plugin(serializer_class, **opts)
|
13
|
+
serializer_class.plugin(:root, **opts) unless serializer_class.plugin_used?(:root)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load_plugin(serializer_class, **_opts)
|
17
|
+
serializer_class.include(InstanceMethods)
|
18
|
+
serializer_class::Convert.include(ConvertInstanceMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.after_load_plugin(serializer_class, **opts)
|
22
|
+
config = serializer_class.config
|
23
|
+
meta_key = opts[:context_metadata_key] || DEFAULT_CONTEXT_METADATA_KEY
|
24
|
+
config[plugin_name] = {key: meta_key}
|
25
|
+
config[:serialize_keys] << meta_key
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def to_h(object, **opts)
|
30
|
+
meta_key = self.class.config[:context_metadata][:key]
|
31
|
+
meta = opts[meta_key]
|
32
|
+
|
33
|
+
if meta && !meta.is_a?(Hash)
|
34
|
+
raise Serega::Error, "Option :#{meta_key} must be a Hash, but #{meta.class} was given"
|
35
|
+
end
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ConvertInstanceMethods
|
42
|
+
def to_h
|
43
|
+
super.tap do |hash|
|
44
|
+
add_context_metadata(hash)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def add_context_metadata(hash)
|
51
|
+
context_metadata_key = self.class.serializer_class.config[:context_metadata][:key]
|
52
|
+
return unless context_metadata_key
|
53
|
+
|
54
|
+
metadata = opts[context_metadata_key]
|
55
|
+
return unless metadata
|
56
|
+
|
57
|
+
deep_merge_context_metadata(hash, metadata)
|
58
|
+
end
|
59
|
+
|
60
|
+
def deep_merge_context_metadata(hash, metadata)
|
61
|
+
hash.merge!(metadata) do |_key, this_val, other_val|
|
62
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
63
|
+
deep_merge_context_metadata(this_val, other_val)
|
64
|
+
else
|
65
|
+
other_val
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
register_plugin(ContextMetadata.plugin_name, ContextMetadata)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
module Formatters
|
6
|
+
def self.plugin_name
|
7
|
+
:formatters
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_plugin(serializer_class, **_opts)
|
11
|
+
serializer_class::Attribute.include(AttributeInstanceMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.after_load_plugin(serializer_class, **_opts)
|
15
|
+
config = serializer_class.config
|
16
|
+
config[plugin_name] = {}
|
17
|
+
config[:attribute_keys] << :format
|
18
|
+
end
|
19
|
+
|
20
|
+
module AttributeInstanceMethods
|
21
|
+
def value_block
|
22
|
+
return @value_block if instance_variable_defined?(:@value_block)
|
23
|
+
|
24
|
+
original_block = super
|
25
|
+
formatter = opts[:format]
|
26
|
+
return original_block unless formatter
|
27
|
+
|
28
|
+
@value_block = formatted_block(formatter, original_block)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def formatted_block(formatter, original_block)
|
34
|
+
proc do |object, context|
|
35
|
+
value = original_block.call(object, context)
|
36
|
+
|
37
|
+
if formatter.is_a?(Symbol)
|
38
|
+
self.class.serializer_class.config.fetch(:formatters).fetch(formatter).call(value)
|
39
|
+
else
|
40
|
+
formatter.call(value)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
register_plugin(Formatters.plugin_name, Formatters)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# Plugin adds `:hide_nil` option to attributes to delete them from final result
|
7
|
+
# if value is nil
|
8
|
+
#
|
9
|
+
module HideNil
|
10
|
+
# @return [Symbol] plugin name
|
11
|
+
def self.plugin_name
|
12
|
+
:hide_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Includes plugin modules to current serializer
|
17
|
+
#
|
18
|
+
# @param serializer_class [Class] current serializer class
|
19
|
+
# @param _opts [Hash] plugin opts
|
20
|
+
#
|
21
|
+
# @return [void]
|
22
|
+
#
|
23
|
+
def self.load_plugin(serializer_class, **_opts)
|
24
|
+
serializer_class::Attribute.include(AttributeMethods)
|
25
|
+
serializer_class::Attribute::CheckOpts.extend(CheckOptsClassMethods)
|
26
|
+
serializer_class::ConvertItem.extend(ConvertItemClassMethods)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.after_load_plugin(serializer_class, **opts)
|
30
|
+
serializer_class.config[:attribute_keys] << :hide_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds #hide_nil? Attribute instance method
|
34
|
+
module AttributeMethods
|
35
|
+
def hide_nil?
|
36
|
+
!!opts[:hide_nil]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module CheckOptsClassMethods
|
41
|
+
private
|
42
|
+
|
43
|
+
def check_each_opt(opts)
|
44
|
+
super
|
45
|
+
CheckOptHideNil.call(opts)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class CheckOptHideNil
|
50
|
+
#
|
51
|
+
# Checks attribute :hide_nil option
|
52
|
+
#
|
53
|
+
# @param opts [Hash] Attribute options
|
54
|
+
#
|
55
|
+
# @raise [Serega::Error] Error that option has invalid value
|
56
|
+
#
|
57
|
+
# @return [void]
|
58
|
+
#
|
59
|
+
def self.call(opts)
|
60
|
+
return unless opts.key?(:hide_nil)
|
61
|
+
|
62
|
+
value = opts[:hide_nil]
|
63
|
+
return if (value == true) || (value == false)
|
64
|
+
|
65
|
+
raise Error, "Invalid option :hide_nil => #{value.inspect}. Must have a boolean value"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module ConvertItemClassMethods
|
70
|
+
private
|
71
|
+
|
72
|
+
def attach_value(value, *args)
|
73
|
+
super unless value.nil?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
register_plugin(HideNil.plugin_name, HideNil)
|
79
|
+
end
|
80
|
+
end
|