serega 0.7.0 → 0.8.1
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 +4 -4
- data/README.md +650 -0
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +18 -7
- data/lib/serega/config.rb +25 -2
- data/lib/serega/helpers/serializer_class_helper.rb +1 -0
- data/lib/serega/map_point.rb +2 -0
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +2 -0
- data/lib/serega/plugins/batch/batch.rb +11 -3
- data/lib/serega/validations/attribute/check_opt_delegate.rb +6 -2
- data/lib/serega/validations/check_initiate_params.rb +1 -3
- data/lib/serega/validations/initiate/check_modifiers.rb +54 -37
- data/lib/serega.rb +3 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f1dd539e1a0b60a167cb67c5e8bfb509d5ce3be4d8ce8151a97a5f646f53378
|
4
|
+
data.tar.gz: 39ed8e53b95cc192a0819fbc4452d2cb6951b9208566d0597802cd98b828669e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b6519e8fb225d6bc132c5144b2268b8b7866e030a3c7a582febac6fc468a3cae449cf9298e05b5468c31c8a975629891e196ee0f1537cee401ceab394c573a2
|
7
|
+
data.tar.gz: bef92a58a8f730af6e97c3a36dbf6062aa7e3b99ad5c44484682447063018ec9dbcb345cc4715d1637a3d6c4c388f4b794a144b8250986db19efbe961b92c472
|
data/README.md
ADDED
@@ -0,0 +1,650 @@
|
|
1
|
+
[](https://badge.fury.io/rb/serega)
|
2
|
+
[](https://github.com/aglushkov/serega/actions/workflows/main.yml)
|
3
|
+
[](https://codeclimate.com/github/aglushkov/serega/test_coverage)
|
4
|
+
[](https://codeclimate.com/github/aglushkov/serega/maintainability)
|
5
|
+
|
6
|
+
# Serega Ruby Serializer
|
7
|
+
|
8
|
+
The Serega Ruby Serializer provides easy and powerful DSL to describe your objects and to serialize them to Hash or JSON.
|
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:
|
17
|
+
|
18
|
+
- Manually [select serialized fields](#selecting-fields)
|
19
|
+
- Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or [activerecord_preloads][activerecord_preloads] plugins)
|
20
|
+
- Built-in object presenter ([presenter][presenter] plugin)
|
21
|
+
- Adding custom metadata (via [metadata][metadata] or [context_metadata][context_metadata] plugins)
|
22
|
+
- Attributes formatters ([formatters][formatters] plugin)
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
`bundle add serega`
|
27
|
+
|
28
|
+
|
29
|
+
### Define serializers
|
30
|
+
|
31
|
+
Most apps should define **base serializer** with common plugins and settings to not repeat them in each serializer.
|
32
|
+
Children serializers will inherit everything (plugins, config, attributes) from parent.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class AppSerializer < Serega
|
36
|
+
# plugin :one
|
37
|
+
# plugin :two
|
38
|
+
|
39
|
+
# config.one = :one
|
40
|
+
# config.two = :two
|
41
|
+
end
|
42
|
+
|
43
|
+
class UserSerializer < AppSerializer
|
44
|
+
# attribute :one
|
45
|
+
# attribute :two
|
46
|
+
end
|
47
|
+
|
48
|
+
class CommentSerializer < AppSerializer
|
49
|
+
# attribute :one
|
50
|
+
# attribute :two
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### Adding attributes
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class UserSerializer < Serega
|
58
|
+
# Regular attribute
|
59
|
+
attribute :first_name
|
60
|
+
|
61
|
+
# Option :key specifies method in object
|
62
|
+
attribute :first_name, key: :old_first_name
|
63
|
+
|
64
|
+
# Block is used to define attribute value
|
65
|
+
attribute(:first_name) { |user| user.profile&.first_name }
|
66
|
+
|
67
|
+
# Option :value can be used with callable object to define attribute value
|
68
|
+
attribute :first_name, value: proc { |user| user.profile&.first_name }
|
69
|
+
|
70
|
+
# Option :delegate can be used to define attribute value. Sub-option :allow_nil by default is false
|
71
|
+
attribute :first_name, delegate: { to: :profile, allow_nil: true }
|
72
|
+
|
73
|
+
# Option :delegate can be used with :key sub-option to change method called on delegated object
|
74
|
+
attribute :first_name, delegate: { to: :profile, key: :fname }
|
75
|
+
|
76
|
+
# Option :const specifies attribute with specific constant value
|
77
|
+
attribute(:type, const: 'user')
|
78
|
+
|
79
|
+
# Option :hide specifies attributes that should not be serialized by default
|
80
|
+
attribute :tags, hide: true
|
81
|
+
|
82
|
+
# Option :serializer specifies nested serializer for attribute
|
83
|
+
# We can specify serializer as Class, String or Proc.
|
84
|
+
# Use String or Proc if you have cross references in serializers.
|
85
|
+
attribute :posts, serializer: PostSerializer
|
86
|
+
attribute :posts, serializer: "PostSerializer"
|
87
|
+
attribute :posts, serializer: -> { PostSerializer }
|
88
|
+
|
89
|
+
# Option `:many` specifies a has_many relationship
|
90
|
+
# Usually it is defined automatically by checking `is_a?(Enumerable)`
|
91
|
+
attribute :posts, serializer: PostSerializer, many: true
|
92
|
+
|
93
|
+
# Option `:preload` can be specified when enabled `:preloads` plugin
|
94
|
+
# It allows to specify associations to preload to attribute value
|
95
|
+
attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
|
96
|
+
|
97
|
+
# Option `:hide_nil` can be specified when enabled `:hide_nil` plugin
|
98
|
+
# It is literally hides attribute if its value is nil
|
99
|
+
attribute :email, hide_nil: true
|
100
|
+
|
101
|
+
# Option `:format` can be specified when enabled `:formatters` plugin
|
102
|
+
# It changes attribute value
|
103
|
+
attribute :created_at, format: :iso_time
|
104
|
+
attribute :updated_at, format: :iso_time
|
105
|
+
|
106
|
+
# Option `:format` also can be used as Proc
|
107
|
+
attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Serializing
|
112
|
+
|
113
|
+
We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and same instance methods `#to_h`, `#to_json`, `#as_json`.
|
114
|
+
`to_h` method is also aliased as `call`.
|
115
|
+
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
user = OpenStruct.new(username: 'serega')
|
119
|
+
|
120
|
+
class UserSerializer < Serega
|
121
|
+
attribute :username
|
122
|
+
end
|
123
|
+
|
124
|
+
UserSerializer.to_h(user) # => {username: "serega"}
|
125
|
+
UserSerializer.to_h([user]) # => [{username: "serega"}]
|
126
|
+
|
127
|
+
UserSerializer.to_json(user) # => '{"username":"serega"}'
|
128
|
+
UserSerializer.to_json([user]) # => '[{"username":"serega"}]'
|
129
|
+
|
130
|
+
UserSerializer.as_json(user) # => {"username":"serega"}
|
131
|
+
UserSerializer.as_json([user]) # => [{"username":"serega"}]
|
132
|
+
```
|
133
|
+
|
134
|
+
---
|
135
|
+
⚠️ When you serialize `Struct` object, specify manually `many: false`. As Struct is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should return array.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
UserSerializer.to_h(user_struct, many: false)
|
139
|
+
```
|
140
|
+
|
141
|
+
### Selecting Fields
|
142
|
+
|
143
|
+
By default all attributes are serialized (except marked as `hide: true`).
|
144
|
+
|
145
|
+
We can provide **modifiers** to select only needed attributes:
|
146
|
+
|
147
|
+
- *only* - lists attributes to serialize;
|
148
|
+
- *except* - lists attributes to not serialize;
|
149
|
+
- *with* - lists attributes to serialize additionally (By default all attributes are exposed and will be serialized, but some attributes can be hidden when they are defined with `hide: true` option, more on this below. `with` modifier can be used to expose such attributes).
|
150
|
+
|
151
|
+
Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
|
152
|
+
|
153
|
+
With plugin [string_modifiers][string_modifiers] we can provide modifiers as single `String` with attributes split by comma `,` and nested values inside brackets `()`, like: `username,enemies(username,email)`. This can be very useful to accept list of fields in **GET** requests.
|
154
|
+
|
155
|
+
When provided non-existing attribute, `Serega::AttributeNotExist` error will be raised. This error can be muted with `check_initiate_params: false` parameter.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class UserSerializer < Serega
|
159
|
+
plugin :string_modifiers # to send all modifiers in one string
|
160
|
+
|
161
|
+
attribute :username
|
162
|
+
attribute :first_name
|
163
|
+
attribute :last_name
|
164
|
+
attribute :email, hide: true
|
165
|
+
attribute :enemies, serializer: UserSerializer, hide: true
|
166
|
+
end
|
167
|
+
|
168
|
+
joker = OpenStruct.new(username: 'The Joker', first_name: 'jack', last_name: 'Oswald White', email: 'joker@mail.com', enemies: [])
|
169
|
+
bruce = OpenStruct.new(username: 'Batman', first_name: 'Bruce', last_name: 'Wayne', email: 'bruce@wayneenterprises.com', enemies: [])
|
170
|
+
joker.enemies << bruce
|
171
|
+
bruce.enemies << joker
|
172
|
+
|
173
|
+
# Default
|
174
|
+
UserSerializer.to_h(bruce) # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
|
175
|
+
|
176
|
+
# With `:only` modifier
|
177
|
+
# Next 3 lines are identical:
|
178
|
+
UserSerializer.to_h(bruce, only: [:username, { enemies: [:username, :email] }])
|
179
|
+
UserSerializer.new(only: [:username, { enemies: [:username, :email] }]).to_h(bruce)
|
180
|
+
UserSerializer.new(only: 'username,enemies(username,email)').to_h(bruce)
|
181
|
+
# => {:username=>"Batman", :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]}
|
182
|
+
|
183
|
+
# With `:except` modifier
|
184
|
+
# Next 3 lines are identical:
|
185
|
+
UserSerializer.new(except: %i[first_name last_name]).to_h(bruce)
|
186
|
+
UserSerializer.to_h(bruce, except: %i[first_name last_name])
|
187
|
+
UserSerializer.to_h(bruce, except: 'first_name,last_name')
|
188
|
+
# => {:username=>"Batman"}
|
189
|
+
|
190
|
+
# With `:with` modifier
|
191
|
+
# Next 3 lines are identical:
|
192
|
+
UserSerializer.new(with: %i[email enemies]).to_h(bruce)
|
193
|
+
UserSerializer.to_h(bruce, with: %i[email enemies])
|
194
|
+
UserSerializer.to_h(bruce, with: 'email,enemies')
|
195
|
+
# => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne", :email=>"bruce@wayneenterprises.com", :enemies=>[{:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}]}
|
196
|
+
|
197
|
+
# With not existing attribute
|
198
|
+
# Next 3 lines are identical:
|
199
|
+
UserSerializer.new(only: %i[first_name enemy]).to_h(bruce)
|
200
|
+
UserSerializer.to_h(bruce, only: %i[first_name enemy])
|
201
|
+
UserSerializer.to_h(bruce, only: 'first_name,enemy')
|
202
|
+
# => raises Serega::AttributeNotExist, "Attribute 'enemy' not exists"
|
203
|
+
|
204
|
+
# With not existing attribute and disabled validation
|
205
|
+
# Next 3 lines are identical:
|
206
|
+
UserSerializer.new(only: %i[first_name enemy], check_initiate_params: false).to_h(bruce)
|
207
|
+
UserSerializer.to_h(bruce, only: %i[first_name enemy], check_initiate_params: false)
|
208
|
+
UserSerializer.to_h(bruce, only: 'first_name,enemy', check_initiate_params: false)
|
209
|
+
# => {:first_name=>"Bruce"}
|
210
|
+
```
|
211
|
+
|
212
|
+
### Using Context
|
213
|
+
|
214
|
+
Sometimes you can decide to use some context during serialization, like current_user or any.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
class UserSerializer < Serega
|
218
|
+
attribute(:email) do |user, ctx|
|
219
|
+
user.email if ctx[:current_user] == user
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
user = OpenStruct.new(email: 'email@example.com')
|
224
|
+
UserSerializer.(user, context: {current_user: user}) # => {:email=>"email@example.com"}
|
225
|
+
UserSerializer.new.to_h(user, context: {current_user: user}) # same
|
226
|
+
```
|
227
|
+
|
228
|
+
## Configuration
|
229
|
+
|
230
|
+
This is initial config options, other config options can be added by plugins
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class AppSerializer < Serega
|
234
|
+
# Configure adapter to serialize to JSON.
|
235
|
+
# It is `JSON.dump` by default. When Oj gem is loaded then default is `Oj.dump(data, mode: :compat)`
|
236
|
+
config.to_json = ->(data) { Oj.dump(data, mode: :compat) }
|
237
|
+
|
238
|
+
# Configure adapter to de-serialize JSON. De-serialization is used only for `#as_json` method.
|
239
|
+
# It is `JSON.parse` by default. When Oj gem is loaded then default is `Oj.load(data)`
|
240
|
+
config.from_json = ->(data) { Oj.load(data) }
|
241
|
+
|
242
|
+
# Disable/enable validation of modifiers params `:with`, `:except`, `:only`
|
243
|
+
# By default it is enabled. After disabling, when provided not existed attribute it will be just skipped.
|
244
|
+
config.check_initiate_params = false # default is true, enabled
|
245
|
+
|
246
|
+
# Stores in memory prepared `maps` of serialized attributes during serialization.
|
247
|
+
# Next time serialization happens with same modifiers (`:only, :except, :with`), we will use already prepared `map`.
|
248
|
+
# Setting defines storage size (count of stored `maps` with different modifiers).
|
249
|
+
config.max_cached_map_per_serializer_count = 50 # default is 0, disabled
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
## Plugins
|
254
|
+
|
255
|
+
### Plugin :preloads
|
256
|
+
|
257
|
+
Allows to define `:preloads` to attributes and then allows to merge preloads
|
258
|
+
from serialized attributes and return single associations hash.
|
259
|
+
|
260
|
+
Plugin accepts options:
|
261
|
+
- `auto_preload_attributes_with_delegate` - default `false`
|
262
|
+
- `auto_preload_attributes_with_serializer` - default `false`
|
263
|
+
- `auto_hide_attributes_with_preload` - default `false`
|
264
|
+
|
265
|
+
This options are very handy if you want to forget about finding preloads manually.
|
266
|
+
|
267
|
+
Preloads can be disabled with `preload: false` attribute option option.
|
268
|
+
Also automatically added preloads can be overwritten with manually specified `preload: :another_value`.
|
269
|
+
|
270
|
+
Some examples, **please read comments in the code below**
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class AppSerializer < Serega
|
274
|
+
plugin :preloads,
|
275
|
+
auto_preload_attributes_with_delegate: true,
|
276
|
+
auto_preload_attributes_with_serializer: true,
|
277
|
+
auto_hide_attributes_with_preload: true
|
278
|
+
end
|
279
|
+
|
280
|
+
class UserSerializer < AppSerializer
|
281
|
+
# No preloads
|
282
|
+
attribute :username
|
283
|
+
|
284
|
+
# Specify `preload: :user_stats` manually
|
285
|
+
attribute :followers_count, preload: :user_stats, value: proc { |user| user.user_stats.followers_count }
|
286
|
+
|
287
|
+
# Automatically `preloads: :user_stats` as `auto_preload_attributes_with_delegate` option is true
|
288
|
+
attribute :comments_count, delegate: { to: :user_stats }
|
289
|
+
|
290
|
+
# Automatically `preloads: :albums` as `auto_preload_attributes_with_serializer` option is true
|
291
|
+
attribute :albums, serializer: 'AlbumSerializer'
|
292
|
+
end
|
293
|
+
|
294
|
+
class AlbumSerializer < AppSerializer
|
295
|
+
attribute :images_count, delegate: { to: :album_stats }
|
296
|
+
end
|
297
|
+
|
298
|
+
# By default preloads are empty, as we specify `auto_hide_attributes_with_preload = true`,
|
299
|
+
# and attributes with preloads will be not serialized
|
300
|
+
UserSerializer.new.preloads # => {}
|
301
|
+
UserSerializer.new.to_h(OpenStruct.new(username: 'foo')) # => {:username=>"foo"}
|
302
|
+
|
303
|
+
UserSerializer.new(with: :followers_count).preloads # => {:user_stats=>{}}
|
304
|
+
UserSerializer.new(with: %i[followers_count comments_count]).preloads # => {:user_stats=>{}}
|
305
|
+
UserSerializer.new(with: [:followers_count, :comments_count, { albums: :images_count }]).preloads # => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
|
306
|
+
```
|
307
|
+
|
308
|
+
---
|
309
|
+
One tricky case, that you will probably never see in real life:
|
310
|
+
|
311
|
+
Manually you can preload multiple associations, like this:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
attribute :image, serializer: ImageSerializer, preload: { attachment: :blob }, value: proc { |record| record.attachment }
|
315
|
+
```
|
316
|
+
|
317
|
+
In this case we mark last element (in this case it will be `blob`) as main,
|
318
|
+
so nested associations, if any, will be preloaded to this `blob`.
|
319
|
+
If you need to preload them to `attachment`,
|
320
|
+
please specify additionally `:preload_path` option like this:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
attribute :image, serializer: ImageSerializer, preload: { attachment: :blob }, preload_path: %i[attachment], value: proc { |record| record.attachment }
|
324
|
+
```
|
325
|
+
---
|
326
|
+
|
327
|
+
📌 Plugin `:preloads` only allows to group preloads together in single Hash, but they should be preloaded manually. For now there are only [activerecord_preloads][activerecord_preloads] plugin that can automatically preload associations.
|
328
|
+
|
329
|
+
|
330
|
+
### Plugin :activerecord_preloads
|
331
|
+
|
332
|
+
(depends on [preloads][preloads] plugin, that must be loaded first)
|
333
|
+
|
334
|
+
Automatically preloads associations to serialized objects.
|
335
|
+
|
336
|
+
It takes all defined preloads from serialized attributes (including attributes from serialized relations),
|
337
|
+
merges them into single associations hash and then uses ActiveRecord::Associations::Preloader
|
338
|
+
to preload associations to serialized objects.
|
339
|
+
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
class AppSerializer < Serega
|
343
|
+
plugin :preloads,
|
344
|
+
auto_preload_attributes_with_delegate: true,
|
345
|
+
auto_preload_attributes_with_serializer: true,
|
346
|
+
auto_hide_attributes_with_preload: false
|
347
|
+
|
348
|
+
plugin :activerecord_preloads
|
349
|
+
end
|
350
|
+
|
351
|
+
class UserSerializer < AppSerializer
|
352
|
+
attribute :username
|
353
|
+
attribute :comments_count, delegate: { to: :user_stats }
|
354
|
+
attribute :albums, serializer: AlbumSerializer
|
355
|
+
end
|
356
|
+
|
357
|
+
class AlbumSerializer < AppSerializer
|
358
|
+
attribute :title
|
359
|
+
attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
|
360
|
+
end
|
361
|
+
|
362
|
+
UserSerializer.to_h(user) # => preloads {users_stats: {}, albums: { downloads: {} }}
|
363
|
+
```
|
364
|
+
|
365
|
+
### Plugin :batch
|
366
|
+
|
367
|
+
Adds ability to load attributes values in batches.
|
368
|
+
|
369
|
+
It can be used to omit N+1, to calculate counters for different objects in single query, to request any data from external storage.
|
370
|
+
|
371
|
+
Added new `:batch` attribute option, example:
|
372
|
+
```
|
373
|
+
attribute :name, batch: { key: :id, loader: :name_loader, default: '' }
|
374
|
+
```
|
375
|
+
|
376
|
+
Attribute `:batch` option must be a hash with this keys:
|
377
|
+
- `key` (required) [Symbol, Proc, callable] - Defines identifier of current object
|
378
|
+
- `loader` (required) [Symbol, Proc, callable] - Defines how to fetch values for batch of keys. Accepts 3 parameters: keys, context, point.
|
379
|
+
- `default` (optional) - Default value used when loader does not return value for current key. By default it is `nil` or `[]` when attribute has additional option `many: true` (ex: `attribute :name, many: true, batch: { ... }`).
|
380
|
+
|
381
|
+
If `:loader` was defined using name (as Symbol) then batch loader must be defined using `config.batch_loaders.define(:loader_name) { ... }` method. Result of this block must be a Hash where keys are - provided keys, and values are - batch loaded values for according keys.
|
382
|
+
|
383
|
+
Batch loader works well with [`activerecord_preloads`][activerecord_preloads] plugin.
|
384
|
+
|
385
|
+
```ruby
|
386
|
+
class PostSerializer < Serega
|
387
|
+
plugin :batch
|
388
|
+
|
389
|
+
# Define batch loader via callable class, it must accept three args (keys, context, nested_attributes)
|
390
|
+
attribute :comments_count, batch: { key: :id, loader: PostCommentsCountBatchLoader, default: 0}
|
391
|
+
|
392
|
+
# Define batch loader via Symbol, later we should define this loader via config.batch_loaders.define(:posts_comments_counter) { ... }
|
393
|
+
attribute :comments_count, batch: { key: :id, loader: :posts_comments_counter, default: 0}
|
394
|
+
|
395
|
+
# Define batch loader with serializer
|
396
|
+
attribute :comments, serializer: CommentSerializer, batch: { key: :id, loader: :posts_comments, default: []}
|
397
|
+
|
398
|
+
# Resulted block must return hash like { key => value(s) }
|
399
|
+
config.batch_loaders.define(:posts_comments_counter) do |keys|
|
400
|
+
Comment.group(:post_id).where(post_id: keys).count
|
401
|
+
end
|
402
|
+
|
403
|
+
# We can return objects that will be automatically serialized if attribute defined with :serializer
|
404
|
+
# Parameter `context` can be used when loading batch
|
405
|
+
# Parameter `point` can be used to find nested attributes that will be serialized
|
406
|
+
config.batch_loaders.define(:posts_comments) do |keys, context, point|
|
407
|
+
# point.nested_points - if you need to manually check all nested attributes that will be serialized
|
408
|
+
# point.preloads - if you need to find nested preloads (works with :preloads plugin only)
|
409
|
+
|
410
|
+
Comment
|
411
|
+
.preload(point.preloads) # Can be skipped when used :activerecord_preloads plugin
|
412
|
+
.where(post_id: keys)
|
413
|
+
.where(is_spam: false)
|
414
|
+
.group_by(&:post_id)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
```
|
418
|
+
|
419
|
+
### Plugin :root
|
420
|
+
|
421
|
+
Allows to add root key to your serialized data
|
422
|
+
|
423
|
+
Accepts options:
|
424
|
+
- :root - specifies root for all responses
|
425
|
+
- :root_one - specifies root for single object serialization only
|
426
|
+
- :root_many - specifies root for multiple objects serialization only
|
427
|
+
|
428
|
+
Adds additional config options:
|
429
|
+
- config.root.one
|
430
|
+
- config.root.many
|
431
|
+
- config.root.one=
|
432
|
+
- config.root_many=
|
433
|
+
|
434
|
+
Default root is `:data`.
|
435
|
+
|
436
|
+
Root also can be changed per serialization.
|
437
|
+
|
438
|
+
Also root can be removed for all responses by providing `root: nil`. In this case no root will be added to response, but
|
439
|
+
you still can to add it per serialization
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
#@example Define plugin
|
443
|
+
|
444
|
+
class UserSerializer < Serega
|
445
|
+
plugin :root # default root is :data
|
446
|
+
end
|
447
|
+
|
448
|
+
class UserSerializer < Serega
|
449
|
+
plugin :root, root: :users
|
450
|
+
end
|
451
|
+
|
452
|
+
class UserSerializer < Serega
|
453
|
+
plugin :root, root_one: :user, root_many: :people
|
454
|
+
end
|
455
|
+
|
456
|
+
class UserSerializer < Serega
|
457
|
+
plugin :root, root: nil # no root by default
|
458
|
+
end
|
459
|
+
```
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
# @example Change root per serialization:
|
463
|
+
|
464
|
+
class UserSerializer < Serega
|
465
|
+
plugin :root
|
466
|
+
end
|
467
|
+
|
468
|
+
UserSerializer.to_h(nil) # => {:data=>nil}
|
469
|
+
UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
|
470
|
+
UserSerializer.to_h(nil, root: nil) # => nil
|
471
|
+
```
|
472
|
+
|
473
|
+
### Plugin :metadata
|
474
|
+
|
475
|
+
Depends on: [`:root`][root] plugin, that must be loaded first
|
476
|
+
|
477
|
+
Adds ability to describe metadata and adds it to serialized response
|
478
|
+
|
479
|
+
Added class-level method `:meta_attribute`, to define metadata, it accepts:
|
480
|
+
- *path [Array<Symbol>] - nested hash keys beginning from the root object.
|
481
|
+
- **options [Hash] - defaults are `hide_nil: false, hide_empty: false`
|
482
|
+
- &block [Proc] - describes value for current meta attribute
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
class AppSerializer < Serega
|
486
|
+
plugin :root
|
487
|
+
plugin :metadata
|
488
|
+
|
489
|
+
meta_attribute(:version) { '1.2.3' }
|
490
|
+
meta_attribute(:ab_tests, :names) { %i[foo bar] }
|
491
|
+
meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
|
492
|
+
next unless records.respond_to?(:total_count)
|
493
|
+
|
494
|
+
{ page: records.page, per_page: records.per_page, total_count: records.total_count }
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
AppSerializer.to_h(nil) # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}}
|
499
|
+
```
|
500
|
+
|
501
|
+
### Plugin :context_metadata
|
502
|
+
|
503
|
+
Depends on: [`:root`][root] plugin, that must be loaded first
|
504
|
+
|
505
|
+
Allows to provide metadata and attach it to serialized response.
|
506
|
+
|
507
|
+
Accepts option `:context_metadata_key` with name of keyword that must be used to provide metadata. By default it is `:meta`
|
508
|
+
|
509
|
+
Key can be changed in children serializers using config `config.context_metadata.key=(value)`
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
class UserSerializer < Serega
|
513
|
+
plugin :root, root: :data
|
514
|
+
plugin :context_metadata, context_metadata_key: :meta
|
515
|
+
|
516
|
+
# Same:
|
517
|
+
# plugin :context_metadata
|
518
|
+
# config.context_metadata.key = :meta
|
519
|
+
end
|
520
|
+
|
521
|
+
UserSerializer.to_h(nil, meta: { version: '1.0.1' })
|
522
|
+
# => {:data=>nil, :version=>"1.0.1"}
|
523
|
+
```
|
524
|
+
|
525
|
+
### Plugin :formatters
|
526
|
+
|
527
|
+
Allows to define `formatters` and apply them on attributes.
|
528
|
+
|
529
|
+
Config option `config.formatters.add` can be used to add formatters.
|
530
|
+
|
531
|
+
Attribute option `:format` now can be used with name of formatter or with callable instance.
|
532
|
+
|
533
|
+
```ruby
|
534
|
+
class AppSerializer < Serega
|
535
|
+
plugin :formatters, formatters: {
|
536
|
+
iso8601: ->(value) { time.iso8601.round(6) },
|
537
|
+
on_off: ->(value) { value ? 'ON' : 'OFF' },
|
538
|
+
money: ->(value) { value.round(2) }
|
539
|
+
}
|
540
|
+
end
|
541
|
+
|
542
|
+
class UserSerializer < Serega
|
543
|
+
# Additionally we can add formatters via config in subclasses
|
544
|
+
config.formatters.add(
|
545
|
+
iso8601: ->(value) { time.iso8601.round(6) },
|
546
|
+
on_off: ->(value) { value ? 'ON' : 'OFF' },
|
547
|
+
money: ->(value) { value.round(2) }
|
548
|
+
)
|
549
|
+
|
550
|
+
# Using predefined formatter
|
551
|
+
attribute :commission, format: :money
|
552
|
+
attribute :is_logined, format: :on_off
|
553
|
+
attribute :created_at, format: :iso8601
|
554
|
+
attribute :updated_at, format: :iso8601
|
555
|
+
|
556
|
+
# Using `callable` formatter
|
557
|
+
attribute :score_percent, format: PercentFormmatter # callable class
|
558
|
+
attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
|
559
|
+
end
|
560
|
+
```
|
561
|
+
|
562
|
+
### Plugin :presenter
|
563
|
+
|
564
|
+
Helps to write clear code by adding attribute names as methods to Presenter
|
565
|
+
|
566
|
+
```ruby
|
567
|
+
class UserSerializer < Serega
|
568
|
+
plugin :presenter
|
569
|
+
|
570
|
+
attribute :name
|
571
|
+
attribute :address
|
572
|
+
|
573
|
+
class Presenter
|
574
|
+
def name
|
575
|
+
[first_name, last_name].compact_blank.join(' ')
|
576
|
+
end
|
577
|
+
|
578
|
+
def address
|
579
|
+
[country, city, address].join("\n")
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
```
|
584
|
+
|
585
|
+
### Plugin :string_modifiers
|
586
|
+
|
587
|
+
Allows to specify modifiers as strings.
|
588
|
+
|
589
|
+
Serialized attributes must be split with `,` and nested attributes must be defined inside brackets `(`, `)`.
|
590
|
+
|
591
|
+
Modifiers can still be provided old way with nested hashes or arrays.
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
PostSerializer.plugin :string_modifiers
|
595
|
+
PostSerializer.new(only: "id,user(id,username)").to_h(post)
|
596
|
+
PostSerializer.new(except: "user(username,email)").to_h(post)
|
597
|
+
PostSerializer.new(with: "user(email)").to_h(post)
|
598
|
+
|
599
|
+
# Modifiers can still be provided old way with nested hashes or arrays.
|
600
|
+
PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
|
601
|
+
```
|
602
|
+
|
603
|
+
### Plugin :hide_nil
|
604
|
+
|
605
|
+
Allows to hide attributes with `nil` values
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
class UserSerializer < Serega
|
609
|
+
plugin :hide_nil
|
610
|
+
|
611
|
+
attribute :email, hide_nil: true
|
612
|
+
end
|
613
|
+
```
|
614
|
+
|
615
|
+
## Errors
|
616
|
+
|
617
|
+
- `Serega::SeregaError` is a base error raised by this gem.
|
618
|
+
- `Serega::AttributeNotExist` error is raised when validating attributes in `:only, :except, :with` modifiers
|
619
|
+
|
620
|
+
## Release
|
621
|
+
|
622
|
+
To release a new version, read [RELEASE.md](https://github.com/aglushkov/serega/blob/master/RELEASE.md).
|
623
|
+
|
624
|
+
## Development
|
625
|
+
|
626
|
+
- `bundle install` - install dependencies
|
627
|
+
- `bin/console` - open irb console with loaded gems
|
628
|
+
- `bundle exec rspec` - run tests
|
629
|
+
- `bundle exec rubocop` - check code standards
|
630
|
+
- `yard stats --list-undoc --no-cache` - view undocumented code
|
631
|
+
- `yard server --reload` - view code documentation
|
632
|
+
|
633
|
+
## Contributing
|
634
|
+
|
635
|
+
Bug reports, pull requests and improvements ideas are very welcome!
|
636
|
+
|
637
|
+
## License
|
638
|
+
|
639
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
640
|
+
|
641
|
+
|
642
|
+
[activerecord_preloads]: #plugin-activerecord_preloads
|
643
|
+
[batch]: #plugin-batch
|
644
|
+
[context_metadata]: #plugin-context_metadata
|
645
|
+
[formatters]: #plugin-formatters
|
646
|
+
[metadata]: #plugin-metadata
|
647
|
+
[preloads]: #plugin-preloads
|
648
|
+
[presenter]: #plugin-presenter
|
649
|
+
[root]: #plugin-root
|
650
|
+
[string_modifiers]: #plugin-string_modifiers
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.1
|
data/lib/serega/attribute.rb
CHANGED
@@ -9,12 +9,15 @@ class Serega
|
|
9
9
|
# Attribute instance methods
|
10
10
|
#
|
11
11
|
module AttributeInstanceMethods
|
12
|
+
# Returns attribute name
|
12
13
|
# @return [Symbol] Attribute name
|
13
14
|
attr_reader :name
|
14
15
|
|
16
|
+
# Returns attribute options
|
15
17
|
# @return [Hash] Attribute options
|
16
18
|
attr_reader :opts
|
17
19
|
|
20
|
+
# Returns attribute block
|
18
21
|
# @return [Proc] Attribute originally added block
|
19
22
|
attr_reader :block
|
20
23
|
|
@@ -40,26 +43,32 @@ class Serega
|
|
40
43
|
@block = block
|
41
44
|
end
|
42
45
|
|
43
|
-
#
|
46
|
+
# Method name that will be used to get attribute value
|
47
|
+
#
|
48
|
+
# @return [Symbol] key
|
44
49
|
def key
|
45
50
|
@key ||= opts.key?(:key) ? opts[:key].to_sym : name
|
46
51
|
end
|
47
52
|
|
48
|
-
#
|
53
|
+
# Shows current opts[:hide] option
|
54
|
+
# @return [Boolean, nil] Attribute :hide option value
|
49
55
|
def hide
|
50
56
|
opts[:hide]
|
51
57
|
end
|
52
58
|
|
53
|
-
#
|
59
|
+
# Shows current opts[:many] option
|
60
|
+
# @return [Boolean, nil] Attribute :many option value
|
54
61
|
def many
|
55
62
|
opts[:many]
|
56
63
|
end
|
57
64
|
|
65
|
+
# Shows whether attribute has specified serializer
|
58
66
|
# @return [Boolean] Checks if attribute is relationship (if :serializer option exists)
|
59
67
|
def relation?
|
60
68
|
!opts[:serializer].nil?
|
61
69
|
end
|
62
70
|
|
71
|
+
# Shows specified serializer class
|
63
72
|
# @return [Serega, nil] Attribute serializer if exists
|
64
73
|
def serializer
|
65
74
|
return @serializer if instance_variable_defined?(:@serializer)
|
@@ -73,6 +82,7 @@ class Serega
|
|
73
82
|
end
|
74
83
|
end
|
75
84
|
|
85
|
+
# Returns final block that will be used to find attribute value
|
76
86
|
# @return [Proc] Proc to find attribute value
|
77
87
|
def value_block
|
78
88
|
return @value_block if instance_variable_defined?(:@value_block)
|
@@ -130,12 +140,13 @@ class Serega
|
|
130
140
|
end
|
131
141
|
|
132
142
|
def delegate_block
|
133
|
-
|
143
|
+
delegate = opts[:delegate]
|
144
|
+
return unless delegate
|
134
145
|
|
135
|
-
key_method_name = key
|
136
|
-
delegate_to =
|
146
|
+
key_method_name = delegate[:key] || key
|
147
|
+
delegate_to = delegate[:to]
|
137
148
|
|
138
|
-
if
|
149
|
+
if delegate[:allow_nil]
|
139
150
|
proc { |object| object.public_send(delegate_to)&.public_send(key_method_name) }
|
140
151
|
else
|
141
152
|
proc { |object| object.public_send(delegate_to).public_send(key_method_name) }
|
data/lib/serega/config.rb
CHANGED
@@ -8,6 +8,10 @@ class Serega
|
|
8
8
|
#
|
9
9
|
class SeregaConfig
|
10
10
|
# :nocov: We can't use both :oj and :json adapters together
|
11
|
+
|
12
|
+
#
|
13
|
+
# Default config options
|
14
|
+
#
|
11
15
|
DEFAULTS = {
|
12
16
|
plugins: [],
|
13
17
|
initiate_keys: %i[only with except check_initiate_params].freeze,
|
@@ -23,7 +27,9 @@ class Serega
|
|
23
27
|
# SeregaConfig Instance methods
|
24
28
|
module SeregaConfigInstanceMethods
|
25
29
|
#
|
26
|
-
#
|
30
|
+
# Shows current config as Hash
|
31
|
+
#
|
32
|
+
# @return [Hash] config options
|
27
33
|
#
|
28
34
|
attr_reader :opts
|
29
35
|
|
@@ -37,31 +43,41 @@ class Serega
|
|
37
43
|
@opts = SeregaUtils::EnumDeepDup.call(opts)
|
38
44
|
end
|
39
45
|
|
46
|
+
#
|
47
|
+
# Shows used plugins
|
48
|
+
#
|
40
49
|
# @return [Array] Used plugins
|
50
|
+
#
|
41
51
|
def plugins
|
42
52
|
opts.fetch(:plugins)
|
43
53
|
end
|
44
54
|
|
45
|
-
#
|
55
|
+
# Returns options names allowed in `Serega#new` method
|
56
|
+
# @return [Array<Symbol>] allowed options keys
|
46
57
|
def initiate_keys
|
47
58
|
opts.fetch(:initiate_keys)
|
48
59
|
end
|
49
60
|
|
61
|
+
# Returns options names allowed in `Serega.attribute` method
|
50
62
|
# @return [Array<Symbol>] Allowed options keys for attribute initialization
|
51
63
|
def attribute_keys
|
52
64
|
opts.fetch(:attribute_keys)
|
53
65
|
end
|
54
66
|
|
67
|
+
# Returns options names allowed in `to_h, to_json, as_json` methods
|
55
68
|
# @return [Array<Symbol>] Allowed options keys for serialization
|
56
69
|
def serialize_keys
|
57
70
|
opts.fetch(:serialize_keys)
|
58
71
|
end
|
59
72
|
|
73
|
+
# Returns :check_initiate_params config option
|
60
74
|
# @return [Boolean] Current :check_initiate_params config option
|
61
75
|
def check_initiate_params
|
62
76
|
opts.fetch(:check_initiate_params)
|
63
77
|
end
|
64
78
|
|
79
|
+
# Sets :check_initiate_params config option
|
80
|
+
#
|
65
81
|
# @param value [Boolean] Set :check_initiate_params config option
|
66
82
|
#
|
67
83
|
# @return [Boolean] :check_initiate_params config option
|
@@ -70,11 +86,14 @@ class Serega
|
|
70
86
|
opts[:check_initiate_params] = value
|
71
87
|
end
|
72
88
|
|
89
|
+
# Returns :max_cached_map_per_serializer_count config option
|
73
90
|
# @return [Boolean] Current :max_cached_map_per_serializer_count config option
|
74
91
|
def max_cached_map_per_serializer_count
|
75
92
|
opts.fetch(:max_cached_map_per_serializer_count)
|
76
93
|
end
|
77
94
|
|
95
|
+
# Sets :max_cached_map_per_serializer_count config option
|
96
|
+
#
|
78
97
|
# @param value [Boolean] Set :check_initiate_params config option
|
79
98
|
#
|
80
99
|
# @return [Boolean] New :max_cached_map_per_serializer_count config option
|
@@ -83,22 +102,26 @@ class Serega
|
|
83
102
|
opts[:max_cached_map_per_serializer_count] = value
|
84
103
|
end
|
85
104
|
|
105
|
+
# Returns current `to_json` adapter
|
86
106
|
# @return [#call] Callable that used to construct JSON
|
87
107
|
def to_json
|
88
108
|
opts.fetch(:to_json)
|
89
109
|
end
|
90
110
|
|
111
|
+
# Sets current `to_json` adapter
|
91
112
|
# @param value [#call] Callable that used to construct JSON
|
92
113
|
# @return [#call] Provided callable object
|
93
114
|
def to_json=(value)
|
94
115
|
opts[:to_json] = value
|
95
116
|
end
|
96
117
|
|
118
|
+
# Returns current `from_json` adapter
|
97
119
|
# @return [#call] Callable that used to parse JSON
|
98
120
|
def from_json
|
99
121
|
opts.fetch(:from_json)
|
100
122
|
end
|
101
123
|
|
124
|
+
# Sets current `from_json` adapter
|
102
125
|
# @param value [#call] Callable that used to parse JSON
|
103
126
|
# @return [#call] Provided callable object
|
104
127
|
def from_json=(value)
|
@@ -9,6 +9,7 @@ class Serega
|
|
9
9
|
# Stores link to current serializer class
|
10
10
|
#
|
11
11
|
module SerializerClassHelper
|
12
|
+
# Shows serializer class current class is namespaced under
|
12
13
|
# @return [Class<Serega>] Serializer class that current class is namespaced under.
|
13
14
|
attr_accessor :serializer_class
|
14
15
|
end
|
data/lib/serega/map_point.rb
CHANGED
@@ -11,9 +11,11 @@ class Serega
|
|
11
11
|
module InstanceMethods
|
12
12
|
extend Forwardable
|
13
13
|
|
14
|
+
# Shows current attribute
|
14
15
|
# @return [Serega::SeregaAttribute] Current attribute
|
15
16
|
attr_reader :attribute
|
16
17
|
|
18
|
+
# Shows nested points
|
17
19
|
# @return [NilClass, Array<Serega::SeregaMapPoint>] Nested points or nil
|
18
20
|
attr_reader :nested_points
|
19
21
|
|
@@ -22,6 +22,8 @@ class Serega
|
|
22
22
|
preload_handler.preload(object, preloads)
|
23
23
|
end
|
24
24
|
|
25
|
+
# Returns handlers which will try to check if serialized object fits for preloading using this handler
|
26
|
+
#
|
25
27
|
# @return [Array] Registered preload adapters for different types of initial records
|
26
28
|
def handlers
|
27
29
|
@handlers ||= [
|
@@ -35,6 +35,7 @@ class Serega
|
|
35
35
|
# end
|
36
36
|
#
|
37
37
|
module Batch
|
38
|
+
# Returns plugin name
|
38
39
|
# @return [Symbol] Plugin name
|
39
40
|
def self.plugin_name
|
40
41
|
:batch
|
@@ -159,6 +160,7 @@ class Serega
|
|
159
160
|
@many = many
|
160
161
|
end
|
161
162
|
|
163
|
+
# Returns proc that will be used to batch load registered keys values
|
162
164
|
# @return [#call] batch loader
|
163
165
|
def loader
|
164
166
|
@batch_loader ||= begin
|
@@ -168,6 +170,7 @@ class Serega
|
|
168
170
|
end
|
169
171
|
end
|
170
172
|
|
173
|
+
# Returns proc that will be used to find batch_key for current attribute.
|
171
174
|
# @return [Object] key (uid) of batch loaded object
|
172
175
|
def key
|
173
176
|
@batch_key ||= begin
|
@@ -176,6 +179,7 @@ class Serega
|
|
176
179
|
end
|
177
180
|
end
|
178
181
|
|
182
|
+
# Returns default value to use if batch loader does not return value for some key
|
179
183
|
# @return [Object] default value for missing key
|
180
184
|
def default_value
|
181
185
|
if opts.key?(:default)
|
@@ -192,6 +196,8 @@ class Serega
|
|
192
196
|
# @see Serega::SeregaConfig
|
193
197
|
#
|
194
198
|
module ConfigInstanceMethods
|
199
|
+
#
|
200
|
+
# Returns all batch loaders registered for current serializer
|
195
201
|
#
|
196
202
|
# @return [Serega::SeregaPlugins::Batch::BatchLoadersConfig] configuration for batch loaders
|
197
203
|
#
|
@@ -253,11 +259,13 @@ class Serega
|
|
253
259
|
#
|
254
260
|
# Serega::SeregaMapPoint additional/patched class methods
|
255
261
|
#
|
256
|
-
# @see
|
262
|
+
# @see SeregaAttribute
|
257
263
|
#
|
258
264
|
module MapPointInstanceMethods
|
259
265
|
#
|
260
|
-
#
|
266
|
+
# Returns BatchModel, an object that encapsulates all batch_loader methods for current point
|
267
|
+
#
|
268
|
+
# @return [BatchModel] batch model that encapsulates everything needed to load current batch
|
261
269
|
#
|
262
270
|
def batch
|
263
271
|
return @batch if instance_variable_defined?(:@batch)
|
@@ -272,7 +280,7 @@ class Serega
|
|
272
280
|
#
|
273
281
|
# Serega additional/patched instance methods
|
274
282
|
#
|
275
|
-
# @see Serega
|
283
|
+
# @see Serega::InstanceMethods
|
276
284
|
#
|
277
285
|
module InstanceMethods
|
278
286
|
private
|
@@ -32,6 +32,7 @@ class Serega
|
|
32
32
|
|
33
33
|
delegate_opts = opts[:delegate]
|
34
34
|
check_opt_delegate_to(delegate_opts)
|
35
|
+
check_opt_delegate_key(delegate_opts)
|
35
36
|
check_opt_delegate_allow_nil(delegate_opts)
|
36
37
|
end
|
37
38
|
|
@@ -42,13 +43,16 @@ class Serega
|
|
42
43
|
Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :to)
|
43
44
|
end
|
44
45
|
|
45
|
-
def
|
46
|
-
|
46
|
+
def check_opt_delegate_key(delegate_opts)
|
47
|
+
Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :key)
|
48
|
+
end
|
47
49
|
|
50
|
+
def check_opt_delegate_allow_nil(delegate_opts)
|
48
51
|
Utils::CheckOptIsBool.call(delegate_opts, :allow_nil)
|
49
52
|
end
|
50
53
|
|
51
54
|
def check_usage_with_other_params(opts, block)
|
55
|
+
raise SeregaError, "Option :delegate can not be used together with option :key" if opts.key?(:key)
|
52
56
|
raise SeregaError, "Option :delegate can not be used together with option :const" if opts.key?(:const)
|
53
57
|
raise SeregaError, "Option :delegate can not be used together with option :value" if opts.key?(:value)
|
54
58
|
raise SeregaError, "Option :delegate can not be used together with block" if block
|
@@ -39,9 +39,7 @@ class Serega
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def check_modifiers
|
42
|
-
Initiate::CheckModifiers.call(serializer_class, opts[:only])
|
43
|
-
Initiate::CheckModifiers.call(serializer_class, opts[:except])
|
44
|
-
Initiate::CheckModifiers.call(serializer_class, opts[:with])
|
42
|
+
Initiate::CheckModifiers.new.call(serializer_class, opts[:only], opts[:with], opts[:except])
|
45
43
|
end
|
46
44
|
|
47
45
|
def serializer_class
|
@@ -10,55 +10,72 @@ class Serega
|
|
10
10
|
# Modifiers validation
|
11
11
|
#
|
12
12
|
class CheckModifiers
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
#
|
14
|
+
# Validates provided fields names are existing attributes
|
15
|
+
#
|
16
|
+
# @param serializer_class [Serega]
|
17
|
+
# @param only [Hash, nil] `only` modifier
|
18
|
+
# @param with [Hash, nil] `with` modifier
|
19
|
+
# @param except [Hash, nil] `except` modifier
|
20
|
+
#
|
21
|
+
# @raise [Serega::AttributeNotExist] when some checked modifier has not existing attribute
|
22
|
+
#
|
23
|
+
# @return [void]
|
24
|
+
#
|
25
|
+
def call(serializer_class, only, with, except)
|
26
|
+
validate(serializer_class, only) if only
|
27
|
+
validate(serializer_class, with) if with
|
28
|
+
validate(serializer_class, except) if except
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
raise_errors if any_error?
|
31
|
+
end
|
28
32
|
|
29
|
-
|
33
|
+
private
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
35
|
+
def validate(serializer_class, fields)
|
36
|
+
fields.each do |name, nested_fields|
|
37
|
+
attribute = serializer_class && serializer_class.attributes[name]
|
38
|
+
|
39
|
+
# Save error when no attribute with checked name exists
|
40
|
+
unless attribute
|
41
|
+
save_error(name)
|
42
|
+
next
|
43
|
+
end
|
34
44
|
|
35
|
-
|
36
|
-
|
45
|
+
# Return when attribute has no nested fields
|
46
|
+
next if nested_fields.empty?
|
37
47
|
|
38
|
-
|
39
|
-
|
40
|
-
validate(nested_serializer, nested_fields, prev_names + [name])
|
48
|
+
with_parent_name(name) do
|
49
|
+
validate(attribute.serializer, nested_fields)
|
41
50
|
end
|
42
51
|
end
|
52
|
+
end
|
43
53
|
|
44
|
-
|
45
|
-
|
54
|
+
def parents_names
|
55
|
+
@parents_names ||= []
|
56
|
+
end
|
46
57
|
|
47
|
-
|
48
|
-
|
58
|
+
def with_parent_name(name)
|
59
|
+
parents_names << name
|
60
|
+
yield
|
61
|
+
parents_names.pop
|
62
|
+
end
|
49
63
|
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
def error_attributes
|
65
|
+
@error_attributes ||= []
|
66
|
+
end
|
53
67
|
|
54
|
-
|
55
|
-
|
68
|
+
def save_error(name)
|
69
|
+
full_attribute_name = [*parents_names, name].join(".")
|
70
|
+
error_attributes << full_attribute_name
|
71
|
+
end
|
56
72
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
73
|
+
def raise_errors
|
74
|
+
raise Serega::AttributeNotExist, "Not existing attributes: #{error_attributes.join(", ")}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def any_error?
|
78
|
+
defined?(@error_attributes)
|
62
79
|
end
|
63
80
|
end
|
64
81
|
end
|
data/lib/serega.rb
CHANGED
@@ -4,9 +4,11 @@ require_relative "serega/version"
|
|
4
4
|
|
5
5
|
# Parent class for your serializers
|
6
6
|
class Serega
|
7
|
+
# Frozen hash
|
7
8
|
# @return [Hash] frozen hash
|
8
9
|
FROZEN_EMPTY_HASH = {}.freeze
|
9
10
|
|
11
|
+
# Frozen array
|
10
12
|
# @return [Array] frozen array
|
11
13
|
FROZEN_EMPTY_ARRAY = [].freeze
|
12
14
|
end
|
@@ -64,6 +66,7 @@ class Serega
|
|
64
66
|
# Serializers class methods
|
65
67
|
#
|
66
68
|
module ClassMethods
|
69
|
+
# Returns current config
|
67
70
|
# @return [SeregaConfig] current serializer config
|
68
71
|
attr_reader :config
|
69
72
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serega
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrey Glushkov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
JSON Serializer
|
@@ -25,6 +25,7 @@ executables: []
|
|
25
25
|
extensions: []
|
26
26
|
extra_rdoc_files: []
|
27
27
|
files:
|
28
|
+
- README.md
|
28
29
|
- VERSION
|
29
30
|
- lib/serega.rb
|
30
31
|
- lib/serega/attribute.rb
|
@@ -92,8 +93,8 @@ homepage: https://github.com/aglushkov/serega
|
|
92
93
|
licenses:
|
93
94
|
- MIT
|
94
95
|
metadata:
|
95
|
-
homepage_uri: https://github.com/aglushkov/serega
|
96
96
|
source_code_uri: https://github.com/aglushkov/serega
|
97
|
+
documentation_uri: https://www.rubydoc.info/gems/serega
|
97
98
|
changelog_uri: https://github.com/aglushkov/serega/blob/master/CHANGELOG.md
|
98
99
|
post_install_message:
|
99
100
|
rdoc_options: []
|