serega 0.42.0 → 0.50.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
data/README.md CHANGED
@@ -1,1307 +1,224 @@
1
- # Serega Ruby Serializer
1
+ # Serega
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/serega.svg)](https://badge.fury.io/rb/serega)
4
- [![GitHub Actions][build-badge]][build]
5
-
6
- The Serega Ruby Serializer provides easy and powerful DSL to describe your
7
- objects and serialize them to Hash, ready for further conversion to JSON or
8
- other formats.
9
-
10
- ---
11
-
12
- 📌 Serega does not depend on any gem and works with any framework
13
-
14
- ---
15
-
16
- It has some great features:
3
+ <!-- TODO: logo (SVG, light and dark variants)
4
+ <p align="center">
5
+ <picture>
6
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
7
+ <img src="assets/logo.svg" alt="Serega" width="240">
8
+ </picture>
9
+ </p>
10
+ -->
17
11
 
18
- - Manually [select serialized fields](#selecting-fields)
19
- - Secure from malicious queries with [depth_limit][depth_limit] plugin
20
- - Solutions for N+1 problem (via built-in [batch loading](#batch-loading), [preloads][preloads] or
21
- [activerecord_preloads][activerecord_preloads] plugin)
22
- - Load serialized objects by ids ([prepare_initial_objects](#prepare-initial-objects))
23
- - Built-in object presenter ([presenter][presenter])
24
- - Adding custom metadata (via [metadata][metadata] or
25
- [context_metadata][context_metadata] plugins)
26
- - Value formatters ([formatters][formatters] plugin) helps to transform
27
- time, date, money, percentage, and any other values in the same way
28
- keeping the code dry
29
- - Conditional attributes - ([if][if] plugin)
30
- - Auto camelCase keys - [camel_case][camel_case] plugin
31
- - Serializing Hash records - [hash_access][hash_access] attribute option
32
-
33
- ## Table of Contents
34
-
35
- <!-- toc -->
36
-
37
- - [Installation](#installation)
38
- - [Usage](#usage)
39
- - [Define serializers](#define-serializers)
40
- - [Adding attributes](#adding-attributes)
41
- - [Defining a nested serializer with a block](#defining-a-nested-serializer-with-a-block)
42
- - [Serializing](#serializing)
43
- - [Selecting Fields](#selecting-fields)
44
- - [Using Context](#using-context)
45
- - [Batch Loading](#batch-loading)
46
- - [Prepare Initial Objects](#prepare-initial-objects)
47
- - [Configuration](#configuration)
48
- - [Preloads](#preloads)
49
- - [Serializing the same object in association](#serializing-the-same-object-in-association)
50
- - [Custom preloading](#custom-preloading)
51
- - [Serializing Hash records](#serializing-hash-records)
52
- - [Presenter](#presenter)
53
- - [Plugins](#plugins)
54
- - [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
55
- - [Plugin :root](#plugin-root)
56
- - [Plugin :metadata](#plugin-metadata)
57
- - [Plugin :context_metadata](#plugin-context_metadata)
58
- - [Plugin :formatters](#plugin-formatters)
59
- - [Plugin :string_modifiers](#plugin-string_modifiers)
60
- - [Plugin :if](#plugin-if)
61
- - [Plugin :camel_case](#plugin-camel_case)
62
- - [Plugin :depth_limit](#plugin-depth_limit)
63
- - [Plugin :explicit_many_option](#plugin-explicit_many_option)
64
- - [Errors](#errors)
65
- - [Release](#release)
66
- - [Development](#development)
67
- - [Contributing](#contributing)
68
- - [License](#license)
69
-
70
- <!-- tocstop -->
71
-
72
- ## Installation
12
+ **Serialize exactly what was asked. Load exactly what is needed.**
73
13
 
74
- `bundle add serega`
75
-
76
- ## Usage
14
+ [![Gem Version](https://badge.fury.io/rb/serega.svg)](https://badge.fury.io/rb/serega)
15
+ [![GitHub Actions][build-badge]][build]
16
+ ![Coverage 100%](https://img.shields.io/badge/coverage-100%25-brightgreen)
17
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
18
+ ![Ruby >= 3.3](https://img.shields.io/badge/ruby-%3E%3D%203.3-red)
77
19
 
78
- ### Define serializers
20
+ Serega is a Ruby serializer that turns your objects into Hashes ready for JSON.
21
+ Clients choose the fields they need, and Serega loads only the data those
22
+ fields use — in a few queries, with no N+1.
79
23
 
80
- Most apps should define **base serializer** with common plugins and settings to
81
- not repeat them in each serializer.
24
+ - ⚡ **Fast** — serialization code is generated per field selection and cached.
25
+ [See benchmarks](#benchmarks).
26
+ - 🎯 **Clients choose fields** — `?fields=name,posts(title)` works out of the
27
+ box, nested at any depth.
28
+ - 🧺 **No N+1** — associations are preloaded and batch loaders run once per
29
+ relation, only for the requested fields.
30
+ - 🪶 **No dependencies** — plain Ruby, works with Rails, Hanami, Roda, Sinatra
31
+ or no framework at all.
82
32
 
83
- Serializers will inherit everything (plugins, config, attributes) from their
84
- superclasses.
33
+ ## Show me
85
34
 
86
35
  ```ruby
87
36
  class AppSerializer < Serega
88
- # plugin :one
89
- # plugin :two
90
-
91
- # config.one = :one
92
- # config.two = :two
37
+ plugin :activerecord_preloads # preloads associations used by requested fields
38
+ config.auto_preload = true
39
+ config.hide_by_default = :auto # fields that need extra queries are sent on request
93
40
  end
94
41
 
95
42
  class UserSerializer < AppSerializer
96
- # attribute :one
97
- # attribute :two
98
- end
99
-
100
- class CommentSerializer < AppSerializer
101
- # attribute :one
102
- # attribute :two
103
- end
104
- ```
105
-
106
- ### Adding attributes
107
-
108
- ⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "\_", "-",
109
- "~" characters.
110
-
111
- We allow ONLY these characters as we want to be able to use attribute names in
112
- URLs without escaping.
113
-
114
- This rule can be turned off with simple config option: `config.check_attribute_name = false`
115
-
116
- ```ruby
117
- class UserSerializer < Serega
118
- # Regular attribute
119
- attribute :first_name
120
-
121
- # Option :method specifies the name used to read the attribute's value —
122
- # a method called on the serialized object by default, or a Hash key when
123
- # combined with :hash_access
124
- attribute :first_name, method: :old_first_name
125
-
126
- # Attribute blocks below require a base serializer for nested serializers.
127
- # See the "Defining a nested serializer with a block" section below.
128
- config.base_serializer = Serega
129
-
130
- # Method :itself is handy to serialize the same object with a nested set of
131
- # attributes. Note: with a custom presenter the serialized value will be
132
- # the presenter instance itself; use `method: :__getobj__` to serialize the
133
- # original unwrapped object instead.
134
- attribute :statistics, method: :itself do
135
- attribute :likes_count
136
- attribute :comments_count
137
- end
138
-
139
- # Block defines a nested serializer for the attribute value.
140
- # See the "Defining a nested serializer with a block" section below.
141
- attribute :author do
142
- attribute :name
143
- end
144
-
145
- # Option :value can be used with a Proc or callable object to define
146
- # attribute value
147
- attribute :first_name, value: UserProfile.new # must have #call method
148
- attribute :first_name, value: proc { |user| user.profile&.first_name }
149
-
150
- # Option :delegate can be used to define attribute value.
151
- # Sub-option :allow_nil by default is false
152
- attribute :first_name, delegate: { to: :profile, allow_nil: true }
153
-
154
- # Option :delegate can be used with :method sub-option, so method chain here
155
- # is user.profile.fname
156
- attribute :first_name, delegate: { to: :profile, method: :fname }
157
-
158
- # Option :default can be used to replace possible nil values.
159
- attribute :first_name, default: ''
160
- attribute :is_active, default: false
161
- attribute :comments_count, default: 0
162
-
163
- # Option :const specifies attribute with a specific constant value
164
- attribute(:type, const: 'user')
165
-
166
- # Option :hide specifies attributes that should not be serialized by default
167
- attribute :tags, hide: true
168
-
169
- # Option :serializer specifies nested serializer for attribute
170
- # We can define the `:serializer` value as a Class, String, or Proc.
171
- # Use String or Proc if you have cross-references in serializers.
172
- attribute :posts, serializer: PostSerializer
43
+ attribute :name
173
44
  attribute :posts, serializer: "PostSerializer"
174
- attribute :posts, serializer: -> { PostSerializer }
175
-
176
- # Option `:many` specifies a has_many relationship. It is optional.
177
- # If not specified, it is defined during serialization by checking
178
- # `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`
179
- # Also the `:many` changes the default value from `nil` to `[]`.
180
- attribute :posts, serializer: PostSerializer, many: true
181
-
182
- # Option `:preload` allows to specify associations to preload to
183
- # attribute value
184
- attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
185
-
186
- # Options `:if, :unless, :if_value and :unless_value` can be specified
187
- # when `:if` plugin is enabled. They hide the attribute key and value from the
188
- # response.
189
- # See more usage examples in the `:if` plugin section.
190
- attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
191
- attribute :email, if_value: :present?
192
-
193
- # Option `:format` can be specified when enabled `:formatters` plugin
194
- # It changes the attribute value
195
- attribute :created_at, format: :iso_time
196
- attribute :updated_at, format: :iso_time
197
-
198
- # Option `:format` also can be used as Proc
199
- attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
200
- end
201
- ```
202
-
203
- ### Defining a nested serializer with a block
204
-
205
- An attribute block defines an anonymous nested serializer. The attribute value
206
- is found as usual — by the attribute name or the `:method`, `:value`,
207
- `:delegate`, `:batch` option — and is serialized by this nested serializer.
208
-
209
- The nested serializer is a regular subclass of an explicitly chosen **base
210
- serializer** — usually a settings-only serializer holding your plugins and
211
- configuration. Choose it with the `base_serializer:` attribute option or the
212
- `config.base_serializer` setting (attribute option wins; an error is raised
213
- when none is set):
214
-
215
- ```ruby
216
- class AppSerializer < Serega
217
- plugin :activerecord_preloads
218
-
219
- # Nested serializers defined with attribute blocks will inherit from
220
- # AppSerializer
221
- config.base_serializer = self
222
- end
223
-
224
- class UserSerializer < AppSerializer
225
- attribute :first_name
226
-
227
- # Serializes user.author with a nested serializer inherited from AppSerializer
228
- attribute :author do
229
- attribute :name
230
- end
231
-
232
- # Serializes the user itself, exposing only counters
233
- attribute :statistics, method: :itself, base_serializer: StatsBaseSerializer do
234
- attribute :likes_count
235
- attribute :comments_count
236
- end
237
- end
238
-
239
- UserSerializer.to_h(user)
240
- # => {first_name: "Bruce", author: {name: "Bob"}, statistics: {likes_count: 10, comments_count: 3}}
241
- ```
242
-
243
- The nested serializer inherits everything the base serializer has — plugins,
244
- config, attributes, batch loaders, the preload handler — through the regular
245
- inheritance mechanism. Any serializer can be used as a base: if it has
246
- attributes, they are serialized by the nested serializer too, together with
247
- the attributes defined in the block. Anything the base does not provide can
248
- be declared inside the block:
249
-
250
- ```ruby
251
- attribute :statistics, method: :itself do
252
- batch(:stats) { |users| Stats.for_users(users) } # => { user_id => stat }
253
-
254
- attribute :likes_count, batch: :stats, value: proc { |user, batches:| batches[:stats][user.id].likes }
255
- end
256
- ```
257
-
258
- Things to keep in mind:
259
-
260
- - The nested serializer is created at the moment the attribute is defined.
261
- Changes made to the base serializer later do not affect already defined
262
- nested serializers.
263
- - Errors raised while serializing nested attributes are reported with a
264
- readable serializer label, for example:
265
- `(when serializing 'comments_count' attribute in UserSerializer.<statistics>)`.
266
-
267
- ### Serializing
268
-
269
- We can serialize objects using class method `.call` aliased as `.to_h` and
270
- same instance methods `#call` and its alias `#to_h`.
271
-
272
- ```ruby
273
- user = OpenStruct.new(username: 'serega')
274
-
275
- class UserSerializer < Serega
276
- attribute :username
277
- end
278
-
279
- UserSerializer.to_h(user) # => {username: "serega"}
280
- UserSerializer.to_h([user]) # => [{username: "serega"}]
281
- ```
282
-
283
- Use `.to_data` / `#to_data` to get the same result as Ruby `Data` objects
284
- (immutable value objects, Ruby 3.2+). Nested serialized relations are also
285
- converted to `Data` objects.
286
-
287
- ```ruby
288
- result = UserSerializer.to_data(user)
289
- result # => #<data username="serega">
290
- result.username # => "serega"
291
-
292
- UserSerializer.to_data([user]) # => [#<data username="serega">]
293
- ```
294
-
295
- If serialized fields are constant, then it's a good idea to initiate the
296
- serializer and reuse it.
297
- It will be a bit faster (the serialization plan will be prepared only once).
298
-
299
- ```ruby
300
- # Example with all fields
301
- serializer = UserSerializer.new
302
- serializer.to_h(user1)
303
- serializer.to_h(user2)
304
-
305
- # Example with custom fields
306
- serializer = UserSerializer.new(only: [:username, :avatar])
307
- serializer.to_h(user1)
308
- serializer.to_h(user2)
309
- ```
310
-
311
- ### Selecting Fields
312
-
313
- By default, all attributes are serialized (except marked as `hide: true`).
314
-
315
- We can provide **modifiers** to select serialized attributes:
316
-
317
- - *only* - lists specific attributes to serialize;
318
- - *except* - lists attributes to not serialize;
319
- - *with* - lists attributes to serialize additionally (By default all attributes
320
- are exposed and will be serialized, but some attributes can be hidden when
321
- they are defined with the `hide: true` option, more on this below. `with`
322
- modifier can be used to expose such attributes).
323
-
324
- Modifiers can be provided as Hash, Array, String, Symbol, or their combinations.
325
-
326
- With plugin [string_modifiers][string_modifiers] we can provide modifiers as
327
- single `String` with attributes split by comma `,` and nested values inside
328
- brackets `()`, like: `first_name,last_name,addresses(line1,line2)`. This can be very useful
329
- to accept the list of fields in **GET** requests.
330
-
331
- When a non-existing attribute is provided, the `Serega::AttributeNotExist` error
332
- will be raised. This error can be muted with the `check_initiate_params: false`
333
- option.
334
-
335
- ```ruby
336
- class UserSerializer < Serega
337
- plugin :string_modifiers # to send all modifiers in one string
338
-
339
- attribute :username
340
- attribute :first_name
341
- attribute :last_name
342
- attribute :email, hide: true
343
- attribute :enemies, serializer: UserSerializer, hide: true
344
45
  end
345
46
 
346
- joker = OpenStruct.new(
347
- username: 'The Joker',
348
- first_name: 'jack',
349
- last_name: 'Oswald White',
350
- email: 'joker@mail.com',
351
- enemies: []
352
- )
353
-
354
- bruce = OpenStruct.new(
355
- username: 'Batman',
356
- first_name: 'Bruce',
357
- last_name: 'Wayne',
358
- email: 'bruce@wayneenterprises.com',
359
- enemies: []
360
- )
361
-
362
- joker.enemies << bruce
363
- bruce.enemies << joker
364
-
365
- # Default
366
- UserSerializer.to_h(bruce)
367
- # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
368
-
369
- # With `:only` modifier
370
- fields = [:username, { enemies: [:username, :email] }]
371
- fields_as_string = 'username,enemies(username,email)'
372
-
373
- UserSerializer.to_h(bruce, only: fields)
374
- UserSerializer.new(only: fields).to_h(bruce)
375
- UserSerializer.new(only: fields_as_string).to_h(bruce)
376
- # =>
377
- # {
378
- # :username=>"Batman",
379
- # :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]
380
- # }
381
-
382
- # With `:except` modifier
383
- fields = %i[first_name last_name]
384
- fields_as_string = 'first_name,last_name'
385
- UserSerializer.new(except: fields).to_h(bruce)
386
- UserSerializer.to_h(bruce, except: fields)
387
- UserSerializer.to_h(bruce, except: fields_as_string)
388
- # => {:username=>"Batman"}
389
-
390
- # With `:with` modifier
391
- fields = %i[email enemies]
392
- fields_as_string = 'email,enemies'
393
- UserSerializer.new(with: fields).to_h(bruce)
394
- UserSerializer.to_h(bruce, with: fields)
395
- UserSerializer.to_h(bruce, with: fields_as_string)
396
- # =>
397
- # {
398
- # :username=>"Batman",
399
- # :first_name=>"Bruce",
400
- # :last_name=>"Wayne",
401
- # :email=>"bruce@wayneenterprises.com",
402
- # :enemies=>[
403
- # {:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}
404
- # ]
405
- # }
406
-
407
- # With no existing attribute
408
- fields = %i[first_name enemy]
409
- fields_as_string = 'first_name,enemy'
410
- UserSerializer.new(only: fields).to_h(bruce)
411
- UserSerializer.to_h(bruce, only: fields)
412
- UserSerializer.to_h(bruce, only: fields_as_string)
413
- # => raises Serega::AttributeNotExist
414
-
415
- # With no existing attribute and disabled validation
416
- fields = %i[first_name enemy]
417
- fields_as_string = 'first_name,enemy'
418
- UserSerializer.new(only: fields, check_initiate_params: false).to_h(bruce)
419
- UserSerializer.to_h(bruce, only: fields, check_initiate_params: false)
420
- UserSerializer.to_h(bruce, only: fields_as_string, check_initiate_params: false)
421
- # => {:first_name=>"Bruce"}
422
- ```
423
-
424
- ### Using Context
425
-
426
- Sometimes it can be required to use the context during serialization, like
427
- current_user or any.
428
-
429
- ```ruby
430
- class UserSerializer < Serega
431
- attribute :email, value: proc { |user, ctx|
432
- user.email if ctx[:current_user] == user
433
- }
434
- end
435
-
436
- user = OpenStruct.new(email: 'email@example.com')
437
- UserSerializer.(user, context: {current_user: user})
438
- # => {:email=>"email@example.com"}
439
-
440
- UserSerializer.new.to_h(user, context: {current_user: user}) # same
441
- # => {:email=>"email@example.com"}
442
- ```
443
-
444
- ### Batch Loading
445
-
446
- Serega includes built-in batch loading functionality to efficiently load data on demand and solve N+1 problems.
447
-
448
- #### Defining Named Batch Loaders
47
+ class PostSerializer < AppSerializer
48
+ batch :comments_count, ->(posts) { Comment.where(post: posts).group(:post_id).count }
449
49
 
450
- Named loaders can be defined using the `batch` class method and reused across attributes:
451
-
452
- ```ruby
453
- class UserSerializer < Serega
454
- # Define named loaders
455
- batch :comments_count, ->(users) { Comment.where(user: users).group(:user_id).count }
456
- batch :comments_count, CommentsCountLoader # Example with callable class
457
-
458
- # Use a loader by its name
459
- attribute :comments_count, batch: :comments_count
460
-
461
- # Equivalent — when the attribute name matches the loader name
50
+ attribute :title
462
51
  attribute :comments_count, batch: true
463
-
464
- # Equivalent — the default value resolution spelled out
465
- attribute :comments_count, batch: :comments_count,
466
- value: proc { |user, batches:| batches[:comments_count][user.id] }
467
-
468
- # Hash form — needed only for sub-options, for example a custom `:id` method
469
- attribute :comments_count, batch: { use: :comments_count, id: :uuid }
470
52
  end
471
- ```
472
53
 
473
- #### Using Custom Loaders
54
+ UserSerializer.to_h(User.all)
55
+ # => [{name: "Bruce Wayne"}, ...] 1 query
474
56
 
475
- Custom loaders can be provided directly using the `:use` option with any callable object:
476
-
477
- ```ruby
478
- class UserSerializer < Serega
479
- # Inline proc loader
480
- attribute :followers_count,
481
- batch: { use: ->(users) { Follow.where(user: users).group(:user_id).count } }
482
-
483
- # Callable class loader
484
- attribute :likes_count, batch: { use: LikesCountLoader }
485
-
486
- # Method reference
487
- attribute :views_count, batch: { use: ViewsCounter.method(:batch_load) }
488
- end
57
+ UserSerializer.to_h(User.all, only: "name,posts(title,comments_count)")
58
+ # => [{name: "Bruce Wayne", posts: [{title: "Gotham at night", comments_count: 12}, ...]}, ...]
59
+ # 3 queries, for any number of users
489
60
  ```
490
61
 
491
- #### Using Multiple Loaders in Same Attribute
492
-
493
- Custom loaders can be provided directly using the `:use` option with any callable object:
494
-
495
- ```ruby
496
- class UserSerializer < Serega
497
- # Define named loaders
498
- batch :facebook_likes, FacebookLikesLoader
499
- batch :twitter_likes, TwitterLikesLoader
500
-
501
- # Summarize likes
502
- attribute :likes_count,
503
- batch: { use: [:facebook_likes, :twitter_likes] },
504
- value: proc { |user, batches:| batches[:facebook_likes][user.id] + batches[:twitter_likes][user.id] }
505
- end
506
- ```
507
-
508
- ### Prepare Initial Objects
509
-
510
- `prepare_initial_objects` replaces the serialized objects before serialization
511
- starts, so a serializer can accept ids or other references and load the records
512
- itself.
513
-
514
- ```ruby
515
- class UserSerializer < Serega
516
- # OccamsRecord returns read-only records — faster and with a much lower
517
- # memory footprint than ActiveRecord models
518
- prepare_initial_objects { |user_ids| OccamsRecord.query(User.where(id: user_ids)).run }
519
-
520
- attribute :first_name
521
- end
522
-
523
- UserSerializer.to_h(["17", "42"]) # => [{first_name: "Ann"}, {first_name: "Bob"}]
524
- ```
525
-
526
- The handler accepts the serialization context as a second positional or a
527
- `:ctx` keyword argument, and can be provided as a callable value:
528
-
529
- ```ruby
530
- class UserSerializer < Serega
531
- prepare_initial_objects { |ids, ctx| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
532
- # or
533
- prepare_initial_objects { |ids, ctx:| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
534
- # or
535
- prepare_initial_objects UsersLoader
536
- end
537
- ```
538
-
539
- The handler runs once per serialization, before the `:many` option is detected,
540
- so it can turn a single object into a collection and back:
541
-
542
- ```ruby
543
- class UserSerializer < Serega
544
- prepare_initial_objects { |user_id| OccamsRecord.query(User.where(id: user_id)).run }
545
-
546
- attribute :first_name
547
- end
548
-
549
- UserSerializer.to_h("17") # => [{first_name: "Ann"}] - an array, as the handler returned a collection
550
- ```
551
-
552
- A provided `:many` option is used as is. A handler returning `nil` serializes
553
- `nil`, and errors raised inside the handler are not wrapped.
554
-
555
- Declared preloads are applied to the prepared objects, so the handler pairs
556
- with [preloads][preloads] and the
557
- [activerecord_preloads][activerecord_preloads] plugin:
558
-
559
- ```ruby
560
- class UserSerializer < Serega
561
- plugin :activerecord_preloads
562
-
563
- prepare_initial_objects { |user_ids| User.where(id: user_ids) }
564
-
565
- attribute :albums_count, preload: :albums, value: proc { |user| user.albums.size }
566
- end
567
- ```
568
-
569
- ⚠️ The `:preload` option needs ActiveRecord objects. When the handler returns
570
- anything else — OccamsRecord read-only records, Structs, Hashes, plain objects
571
- — preloading raises `Serega::SeregaError` (`Can't preload ... to ...`) during
572
- serialization. Load that data with [batch loading](#batch-loading) instead,
573
- which works with any objects.
574
-
575
- The handler runs for the serialized objects only, and not for objects of nested
576
- serializers. It is inherited by subclasses, so declare it on concrete
577
- serializers rather than on a base serializer shared by all of them.
578
-
579
- ## Configuration
580
-
581
- Here are the default options. Other options can be added with plugins.
582
-
583
- ⚠️ Attributes are prepared at the moment they are defined. If a config option
584
- influences attributes (`auto_preload`, `hide_by_default`, `batch_id_option`,
585
- formatters, etc.), changing it affects only attributes defined **after** the
586
- change — including nested serializers defined with attribute blocks, which
587
- are created at their definition point. Configure the serializer before
588
- defining attributes.
589
-
590
- ```ruby
591
- class AppSerializer < Serega
592
- # With `activerecord_preloads` plugin it automatically adds `preload` option
593
- # to attributes with `:delegate` or `:serializer` option.
594
- # It helps to preload associations automatically, omitting N+1 requests.
595
- config.auto_preload = false
596
-
597
- # Hides attributes by default. Accepts:
598
- # false - nothing hidden (default)
599
- # true - all attributes hidden
600
- # [:preload] - attributes with :preload option hidden
601
- # [:batch] - attributes with :batch option hidden
602
- # [:preload, :batch] - either option triggers hiding
603
- # Useful to avoid extra DB requests for attributes that were not requested.
604
- config.hide_by_default = false
605
-
606
- # Default method used on serialized object to resolve batch value
607
- # For example:
608
- # attribute :counter, batch: CounterBatchLoader
609
- # # Attribute values will be resolved as:
610
- # proc { |object, batches:| batches[:counter][object.id] }
611
- config.batch_id_option = :id
612
-
613
- # Defaults for the `hash_access:` attribute option — `default_mode` is
614
- # what `hash_access: true` (and a Hash form omitting :mode) resolves to.
615
- # See "Serializing Hash records".
616
- config.hash_access.default_mode = :symbol # the default
617
- config.hash_access.default_allow_missing_key = false # the default
618
-
619
- # Parent class for nested serializers defined with attribute blocks.
620
- # Usually a settings-only serializer, e.g. `config.base_serializer = self`
621
- # in an application base serializer class. There is no default — an
622
- # attribute block raises an error when no base serializer is chosen (it can
623
- # also be provided per-attribute with the `base_serializer:` option).
624
- config.base_serializer = self
625
-
626
- # Disable/enable validation of modifiers (`:with, :except, :only`)
627
- # By default, this validation is enabled.
628
- # After disabling, all requested incorrect attributes will be skipped.
629
- config.check_initiate_params = false # default is true, enabled
630
-
631
- # Stores in memory prepared `plans` - list of serialized attributes.
632
- # Next time serialization happens with the same modifiers (`only, except, with`),
633
- # we will reuse already prepared `plans`.
634
- # This defines storage size (count of stored `plans` with different modifiers).
635
- config.max_cached_plans_per_serializer_count = 50 # default is 0, disabled
636
- end
637
- ```
638
-
639
- ## Preloads
640
-
641
- Serega includes built-in preloads functionality that lets you declare
642
- `:preload` on attributes. It is used by the `:activerecord_preloads` plugin:
643
- enable it and every association you declare is loaded once during
644
- serialization, automatically, with no N+1.
645
-
646
- Configuration options:
647
-
648
- - `config.auto_preload` - default `false` (use `true` or `{ has_delegate_option: true, has_serializer_option: true }`)
649
- - `config.hide_by_default` - default `false` (use `:auto` to hide attributes with preloads or batch loaders)
650
-
651
- These options are extremely useful if you want to forget about finding
652
- preloads manually.
653
-
654
- The `:preload` value is passed to the preload handler exactly as written — a
655
- Symbol, Array, Hash, or any custom value your ORM understands. Preloads can be
656
- disabled with `preload: false` (or `preload: nil`); `preload: true` is not a
657
- valid value. Automatically added preloads can be overwritten with the manually
658
- specified `preload: :xxx` option.
659
-
660
- The actual loading is performed by a handler registered with `preload_with`. The
661
- `:activerecord_preloads` plugin registers one for you; to preload with another
662
- ORM — or to attach data to plain non-ORM objects by your own rules — register
663
- your own (see [Custom preloading](#custom-preloading)). Declaring `:preload`
664
- without a registered handler raises an error, so a preload never silently does
665
- nothing.
666
-
667
- For some examples, **please read the comments in the code below**
668
-
669
- ```ruby
670
- class AppSerializer < Serega
671
- config.auto_preload = true
672
- config.hide_by_default = :auto
673
- end
674
-
675
- class UserSerializer < AppSerializer
676
- # No preloads
677
- attribute :username
678
-
679
- # `preload: :user_stats` added manually
680
- attribute :followers_count, preload: :user_stats,
681
- value: proc { |user| user.user_stats.followers_count }
682
-
683
- # `preload: :user_stats` added automatically, as
684
- # `auto_preload` includes `has_delegate_option: true`
685
- attribute :comments_count, delegate: { to: :user_stats }
686
-
687
- # `preload: :albums` added automatically as
688
- # `auto_preload` includes `has_serializer_option: true`
689
- attribute :albums, serializer: 'AlbumSerializer'
690
- end
691
-
692
- class AlbumSerializer < AppSerializer
693
- attribute :images_count, delegate: { to: :album_stats }
694
- end
62
+ ## Installation
695
63
 
696
- # With `hide_by_default = :auto`, attributes that declare a preload are hidden
697
- # unless explicitly requested with `:with`. When requested, their associations
698
- # are preloaded automatically. For example:
699
- #
700
- # UserSerializer.to_h(users, with: [:followers_count, { albums: :images_count }])
701
- #
702
- # preloads `:user_stats` and `:albums` on the users, and `:album_stats` on the
703
- # albums.
64
+ ```sh
65
+ bundle add serega
704
66
  ```
705
67
 
706
- ---
707
-
708
- ### Serializing the same object in association
68
+ ## Quick start
709
69
 
710
- For example, you show your current user as "user" and use the same user object
711
- to serialize "user_stats". `UserStatSerializer` relies on user fields and any
712
- other user associations. Specify `preload: nil` so that nothing is preloaded for
713
- this attribute on the "user" object — the associations declared in
714
- `UserStatSerializer` are still preloaded onto the same user object.
70
+ Define a base serializer with the setup your app uses. Every serializer
71
+ inherits it.
715
72
 
716
73
  ```ruby
74
+ # app/serializers/app_serializer.rb
717
75
  class AppSerializer < Serega
76
+ plugin :activerecord_preloads
718
77
  config.auto_preload = true
719
- config.hide_by_default = :auto
720
78
  end
721
79
 
80
+ # app/serializers/user_serializer.rb
722
81
  class UserSerializer < AppSerializer
723
- attribute :username
724
- attribute :user_stats,
725
- serializer: 'UserStatSerializer',
726
- value: proc { |user| user },
727
- preload: nil
728
- end
729
- ```
730
-
731
- ---
732
-
733
- ### Custom preloading
734
-
735
- The `:activerecord_preloads` plugin works out of the box, but the preload
736
- mechanism itself is not tied to ActiveRecord — or to ORMs at all. Register a
737
- handler with `preload_with` and it is called once per preloaded attribute with
738
- the gathered objects and that attribute's `:preload` value, before the
739
- attribute is serialized:
740
-
741
- ```ruby
742
- class UserSerializer < Serega
82
+ attribute :id
743
83
  attribute :name
744
- attribute :posts, serializer: PostSerializer, preload: :posts
745
-
746
- # `objects` is the gathered users; `preloads` is this attribute's
747
- # `:preload` value (`:posts`). `MyORM.preload` does the eager loading.
748
- preload_with { |objects, preloads| MyORM.preload(objects, preloads) }
84
+ attribute :email, value: proc { |user, ctx| user.email if user == ctx[:current_user] }
85
+ attribute :avatar_url, hide: true
749
86
  end
750
- ```
751
87
 
752
- The handler can be a block or any callable taking `(objects, preloads)`. It is
753
- inherited by child serializers and can be overridden per subclass. The
754
- `:preload` value reaches the handler untouched, so you can pass whatever your
755
- handler expects — not necessarily something an ORM understands. The gathered
756
- objects can be plain Ruby objects too: the handler decides where the data comes
757
- from (another ORM, an HTTP API, a cache) and how to attach it to the objects,
758
- by whatever rules you define.
88
+ # app/controllers/users_controller.rb
89
+ class UsersController < ApplicationController
90
+ def index
91
+ users = UserSerializer.to_json(
92
+ User.all,
93
+ only: params[:fields], # "id,name" from GET /users?fields=id,name
94
+ context: {current_user: current_user}
95
+ )
759
96
 
760
- The handler runs **once per preloaded attribute**, not once per distinct
761
- `:preload` value. If the same `:preload` is declared on several attributes, the
762
- handler is invoked once for each of them over the same objects, so it should
763
- check whether the data is already loaded before loading it again. (The
764
- `:activerecord_preloads` handler relies on `ActiveRecord::Associations::Preloader`,
765
- which already skips associations that are loaded.)
766
-
767
- Because you choose the `:preload` value, it doubles as a **discriminator**: give
768
- two attributes different `:preload` values and branch on `preloads` inside the
769
- handler — no need for the value to be a real association name.
770
-
771
- ```ruby
772
- class PostSerializer < Serega
773
- attribute :owner, serializer: UserSerializer, preload: :owner
774
- attribute :author, serializer: UserSerializer, preload: :author
775
-
776
- # Both attributes load the same person, but from different sources.
777
- preload_with do |objects, preloads|
778
- case preloads
779
- when :owner then load_owners(objects) # e.g. from the DB
780
- when :author then load_authors(objects) # e.g. from an external API
781
- else MyORM.preload(objects, preloads)
782
- end
97
+ render json: users
783
98
  end
784
99
  end
785
100
  ```
786
101
 
787
- ---
788
-
789
- ## Serializing Hash records
790
-
791
- (objects should implement `#[]`, `#fetch`, and `#key?` - Hash already has them)
102
+ ## Benchmarks
792
103
 
793
- The `hash_access: <mode>` attribute option makes an attribute read its value from
794
- Hash keys instead of calling a method.
104
+ Serega is on par with the fastest Ruby serializers in speed and allocations,
105
+ and in some cases beats them. Results, the details of each case and how to run
106
+ them: [benchmarks/README.md](https://github.com/andreyruby/serega/blob/master/benchmarks/README.md).
795
107
 
796
- Allowed `<mode>`:
108
+ ## Features
797
109
 
798
- - `:symbol` reads record[:name]
799
- - `:string` reads record["name"]
800
- - `true` shorthand for `config.hash_access.default_mode` (`:symbol` by default)
110
+ ### Select fields
801
111
 
802
- By default missing keys will raise `KeyError`.
803
-
804
- `config.hash_access.default_allow_missing_key = true` will change `KeyError`
805
- to return `nil` value instead. `config.hash_access.default_mode = <mode>`
806
- changes what `true` (and a Hash form omitting `:mode`) resolves to.
112
+ `only`, `except` and `with` accept Symbols, Arrays, Hashes or a string like
113
+ `"name,posts(title)"`. Unknown fields raise `Serega::AttributeNotExist`.
807
114
 
808
115
  ```ruby
809
- class UserSerializer < Serega
810
- config.hash_access.default_mode = :symbol # the default
811
- config.hash_access.default_allow_missing_key = true # false by default
812
-
813
- attribute :name, hash_access: true # reads record[:name] (config.hash_access.default_mode)
814
- attribute :name, hash_access: :symbol # reads record[:name]
815
- attribute :name, hash_access: :string # reads record["name"]
816
-
817
- # Use long form if you need to override `allow_missing_key`
818
- attribute :name, hash_access: { allow_missing_key: false }
819
- attribute :name, hash_access: { mode: :symbol, allow_missing_key: false }
820
- attribute :name, hash_access: { mode: :string, allow_missing_key: false }
821
- end
116
+ UserSerializer.to_h(user, only: [:name, {posts: [:title]}])
117
+ UserSerializer.to_h(user, except: :email)
118
+ UserSerializer.to_h(user, with: :avatar_url) # adds hidden fields
822
119
  ```
823
120
 
824
- Delegated attributes configure hash access **per step**:
825
-
826
- - `to_hash_access` configures reading `<mode>` for intermediate object
827
- - `hash_access` configures reading `<mode>` for final key
828
-
829
- ```ruby
830
- # reads `record.address.city` (no hash access)
831
- attribute :city, delegate: { to: :address } # record.address.city
832
-
833
- # reads record.address[:city]
834
- attribute :city, delegate: { to: :address, hash_access: :symbol }
121
+ → [Selecting fields guide](docs/selecting-fields.md)
835
122
 
836
- # reads record[:address].city
837
- attribute :city, delegate: { to: :address, to_hash_access: :symbol }
123
+ ### Batch loading
838
124
 
839
- # reads record[:address][:city]
840
- attribute :city, delegate: { to: :address, to_hash_access: :symbol, hash_access: :symbol }
841
-
842
- # reads record["address"] and if exists returns record["address"][:city]
843
- attribute :city,
844
- delegate: {
845
- to: :address,
846
- allow_nil: true,
847
- to_hash_access: { mode: :string, allow_missing_key: true },
848
- hash_access: { mode: :symbol, allow_missing_key: false }
849
- }
850
- ```
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:
125
+ Load data for all serialized objects with one call.
859
126
 
860
127
  ```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
128
+ class UserSerializer < AppSerializer
129
+ batch :followers_count, ->(users) { Follow.where(user: users).group(:user_id).count }
878
130
 
879
- def role
880
- id == __ctx__[:current_user_id] ? :self : :other
881
- end
882
- end
131
+ attribute :followers_count, batch: true
883
132
  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
133
  ```
889
134
 
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__`.
135
+ → [Batch loading guide](docs/batch-loading.md) ·
136
+ [Preloads guide](docs/preloads.md)
903
137
 
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.
138
+ ### Formatters
907
139
 
908
- ## Plugins
909
-
910
- ### Plugin :activerecord_preloads
911
-
912
- Automatically preloads associations to serialized objects.
913
-
914
- Every association you declare with `:preload` is loaded once during
915
- serialization using `ActiveRecord::Associations::Preloader`, so there are no
916
- N+1 queries. The plugin does this by registering a [`preload_with`](#custom-preloading)
917
- handler; to use a different ORM, register your own instead.
140
+ Format dates, money and other values the same way everywhere.
918
141
 
919
142
  ```ruby
920
143
  class AppSerializer < Serega
921
- config.auto_preload = true
922
- plugin :activerecord_preloads
923
- end
924
-
925
- class AlbumSerializer < AppSerializer
926
- # no preloads
927
- attribute :title
928
-
929
- # preloads :downloads, as manually specified
930
- attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
144
+ formatter :iso8601, [:iso8601, 3], allow_nil: true
931
145
  end
932
146
 
933
147
  class UserSerializer < AppSerializer
934
- # no preloads
935
- attribute :username
936
-
937
- # preloads :user_stats, as auto_preload is enabled for :delegate attributes
938
- attribute :comments_count, delegate: { to: :user_stats }
939
-
940
- # preloads :albums, as auto_preload is enabled for :serializer attributes
941
- attribute :albums, serializer: AlbumSerializer
942
- end
943
-
944
- UserSerializer.to_h(users)
945
- # 1 query to load :user_stats for all users
946
- # + 1 query to load :albums for all users
947
- # + 1 query to load :downloads for all albums
948
- # = 3 queries total, regardless of how many users/albums are serialized
949
- ```
950
-
951
- ### Plugin :root
952
-
953
- Adds a root key to serialized data.
954
-
955
- Accepts options:
956
-
957
- - :root - specifies root for all responses
958
- - :root_one - specifies the root key for single object serialization only
959
- - :root_many - specifies the root key for multiple objects serialization only
960
-
961
- Adds additional config options:
962
-
963
- - config.root.one
964
- - config.root.many
965
- - config.root.one=
966
- - config.root.many=
967
-
968
- The default root is `:data`.
969
-
970
- ```ruby
971
- class UserSerializer < Serega
972
- plugin :root # default root is :data
973
- end
974
-
975
- class UserSerializer < Serega
976
- plugin :root, root: :users
977
- end
978
-
979
- class UserSerializer < Serega
980
- plugin :root, root_one: :user, root_many: :people
981
- end
982
-
983
- class UserSerializer < Serega
984
- plugin :root, root: nil # no root key by default
985
- end
986
- ```
987
-
988
- The root key can also be changed per serialization, or removed entirely by
989
- providing `root: nil` (it can still be added back per serialization).
990
-
991
- ```ruby
992
- class UserSerializer < Serega
993
- plugin :root
148
+ attribute :created_at, format: :iso8601
994
149
  end
995
-
996
- UserSerializer.to_h(nil) # => {:data=>nil}
997
- UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
998
- UserSerializer.to_h(nil, root: nil) # => nil
999
150
  ```
1000
151
 
1001
- ### Plugin :metadata
1002
-
1003
- Depends on: [`:root`][root] plugin, that must be loaded first
1004
-
1005
- Adds metadata to the serialized response via the class-level `meta_attribute`
1006
- method, which accepts:
152
+ → [Formatters guide](docs/formatters.md)
1007
153
 
1008
- - `*path` [Array of Symbols] - nested hash keys.
1009
- - `**options` [Hash]
154
+ ### Hashes, Structs and Data
1010
155
 
1011
- - `:const` - describes metadata value (if it is constant)
1012
- - `:value` - describes metadata value as any `#callable` instance
1013
- - `:hide_nil` - does not show the metadata key if the value is nil.
1014
- It is `false` by default
1015
- - `:hide_empty` - does not show the metadata key if the value is nil or empty.
1016
- It is `false` by default.
1017
-
1018
- - `&block` [Proc] - describes value for the current meta attribute
156
+ Serialize Hash records, and get results as `Struct` or `Data` objects.
1019
157
 
1020
158
  ```ruby
1021
- class AppSerializer < Serega
1022
- plugin :root
1023
- plugin :metadata
1024
-
1025
- meta_attribute(:version, const: '1.2.3')
1026
- meta_attribute(:ab_tests, :names, value: ABTests.new.method(:names))
1027
- meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
1028
- next unless records.respond_to?(:total_count)
1029
-
1030
- {
1031
- page: records.page,
1032
- per_page: records.per_page,
1033
- total_count: records.total_count
1034
- }
1035
- end
159
+ class EventSerializer < Serega
160
+ attribute :title, hash_access: true # reads event[:title]
1036
161
  end
1037
162
 
1038
- AppSerializer.to_h(nil)
1039
- # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=> ... }}
163
+ UserSerializer.to_h(user) # => {name: "Bruce Wayne"}
164
+ UserSerializer.to_struct(user) # => #<struct name="Bruce Wayne">
165
+ UserSerializer.to_data(user) # => #<data name="Bruce Wayne">
1040
166
  ```
1041
167
 
1042
- ### Plugin :context_metadata
1043
-
1044
- Depends on: [`:root`][root] plugin, that must be loaded first
168
+ → [Hash records guide](docs/hash-records.md) ·
169
+ [Serializing guide](docs/serializing.md)
1045
170
 
1046
- Adds metadata supplied per serialization call (as opposed to `:metadata`,
1047
- which is defined statically on the serializer).
171
+ ### Ractors
1048
172
 
1049
- Accepts option `:context_metadata_key` with the name of the root metadata keyword.
1050
- By default, it has the `:meta` value.
1051
-
1052
- The key can be changed in children serializers using this method:
1053
- `config.context_metadata.key=(value)`.
173
+ Serialize in parallel Ractors.
1054
174
 
1055
175
  ```ruby
1056
176
  class UserSerializer < Serega
1057
- plugin :root, root: :data
1058
- plugin :context_metadata, context_metadata_key: :meta
1059
-
1060
- # Same:
1061
- # plugin :context_metadata
1062
- # config.context_metadata.key = :meta
1063
- end
1064
-
1065
- UserSerializer.to_h(nil, meta: { version: '1.0.1' })
1066
- # => {:data=>nil, :version=>"1.0.1"}
1067
- ```
1068
-
1069
- ### Plugin :formatters
1070
-
1071
- Defines named value formatters once and applies them to any attribute.
1072
-
1073
- Use `config.formatters.add()` to register formatters. The `:format`
1074
- attribute option then accepts a formatter name or a callable directly.
1075
-
1076
- Formatters receive up to 2 parameters: the value and the context.
1077
-
1078
- ```ruby
1079
- class AppSerializer < Serega
1080
- plugin :formatters, formatters: {
1081
- iso8601: ->(value) { value.iso8601 },
1082
- on_off: ->(value) { value ? 'ON' : 'OFF' },
1083
- money: ->(value) { value.round(2) },
1084
- date: DateTypeFormatter # callable
1085
- }
1086
- end
1087
-
1088
- class UserSerializer < Serega
1089
- # Additionally we can add formatters via config in subclasses
1090
- config.formatters.add(
1091
- iso8601: ->(value) { value.iso8601 },
1092
- on_off: ->(value) { value ? 'ON' : 'OFF' },
1093
- money: ->(value) { value.round(2) }
1094
- )
1095
-
1096
- # Using predefined formatter
1097
- attribute :commission, format: :money
1098
- attribute :is_logged_in, format: :on_off
1099
- attribute :created_at, format: :iso8601
1100
- attribute :updated_at, format: :iso8601
1101
-
1102
- # Using `callable` formatter
1103
- attribute :score_percent, format: PercentFormatter # callable class
1104
- attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
1105
- end
1106
- ```
1107
-
1108
- ### Plugin :string_modifiers
1109
-
1110
- Allows `:only`, `:except` and `:with` to be given as a single comma-separated
1111
- string, with nested attributes in parentheses. Useful for accepting a field
1112
- list straight from a query parameter.
1113
-
1114
- Modifiers can still be provided the old way, as nested hashes or arrays.
1115
-
1116
- ```ruby
1117
- PostSerializer.plugin :string_modifiers
1118
- PostSerializer.new(only: "id,user(id,username)").to_h(post)
1119
- PostSerializer.new(except: "user(username,email)").to_h(post)
1120
- PostSerializer.new(with: "user(email)").to_h(post)
1121
-
1122
- PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
1123
- ```
1124
-
1125
- ### Plugin :if
1126
-
1127
- Adds `:if`, `:unless`, `:if_value`, `:unless_value` attribute options to
1128
- conditionally remove attributes from the response.
1129
-
1130
- `:if`/`:unless` receive the serialized object and context, and are checked
1131
- before the attribute value is found. `:if_value`/`:unless_value` receive the
1132
- already-found value and context, checked after. The latter two cannot be
1133
- used with the `:serializer` option, since a relationship has no "serialized
1134
- value" of its own — use `:if`/`:unless` instead.
1135
-
1136
- See also a `:hide` option that is available without any plugins to hide
1137
- attribute without conditions.
1138
- Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
1139
-
1140
- ```ruby
1141
- class UserSerializer < Serega
1142
- attribute :email, if: :active? # translates to `if user.active?`
1143
- attribute :email, if: proc {|user| user.active?} # same
1144
- attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
1145
- attribute :email, if: CustomPolicy.method(:view_email?)
1146
-
1147
- attribute :email, unless: :hidden? # translates to `unless user.hidden?`
1148
- attribute :email, unless: proc {|user| user.hidden?} # same
1149
- attribute :email, unless: proc {|user, context| context[:show_emails]}
1150
- attribute :email, unless: CustomPolicy.method(:hide_email?)
1151
-
1152
- attribute :email, if_value: :present? # if email.present?
1153
- attribute :email, if_value: proc {|email| email.present?} # same
1154
- attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
1155
- attribute :email, if_value: CustomPolicy.method(:view_email?)
1156
-
1157
- attribute :email, unless_value: :blank? # unless email.blank?
1158
- attribute :email, unless_value: proc {|email| email.blank?} # same
1159
- attribute :email, unless_value: proc {|email, context| context[:show_emails]}
1160
- attribute :email, unless_value: CustomPolicy.method(:hide_email?)
1161
- end
1162
- ```
1163
-
1164
- ### Plugin :camel_case
1165
-
1166
- Without this plugin, responding with *camelCased* keys means specifying the
1167
- attribute name and method directly for each attribute:
1168
- `attribute :firstName, method: first_name`
1169
-
1170
- This plugin camelCases every attribute name automatically, replacing `_x`
1171
- with `X` throughout the string. The transformation runs once, when the
1172
- attribute is defined, not on every serialization.
1173
-
1174
- Provide a custom transformation when adding the plugin, for example
1175
- `plugin :camel_case, transform: ->(name) { name.camelize }`
1176
-
1177
- Skip camelCase for a single attribute with `camel_case: false`.
1178
-
1179
- This plugin transforms only attribute keys, without affecting the `root`,
1180
- `metadata` and `context_metadata` plugins keys.
1181
-
1182
- If you wish to [select serialized fields](#selecting-fields), you should
1183
- provide them camelCased.
1184
-
1185
- ```ruby
1186
- class AppSerializer < Serega
1187
- plugin :camel_case
1188
- end
1189
-
1190
- class UserSerializer < AppSerializer
1191
- attribute :first_name
1192
- attribute :last_name
1193
- attribute :full_name, camel_case: false,
1194
- value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
177
+ attribute :name
178
+ make_shareable!
1195
179
  end
1196
180
 
1197
- require "ostruct"
1198
- user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
1199
- UserSerializer.to_h(user)
1200
- # => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
1201
-
1202
- UserSerializer.new(only: %i[firstName lastName]).to_h(user)
1203
- # => {firstName: "Bruce", lastName: "Wayne"}
181
+ Ractor.new(user) { |user| UserSerializer.to_h(user) }.value
1204
182
  ```
1205
183
 
1206
- ### Plugin :depth_limit
1207
-
1208
- Helps to secure from malicious queries that serialize too much
1209
- or from accidental serializing of objects with cyclic relations.
1210
-
1211
- Depth limit is checked when constructing a serialization plan, that is when
1212
- `#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
1213
- It can be useful to instantiate serializer before any other business logic
1214
- to get possible errors earlier.
1215
-
1216
- Any class-level serialization methods also check the depth limit as they also
1217
- instantiate serializer.
184
+ → [Ractors guide](docs/ractors.md)
1218
185
 
1219
- When the depth limit is exceeded `Serega::DepthLimitError` is raised.
1220
- Depth limit error details can be found in the additional
1221
- `Serega::DepthLimitError#details` method
186
+ ### And more
1222
187
 
1223
- The limit can be checked or changed with the next config options:
188
+ - [Attributes](docs/attributes.md) — all attribute options, relations and nested serializers
189
+ - [Conditional attributes](docs/conditional-attributes.md) — `if:`, `unless:`, `if_value:`, `unless_value:`
190
+ - [Context](docs/context.md) — pass `current_user` and other request data
191
+ - [Presenter](docs/presenter.md) — write computed attributes as methods
192
+ - [Prepare initial objects](docs/prepare-initial-objects.md) — serialize records by ids
193
+ - [Configuration](docs/configuration.md) — all config options
194
+ - [Root key and metadata](docs/root-and-metadata.md) — wrap results in `{data: ...}` and add metadata
1224
195
 
1225
- - `config.depth_limit.limit`
1226
- - `config.depth_limit.limit=`
196
+ ## Documentation
1227
197
 
1228
- There is no default limit, but it should be set when enabling the plugin.
198
+ - 📋 [Cheatsheet](CHEATSHEET.md) — every option on one page
199
+ - 📚 [Guides](docs/README.md) — each feature in depth
200
+ - 🔎 [Documentation site](https://andreyruby.github.io/serega/) — guides and API reference
201
+ - 🔤 [Type signatures](https://github.com/andreyruby/serega/tree/master/sig) — RBS of the public API
202
+ - 📝 [Changelog](https://github.com/andreyruby/serega/blob/master/CHANGELOG.md)
1229
203
 
1230
- ```ruby
1231
- class AppSerializer < Serega
1232
- plugin :depth_limit, limit: 10 # set limit for all child classes
1233
- end
1234
-
1235
- class UserSerializer < AppSerializer
1236
- config.depth_limit.limit = 5 # overrides limit for UserSerializer
1237
- end
1238
- ```
1239
-
1240
- ### Plugin :explicit_many_option
1241
-
1242
- Requires the `:many` option on every relationship attribute (an attribute
1243
- with the `:serializer` option or a block defining a nested serializer), so
1244
- it's always explicit whether it returns one object or many.
1245
-
1246
- ```ruby
1247
- class BaseSerializer < Serega
1248
- plugin :explicit_many_option
1249
- config.base_serializer = self
1250
- end
1251
-
1252
- class PostSerializer < BaseSerializer
1253
- attribute :text
1254
-
1255
- attribute :user, many: false do
1256
- attribute :name
1257
- end
204
+ ## Development
1258
205
 
1259
- attribute :comments, serializer: PostSerializer, many: true
1260
- end
206
+ ```sh
207
+ bundle install
208
+ bundle exec rake # tests and linter
209
+ bin/console # irb with serega loaded
1261
210
  ```
1262
211
 
1263
- ## Errors
1264
-
1265
- - The `Serega::SeregaError` is a base error raised by this gem.
1266
- - The `Serega::AttributeNotExist` error is raised when validating attributes in
1267
- `:only, :except, :with` modifiers. This error contains additional methods:
1268
-
1269
- - `#serializer` - shows current serializer
1270
- - `#attributes` - lists not existing attributes
1271
-
1272
- ## Release
1273
-
1274
- To release a new version, read [RELEASE.md](https://github.com/aglushkov/serega/blob/master/RELEASE.md).
1275
-
1276
- ## Development
1277
-
1278
- - `bundle install` - install dependencies
1279
- - `bin/console` - open irb console with loaded gems
1280
- - `bundle exec rspec` - run tests
1281
- - `bundle exec rubocop` - check code standards
1282
- - `yard stats --list-undoc --no-cache` - view undocumented code
1283
- - `yard server --reload` - view code documentation
212
+ Releasing: [RELEASE.md](https://github.com/andreyruby/serega/blob/master/RELEASE.md).
1284
213
 
1285
214
  ## Contributing
1286
215
 
1287
- Bug reports, pull requests and improvements ideas are very welcome!
216
+ Bug reports, pull requests and ideas are welcome!
1288
217
 
1289
218
  ## License
1290
219
 
1291
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
220
+ The gem is available as open source under the terms of the
221
+ [MIT License](https://opensource.org/licenses/MIT).
1292
222
 
1293
- [activerecord_preloads]: #plugin-activerecord_preloads
1294
- [batch-loading]: #batch-loading
1295
- [camel_case]: #plugin-camel_case
1296
- [context_metadata]: #plugin-context_metadata
1297
- [depth_limit]: #plugin-depth_limit
1298
- [formatters]: #plugin-formatters
1299
- [hash_access]: #serializing-hash-records
1300
- [metadata]: #plugin-metadata
1301
- [preloads]: #preloads
1302
- [presenter]: #presenter
1303
- [root]: #plugin-root
1304
- [string_modifiers]: #plugin-string_modifiers
1305
- [if]: #plugin-if
1306
- [build-badge]: https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push
1307
- [build]: https://github.com/aglushkov/serega/actions/workflows/main.yml
223
+ [build-badge]: https://github.com/andreyruby/serega/actions/workflows/main.yml/badge.svg?event=push
224
+ [build]: https://github.com/andreyruby/serega/actions/workflows/main.yml