serega 0.41.0 → 0.42.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d54f033e70155c517c02d15cd9a4eb5399e385b0ffdd68d8c6c2b390201a007
4
- data.tar.gz: 23257bef39a343bea74c063f452fa1feed58738c454e467838cceb2fecdbe862
3
+ metadata.gz: 4ec97ed9e6fc97612c5094991de19b1c5d43a44168e5a29593bdf75f77e5f200
4
+ data.tar.gz: 54d42defad67c2ea46bac516870caacb36ce38643826137900b46a8661001cc3
5
5
  SHA512:
6
- metadata.gz: 2a0df718af2b8f019af2fe9dc8bb82dc21a03631a211ecc00a3fd4e83d6ed3324505727c9124975181869fd11fb452e7670a060ae7e04eddd161186e3353366f
7
- data.tar.gz: e5bc125a847e929bdaeb7ce044adbb4e866324d702032ee19abd76208949f406b166f4ecf111453d0f7130aa3dfb7d36e3269010df8adc7e05c50eba3c3a52cd
6
+ metadata.gz: 5c9f5c91fc97ae0b08578b6fb7c98c3718194094af81fb6175cd3854f602b8daa450204cd75b054cd9ee62586aed08f120b32d209a4817fb5bfdb74c6b83bd55
7
+ data.tar.gz: bd05febbd13a095a4e00f0336514c540c0781eac2707e29e8d9c64e6b4bc0152a7b717e05b93bfdca969e01b1fed4eeeeb52a330b3e9c7a820986b0b30e5b9e0
data/README.md CHANGED
@@ -20,7 +20,7 @@ It has some great features:
20
20
  - Solutions for N+1 problem (via built-in [batch loading](#batch-loading), [preloads][preloads] or
21
21
  [activerecord_preloads][activerecord_preloads] plugin)
22
22
  - Load serialized objects by ids ([prepare_initial_objects](#prepare-initial-objects))
23
- - Built-in object presenter ([presenter][presenter] plugin)
23
+ - Built-in object presenter ([presenter][presenter])
24
24
  - Adding custom metadata (via [metadata][metadata] or
25
25
  [context_metadata][context_metadata] plugins)
26
26
  - Value formatters ([formatters][formatters] plugin) helps to transform
@@ -49,13 +49,13 @@ It has some great features:
49
49
  - [Serializing the same object in association](#serializing-the-same-object-in-association)
50
50
  - [Custom preloading](#custom-preloading)
51
51
  - [Serializing Hash records](#serializing-hash-records)
52
+ - [Presenter](#presenter)
52
53
  - [Plugins](#plugins)
53
54
  - [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
54
55
  - [Plugin :root](#plugin-root)
55
56
  - [Plugin :metadata](#plugin-metadata)
56
57
  - [Plugin :context_metadata](#plugin-context_metadata)
57
58
  - [Plugin :formatters](#plugin-formatters)
58
- - [Plugin :presenter](#plugin-presenter)
59
59
  - [Plugin :string_modifiers](#plugin-string_modifiers)
60
60
  - [Plugin :if](#plugin-if)
61
61
  - [Plugin :camel_case](#plugin-camel_case)
@@ -128,8 +128,8 @@ class UserSerializer < Serega
128
128
  config.base_serializer = Serega
129
129
 
130
130
  # Method :itself is handy to serialize the same object with a nested set of
131
- # attributes. Note: with the :presenter plugin the serialized value will be
132
- # the Presenter instance itself; use `method: :__getobj__` to serialize the
131
+ # attributes. Note: with a custom presenter the serialized value will be
132
+ # the presenter instance itself; use `method: :__getobj__` to serialize the
133
133
  # original unwrapped object instead.
134
134
  attribute :statistics, method: :itself do
135
135
  attribute :likes_count
@@ -849,6 +849,62 @@ attribute :city,
849
849
  }
850
850
  ```
851
851
 
852
+ ## Presenter
853
+
854
+ Moves computed attribute logic out of `:value` blocks and into a
855
+ `presenter do ... end` block, keeping serializers readable as schemas rather
856
+ than bags of lambdas.
857
+
858
+ Computed attributes can be written as inline blocks:
859
+
860
+ ```ruby
861
+ class UserSerializer < Serega
862
+ attribute :name, value: proc { |user| [user.first_name, user.last_name].compact.join(' ') }
863
+ attribute :role, value: proc { |user, ctx| user.id == ctx[:current_user_id] ? :self : :other }
864
+ end
865
+ ```
866
+
867
+ They can also be written as methods in a `presenter do ... end` block:
868
+
869
+ ```ruby
870
+ class UserSerializer < Serega
871
+ attribute :name
872
+ attribute :role
873
+
874
+ presenter do
875
+ def name
876
+ [first_name, last_name].compact.join(' ')
877
+ end
878
+
879
+ def role
880
+ id == __ctx__[:current_user_id] ? :self : :other
881
+ end
882
+ end
883
+ end
884
+
885
+ user = OpenStruct.new(id: 1, first_name: 'Bruce', last_name: 'Wayne')
886
+ UserSerializer.to_h(user, context: {current_user_id: 1})
887
+ # => {name: "Bruce Wayne", role: :self}
888
+ ```
889
+
890
+ Multiple `presenter` blocks accumulate. A child serializer runs its parent's
891
+ blocks followed by its own, so it gets the parent's presenter methods while its
892
+ own stay out of the parent. The parent's blocks are copied when the child class
893
+ is created, so a `presenter` block added to a parent afterwards does not reach it.
894
+
895
+ Presenter methods run on a `SimpleDelegator` wrapping the serialized object, so
896
+ every method of that object is available directly. Any method not defined in a
897
+ `presenter` block is delegated to the object on the first call, and a real
898
+ delegator method is defined for it — so all subsequent serializations call it
899
+ directly.
900
+
901
+ The wrapped object is accessible via `__getobj__` when you need an unambiguous
902
+ reference to it. The serialization context is accessible via `__ctx__`.
903
+
904
+ Objects are wrapped only when the serializer has presenter methods. A serializer
905
+ without them costs nothing — its attribute values, batch loaders and value
906
+ callables receive the raw objects.
907
+
852
908
  ## Plugins
853
909
 
854
910
  ### Plugin :activerecord_preloads
@@ -1049,61 +1105,6 @@ class UserSerializer < Serega
1049
1105
  end
1050
1106
  ```
1051
1107
 
1052
- ### Plugin :presenter
1053
-
1054
- Moves computed attribute logic out of blocks and into a dedicated `Presenter` class,
1055
- keeping serializers readable as schemas rather than bags of lambdas.
1056
-
1057
- Without the plugin, computed attributes live as inline blocks:
1058
-
1059
- ```ruby
1060
- class UserSerializer < Serega
1061
- attribute :name, value: proc { |u| [u.first_name, u.last_name].compact.join(' ') }
1062
- attribute :role, value: proc { |u, ctx| u == ctx[:current_user] ? :self : :other }
1063
- end
1064
- ```
1065
-
1066
- With the plugin, they move into a `presenter do ... end` block:
1067
-
1068
- ```ruby
1069
- class UserSerializer < Serega
1070
- plugin :presenter
1071
-
1072
- attribute :name
1073
- attribute :role
1074
-
1075
- presenter do
1076
- def name
1077
- [first_name, last_name].compact.join(' ')
1078
- end
1079
-
1080
- def role
1081
- id == __ctx__[:current_user_id] ? :self : :other
1082
- end
1083
- end
1084
- end
1085
- ```
1086
-
1087
- The block is evaluated inside the serializer's own `Presenter` class, so
1088
- multiple `presenter` blocks accumulate.
1089
-
1090
- `Presenter` inherits from `SimpleDelegator`, so every method of the serialized
1091
- object is available directly inside presenter methods. Any method not explicitly
1092
- defined on `Presenter` is resolved via `method_missing` on the first call, which
1093
- also defines a real delegator method — so all subsequent serializations call it
1094
- directly, without going through `method_missing` again.
1095
-
1096
- The original wrapped object is accessible via `__getobj__` (standard
1097
- `SimpleDelegator` API) when you need an unambiguous reference to it.
1098
-
1099
- The serialization context is accessible via the private method `__ctx__`.
1100
-
1101
- Objects are wrapped in the `Presenter` only when the serializer's `Presenter`
1102
- class (or an inherited one) actually defines custom methods. Loading the
1103
- plugin in a base serializer adds no overhead to serializers that don't
1104
- customize their presenters — their attribute values, batch loaders and value
1105
- callables keep receiving the raw objects.
1106
-
1107
1108
  ### Plugin :string_modifiers
1108
1109
 
1109
1110
  Allows `:only`, `:except` and `:with` to be given as a single comma-separated
@@ -1298,7 +1299,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
1298
1299
  [hash_access]: #serializing-hash-records
1299
1300
  [metadata]: #plugin-metadata
1300
1301
  [preloads]: #preloads
1301
- [presenter]: #plugin-presenter
1302
+ [presenter]: #presenter
1302
1303
  [root]: #plugin-root
1303
1304
  [string_modifiers]: #plugin-string_modifiers
1304
1305
  [if]: #plugin-if
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.41.0
1
+ 0.42.0
data/lib/serega/config.rb CHANGED
@@ -33,7 +33,7 @@ class Serega
33
33
  delegate_default_allow_nil: false,
34
34
  max_cached_plans_per_serializer_count: 0,
35
35
  auto_preload: {has_delegate_option: false, has_serializer_option: false},
36
- auto_preload_excluded_methods: %i[itself].freeze,
36
+ auto_preload_excluded_methods: %i[itself __getobj__].freeze,
37
37
  hide_by_default: false,
38
38
  batch_id_option: :id,
39
39
  base_serializer: nil,
@@ -16,7 +16,7 @@ class Serega
16
16
  #
17
17
  # @private
18
18
  module InstanceMethods
19
- attr_reader :context, :plan, :many, :opts, :level_queue
19
+ attr_reader :context, :plan, :many, :opts, :level_queue, :presenter
20
20
 
21
21
  # @param plan [SeregaPlan] Serialization plan
22
22
  # @param context [Hash] Serialization context
@@ -30,6 +30,8 @@ class Serega
30
30
  @many = many
31
31
  @opts = opts
32
32
  @level_queue = opts[:level_queue]
33
+ # Looked up once here and reused for every enqueued chunk of the level.
34
+ @presenter = self.class.serializer_class.presenter
33
35
  end
34
36
 
35
37
  # Enqueues this level and returns its result container(s). The containers are
@@ -78,9 +80,14 @@ class Serega
78
80
  # containers. This is where objects enter their level, so every object a
79
81
  # point resolves against and a batch loader receives has the same shape.
80
82
  #
81
- # Patched in:
82
- # - plugin :presenter (wraps each object in a Presenter before enqueueing)
83
+ # Each object is wrapped in the serializer's presenter before it is enqueued,
84
+ # so the whole level value resolution and batch loaders alike — sees
85
+ # presenters. Serializers without a presenter enqueue the objects as they
86
+ # are — wrapping would only add overhead and break class checks
87
+ # (object.is_a?, Hash === object) without changing anything.
83
88
  def enqueue(objects)
89
+ objects = objects.map { |object| presenter.new(object, context) } if presenter
90
+
84
91
  level_queue.enqueue(self, objects)
85
92
  end
86
93
 
@@ -85,12 +85,18 @@ class Serega
85
85
  # Runs this point's declared preloads over the given objects using the
86
86
  # serializer's registered preload handler.
87
87
  #
88
+ # Presenters are unwrapped first, as preload handlers work with the
89
+ # serialized objects themselves.
90
+ #
88
91
  # @param objects [Array] objects serialized at this point's level
89
92
  # @return [void]
90
93
  def run_preloads(objects)
91
94
  return unless preloads
92
95
 
93
- handler = self.class.serializer_class.preload_with
96
+ serializer_class = self.class.serializer_class
97
+ objects = objects.map(&:__getobj__) if serializer_class.presenter
98
+
99
+ handler = serializer_class.preload_with
94
100
  unless handler
95
101
  raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
96
102
  end
@@ -88,28 +88,9 @@ class Serega
88
88
  # @private
89
89
  def self.after_load_plugin(serializer_class, **_opts)
90
90
  serializer_class.preload_with do |objects, preloads|
91
- Preloader.preload(ActiverecordPreloads.records(serializer_class, objects), preloads)
91
+ Preloader.preload(objects, preloads)
92
92
  end
93
93
  end
94
-
95
- #
96
- # The underlying records to preload onto. The :presenter plugin wraps every
97
- # serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
98
- # the real records, so unwrap them via #__getobj__ when presenter is used.
99
- # Objects are wrapped only when the Presenter class has custom methods.
100
- #
101
- # @param serializer_class [Class<Serega>] Current serializer class
102
- # @param objects [Array] objects serialized at the current level
103
- #
104
- # @return [Array] the underlying records
105
- #
106
- # @private
107
- def self.records(serializer_class, objects)
108
- return objects unless serializer_class.plugin_used?(:presenter)
109
- return objects unless serializer_class.custom_presenter?
110
-
111
- objects.map(&:__getobj__)
112
- end
113
94
  end
114
95
 
115
96
  register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
@@ -30,10 +30,10 @@ class Serega
30
30
  # @raise [SeregaError] Raises SeregaError when plugin was not found
31
31
  #
32
32
  # @example Find plugin when providing name
33
- # SeregaPlugins.find_plugin(:presenter) # => SeregaPlugins::Presenter
33
+ # SeregaPlugins.find_plugin(:root) # => SeregaPlugins::Root
34
34
  #
35
35
  # @example Find plugin when providing plugin itself
36
- # SeregaPlugins.find_plugin(Presenter) # => Presenter
36
+ # SeregaPlugins.find_plugin(Root) # => Root
37
37
  #
38
38
  # @return [Class<Module>] Plugin core module
39
39
  #
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "forwardable"
5
+
6
+ class Serega
7
+ #
8
+ # Wraps the serialized object, so computed attribute values can be defined as
9
+ # methods instead of `:value` callables.
10
+ #
11
+ # SeregaPresenter inherits from SimpleDelegator:
12
+ # - All methods of the serialized object are available directly inside presenter methods.
13
+ # - Methods not defined on SeregaPresenter are resolved via method_missing on the first call
14
+ # and then defined as real delegators, so subsequent calls skip method_missing entirely.
15
+ # - The original object is accessible via __getobj__ (standard SimpleDelegator API).
16
+ # - The serialization context is accessible via the private method __ctx__.
17
+ #
18
+ # The `presenter do ... end` block is evaluated inside the serializer's own
19
+ # SeregaPresenter class, so multiple blocks accumulate.
20
+ #
21
+ # @example
22
+ # class UserSerializer < Serega
23
+ # attribute :name
24
+ # attribute :role
25
+ #
26
+ # presenter do
27
+ # def name
28
+ # [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
29
+ # end
30
+ #
31
+ # def role
32
+ # id == __ctx__[:current_user_id] ? :self : :other
33
+ # end
34
+ # end
35
+ # end
36
+ #
37
+ # @private
38
+ class SeregaPresenter < SimpleDelegator
39
+ #
40
+ # @param object [Object] Serialized object to wrap
41
+ # @param ctx [Hash, nil] Serialization context
42
+ #
43
+ def initialize(object, ctx = nil)
44
+ super(object)
45
+ @__ctx__ = ctx
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :__ctx__
51
+
52
+ #
53
+ # Delegates all missing methods to serialized object.
54
+ #
55
+ # Creates delegator method after first #method_missing hit to improve
56
+ # performance of following serializations.
57
+ #
58
+ def method_missing(name, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
59
+ super.tap do
60
+ self.class.def_delegator :__getobj__, name
61
+ end
62
+ end
63
+
64
+ extend Forwardable
65
+ end
66
+ end
data/lib/serega.rb CHANGED
@@ -62,6 +62,7 @@ require_relative "serega/validations/check_batch_loader_params"
62
62
  require_relative "serega/validations/check_serialize_params"
63
63
 
64
64
  require_relative "serega/config"
65
+ require_relative "serega/presenter"
65
66
  require_relative "serega/object_serializer"
66
67
  require_relative "serega/plan_point"
67
68
  require_relative "serega/plan"
@@ -70,6 +71,7 @@ require_relative "serega/plugins"
70
71
 
71
72
  class Serega
72
73
  @config = SeregaConfig.new
74
+ @presenter_class = nil
73
75
 
74
76
  # Validates `Serializer.attribute` params
75
77
  check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
@@ -170,10 +172,17 @@ class Serega
170
172
  end
171
173
 
172
174
  #
173
- # Adds attribute
175
+ # Lists blocks given to `presenter`, in definition order
174
176
  #
175
- # Patched in:
176
- # - plugin :presenter (additionally adds method in Presenter class)
177
+ # @return [Array<Proc>] presenter blocks list
178
+ #
179
+ # @private
180
+ def presenter_blocks
181
+ @presenter_blocks ||= []
182
+ end
183
+
184
+ #
185
+ # Adds attribute
177
186
  #
178
187
  # @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
179
188
  # @param opts [Hash] Options to serialize attribute
@@ -233,6 +242,30 @@ class Serega
233
242
  batch_loaders[batch_loader.name] = batch_loader
234
243
  end
235
244
 
245
+ #
246
+ # Defines presenter methods — evaluates the block inside the serializer's own
247
+ # presenter class. Multiple blocks accumulate.
248
+ #
249
+ # presenter do
250
+ # def name
251
+ # [first_name, last_name].compact.join(" ")
252
+ # end
253
+ # end
254
+ #
255
+ # @param block [Proc] Presenter methods
256
+ #
257
+ # @return [Class<SeregaPresenter>, nil] the serializer presenter class,
258
+ # nil when the serializer has none
259
+ #
260
+ def presenter(&block)
261
+ return @presenter_class unless block
262
+
263
+ @presenter_class ||= Class.new(SeregaPresenter)
264
+ @presenter_class.class_exec(&block)
265
+ presenter_blocks << block
266
+ @presenter_class
267
+ end
268
+
236
269
  #
237
270
  # Registers (or returns) the handler used to preload an attribute's
238
271
  # associations onto the records gathered during serialization.
@@ -378,7 +411,6 @@ class Serega
378
411
 
379
412
  # Patched in:
380
413
  # - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
381
- # - plugin :presenter (defines Presenter)
382
414
  def inherited(subclass)
383
415
  config_class = Class.new(self::SeregaConfig)
384
416
  config_class.serializer_class = subclass
@@ -446,6 +478,11 @@ class Serega
446
478
  # Assign same initial objects handler
447
479
  subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
448
480
 
481
+ # Assign same presenter blocks
482
+ presenter_blocks.each do |presenter_block|
483
+ subclass.presenter(&presenter_block)
484
+ end
485
+
449
486
  super
450
487
  end
451
488
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.41.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey
@@ -64,10 +64,10 @@ files:
64
64
  - lib/serega/plugins/metadata/validations/check_opt_value.rb
65
65
  - lib/serega/plugins/metadata/validations/check_opts.rb
66
66
  - lib/serega/plugins/metadata/validations/check_path.rb
67
- - lib/serega/plugins/presenter/presenter.rb
68
67
  - lib/serega/plugins/root/root.rb
69
68
  - lib/serega/plugins/string_modifiers/parse_string_modifiers.rb
70
69
  - lib/serega/plugins/string_modifiers/string_modifiers.rb
70
+ - lib/serega/presenter.rb
71
71
  - lib/serega/utils/collection_detector.rb
72
72
  - lib/serega/utils/enum_deep_dup.rb
73
73
  - lib/serega/utils/enum_deep_freeze.rb
@@ -1,242 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "delegate"
4
- require "forwardable"
5
-
6
- class Serega
7
- module SeregaPlugins
8
- #
9
- # Plugin :presenter — moves computed attribute logic into a dedicated Presenter class.
10
- #
11
- # Presenter inherits from SimpleDelegator:
12
- # - All methods of the serialized object are available directly inside presenter methods.
13
- # - Methods not defined on Presenter are resolved via method_missing on the first call
14
- # and then defined as real delegators, so subsequent calls skip method_missing entirely.
15
- # - The original object is accessible via __getobj__ (standard SimpleDelegator API).
16
- # - The serialization context is accessible via the private method __ctx__.
17
- #
18
- # The `presenter do ... end` block is evaluated inside the serializer's own
19
- # Presenter class, so multiple blocks accumulate.
20
- #
21
- # @example
22
- # class UserSerializer < Serega
23
- # plugin :presenter
24
- #
25
- # attribute :name
26
- # attribute :role
27
- #
28
- # presenter do
29
- # def name
30
- # [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
31
- # end
32
- #
33
- # def role
34
- # id == __ctx__[:current_user_id] ? :self : :other
35
- # end
36
- # end
37
- # end
38
- #
39
- module Presenter
40
- # @return [Symbol] Plugin name
41
- # @private
42
- def self.plugin_name
43
- :presenter
44
- end
45
-
46
- #
47
- # Applies plugin code to specific serializer
48
- #
49
- # @param serializer_class [Class<Serega>] Current serializer class
50
- # @param _opts [Hash] Plugin options
51
- #
52
- # @return [void]
53
- #
54
- # @private
55
- def self.load_plugin(serializer_class, **_opts)
56
- serializer_class.extend(ClassMethods)
57
- serializer_class::SeregaObjectSerializer.include(SeregaObjectSerializerInstanceMethods)
58
- end
59
-
60
- #
61
- # Runs callbacks after plugin was attached
62
- #
63
- # @param serializer_class [Class<Serega>] Current serializer class
64
- # @param _opts [Hash] Plugin options
65
- #
66
- # @return [void]
67
- #
68
- # @private
69
- def self.after_load_plugin(serializer_class, **_opts)
70
- presenter_class = Class.new(Presenter)
71
- presenter_class.serializer_class = serializer_class
72
- serializer_class.const_set(:Presenter, presenter_class)
73
-
74
- # The presenter's unwrap method returns the serialized object itself,
75
- # not an association — it must never be auto-preloaded.
76
- config = serializer_class.config
77
- config.auto_preload_excluded_methods = config.auto_preload_excluded_methods | [:__getobj__]
78
- end
79
-
80
- # Presenter class
81
- # @private
82
- class Presenter < SimpleDelegator
83
- # Presenter instance methods
84
- # @private
85
- module InstanceMethods
86
- #
87
- # @param object [Object] Serialized object to wrap
88
- # @param ctx [Hash, nil] Serialization context
89
- #
90
- def initialize(object, ctx = nil)
91
- super(object)
92
- @__ctx__ = ctx
93
- end
94
-
95
- private
96
-
97
- attr_reader :__ctx__
98
-
99
- #
100
- # Delegates all missing methods to serialized object.
101
- #
102
- # Creates delegator method after first #method_missing hit to improve
103
- # performance of following serializations.
104
- #
105
- def method_missing(name, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
106
- super.tap do
107
- self.class.def_delegator :__getobj__, name
108
- end
109
- end
110
- end
111
-
112
- extend SeregaHelpers::SerializerClassHelper
113
- extend Forwardable
114
- include InstanceMethods
115
-
116
- # Tracks whether user code was added to the Presenter class.
117
- #
118
- # These singleton methods are defined after the base class body above,
119
- # so the plugin's own includes do not mark the base class as modified.
120
- # Lazy delegators defined by #method_missing do mark the class, but
121
- # they can appear only on presenters that are already wrapping.
122
- class << self
123
- #
124
- # Checks if this Presenter class (or an inherited one) was extended
125
- # with custom user code and therefore objects must be wrapped
126
- #
127
- # @return [Boolean] whether custom presenter methods were defined
128
- #
129
- def modified?
130
- return true if defined?(@modified)
131
- return false if equal?(Presenter) # the plugin's base class — the walk stops here
132
-
133
- superclass.modified?
134
- end
135
-
136
- # Marks the class as modified, then includes the module
137
- # @return [void]
138
- def include(*modules)
139
- @modified = true
140
- super
141
- end
142
-
143
- # Marks the class as modified, then prepends the module
144
- # @return [void]
145
- def prepend(*modules)
146
- @modified = true
147
- super
148
- end
149
-
150
- private
151
-
152
- def method_added(name)
153
- @modified = true
154
- super
155
- end
156
- end
157
- end
158
-
159
- #
160
- # Serega additional/patched class methods
161
- #
162
- # @see Serega
163
- #
164
- module ClassMethods
165
- #
166
- # Defines presenter methods — evaluates the block inside the
167
- # serializer's own Presenter class. Multiple blocks accumulate.
168
- #
169
- # presenter do
170
- # def name
171
- # [first_name, last_name].compact.join(" ")
172
- # end
173
- # end
174
- #
175
- # @return [void]
176
- #
177
- def presenter(&block)
178
- raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
179
-
180
- self::Presenter.class_exec(&block)
181
- nil
182
- end
183
-
184
- #
185
- # Checks if the serializer's Presenter class (or an inherited one) was
186
- # extended with custom user code. When it was not, serialized objects
187
- # are not wrapped in the Presenter at all.
188
- #
189
- # @return [Boolean] whether custom presenter methods were defined
190
- #
191
- # @private
192
- def custom_presenter?
193
- self::Presenter.modified?
194
- end
195
-
196
- private
197
-
198
- def inherited(subclass)
199
- super
200
-
201
- presenter_class = Class.new(self::Presenter)
202
- presenter_class.serializer_class = subclass
203
- subclass.const_set(:Presenter, presenter_class)
204
- end
205
- end
206
-
207
- #
208
- # SeregaObjectSerializer additional/patched class methods
209
- #
210
- # @see Serega::SeregaObjectSerializer
211
- #
212
- # @private
213
- module SeregaObjectSerializerInstanceMethods
214
- # The custom-presenter check is made once per object serializer here
215
- # and its result is reused for every enqueued chunk of the level.
216
- def initialize(**opts)
217
- super
218
- @wrap_in_presenter = self.class.serializer_class.custom_presenter?
219
- end
220
-
221
- private
222
-
223
- #
224
- # Wraps each serialized object in Presenter.new(object, ctx) before it is
225
- # enqueued, so the whole level — value resolution and batch loaders alike —
226
- # sees presenters. Objects are not wrapped when the Presenter class has
227
- # no custom methods — such wrapping would only add overhead and break
228
- # class checks (object.is_a?, Hash === object) without changing anything.
229
- #
230
- def enqueue(objects)
231
- return super unless @wrap_in_presenter
232
-
233
- presenter = self.class.serializer_class::Presenter
234
- presenters = objects.map { |object| presenter.new(object, context) }
235
- super(presenters)
236
- end
237
- end
238
- end
239
-
240
- register_plugin(Presenter.plugin_name, Presenter)
241
- end
242
- end