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