alba 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +11 -0
- data/.github/dependabot.yml +4 -18
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/main.yml +4 -4
- data/.github/workflows/perf.yml +2 -2
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +30 -0
- data/Gemfile +6 -2
- data/HACKING.md +41 -0
- data/README.md +599 -128
- data/Rakefile +2 -2
- data/alba.gemspec +8 -4
- data/benchmark/README.md +81 -0
- data/benchmark/collection.rb +60 -74
- data/benchmark/single_resource.rb +33 -2
- data/docs/migrate_from_jbuilder.md +18 -4
- data/docs/rails.md +44 -0
- data/lib/alba/association.rb +49 -9
- data/lib/alba/conditional_attribute.rb +54 -0
- data/lib/alba/default_inflector.rb +13 -24
- data/lib/alba/errors.rb +10 -0
- data/lib/alba/layout.rb +67 -0
- data/lib/alba/nested_attribute.rb +18 -0
- data/lib/alba/resource.rb +240 -156
- data/lib/alba/typed_attribute.rb +1 -1
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +43 -58
- data/logo/alba-card.png +0 -0
- data/logo/alba-sign.png +0 -0
- data/logo/alba-typography.png +0 -0
- metadata +21 -11
- data/gemfiles/all.gemfile +0 -19
- data/lib/alba/key_transform_factory.rb +0 -33
- data/lib/alba/many.rb +0 -21
- data/lib/alba/one.rb +0 -21
- data/sider.yml +0 -60
data/README.md
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
+
![alba card](https://raw.githubusercontent.com/okuramasafumi/alba/main/logo/alba-card.png)
|
2
|
+
----------
|
1
3
|
[![Gem Version](https://badge.fury.io/rb/alba.svg)](https://badge.fury.io/rb/alba)
|
2
4
|
[![CI](https://github.com/okuramasafumi/alba/actions/workflows/main.yml/badge.svg)](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)
|
3
|
-
[![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/
|
5
|
+
[![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/main/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
|
4
6
|
[![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
|
5
|
-
[![Inline docs](http://inch-ci.org/github/okuramasafumi/alba.svg?branch=main)](http://inch-ci.org/github/okuramasafumi/alba)
|
6
7
|
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)
|
7
8
|
![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)
|
8
9
|
|
9
10
|
# Alba
|
10
11
|
|
11
|
-
Alba is
|
12
|
+
Alba is a JSON serializer for Ruby, JRuby, and TruffleRuby.
|
12
13
|
|
13
14
|
## Discussions
|
14
15
|
|
@@ -18,21 +19,25 @@ If you've already used Alba, please consider posting your thoughts and feelings
|
|
18
19
|
|
19
20
|
If you have feature requests or interesting ideas, join us with [Ideas](https://github.com/okuramasafumi/alba/discussions/categories/ideas). Let's make Alba even better, together!
|
20
21
|
|
22
|
+
## Resources
|
23
|
+
|
24
|
+
If you want to know more about Alba, there's a [screencast](https://hanamimastery.com/episodes/21-serialization-with-alba) created by Sebastian from [Hanami Mastery](https://hanamimastery.com/). It covers basic features of Alba and how to use it in Hanami.
|
25
|
+
|
21
26
|
## Why Alba?
|
22
27
|
|
23
|
-
Because it's fast,
|
28
|
+
Because it's fast, easy and feature rich!
|
24
29
|
|
25
30
|
### Fast
|
26
31
|
|
27
|
-
Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/
|
32
|
+
Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/main/benchmark).
|
28
33
|
|
29
|
-
###
|
34
|
+
### Easy
|
30
35
|
|
31
|
-
Alba
|
36
|
+
Alba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and short codebase. Finally it's easy to extend since it provides some methods for override to change default behavior of Alba.
|
32
37
|
|
33
|
-
###
|
38
|
+
### Feature rich
|
34
39
|
|
35
|
-
Alba is
|
40
|
+
While Alba's core is simple, it provides additional features when you need them, For example, Alba provides [a way to control circular associations](#circular-associations-control), [inferring resource classes, root key and associations](#inference) and [supports layouts](#layout).
|
36
41
|
|
37
42
|
## Installation
|
38
43
|
|
@@ -72,15 +77,6 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
72
77
|
* Layout
|
73
78
|
* No runtime dependencies
|
74
79
|
|
75
|
-
## Anti features
|
76
|
-
|
77
|
-
* Sorting keys
|
78
|
-
* Class level support of parameters
|
79
|
-
* Supporting all existing JSON encoder/decoder
|
80
|
-
* Cache
|
81
|
-
* [JSON:API](https://jsonapi.org) support
|
82
|
-
* And many others
|
83
|
-
|
84
80
|
## Usage
|
85
81
|
|
86
82
|
### Configuration
|
@@ -91,9 +87,13 @@ Alba's configuration is fairly simple.
|
|
91
87
|
|
92
88
|
Backend is the actual part serializing an object into JSON. Alba supports these backends.
|
93
89
|
|
94
|
-
|
95
|
-
|
96
|
-
|
90
|
+
|name|description|requires_external_gem|
|
91
|
+
|--|--|--|
|
92
|
+
|`oj`, `oj_strict`|Using Oj in `strict` mode|Yes(C extension)|
|
93
|
+
|`oj_rails`|It's `oj` but in `rails` mode|Yes(C extension)|
|
94
|
+
|`oj_default`|It's `oj` but respects mode set by users|Yes(C extension)|
|
95
|
+
|`active_support`|For Rails compatibility|Yes|
|
96
|
+
|`default`, `json`|Using `json` gem|No|
|
97
97
|
|
98
98
|
You can set a backend like this:
|
99
99
|
|
@@ -116,23 +116,21 @@ You can consider setting a backend with Symbol as a shortcut to set encoder.
|
|
116
116
|
You can enable inference feature using `enable_inference!` method.
|
117
117
|
|
118
118
|
```ruby
|
119
|
-
Alba.enable_inference!
|
119
|
+
Alba.enable_inference!(with: :active_support)
|
120
120
|
```
|
121
121
|
|
122
|
-
You
|
122
|
+
You can choose which inflector Alba uses for inference. Possible value for `with` option are:
|
123
123
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
```ruby
|
129
|
-
Alba.on_error :ignore
|
130
|
-
```
|
124
|
+
- `:active_support` for `ActiveSupport::Inflector`
|
125
|
+
- `:dry` for `Dry::Inflector`
|
126
|
+
- any object which responds to some methods (see [below](#custom-inflector))
|
131
127
|
|
132
128
|
For the details, see [Error handling section](#error-handling)
|
133
129
|
|
134
130
|
### Simple serialization with root key
|
135
131
|
|
132
|
+
You can define attributes with (yes) `attributes` macro with attribute names. If your attribute need some calculations, you can use `attribute` with block.
|
133
|
+
|
136
134
|
```ruby
|
137
135
|
class User
|
138
136
|
attr_accessor :id, :name, :email, :created_at, :updated_at
|
@@ -159,11 +157,67 @@ end
|
|
159
157
|
|
160
158
|
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
161
159
|
UserResource.new(user).serialize
|
162
|
-
# => "{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}"
|
160
|
+
# => "{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}}"
|
161
|
+
```
|
162
|
+
|
163
|
+
You can define instance methods on resources so that you can use it as attribute name in `attributes`.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
# The serialization result is the same as above
|
167
|
+
class UserResource
|
168
|
+
include Alba::Resource
|
169
|
+
|
170
|
+
root_key :user, :users # Later is for plural
|
171
|
+
|
172
|
+
attributes :id, :name, :name_with_email
|
173
|
+
|
174
|
+
# Attribute methods must accept one argument for each serialized object
|
175
|
+
def name_with_email(user)
|
176
|
+
"#{user.name}: #{user.email}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
This even works with users collection.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
user1 = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
185
|
+
user2 = User.new(2, 'Test User', 'test@example.com')
|
186
|
+
UserResource.new([user1, user2]).serialize
|
187
|
+
# => "{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"},{\"id\":2,\"name\":\"Test User\",\"name_with_email\":\"Test User: test@example.com\"}]}"
|
188
|
+
```
|
189
|
+
|
190
|
+
If you have a simple case where you want to change only the name, you can use the Symbol to Proc shortcut:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class UserResource
|
194
|
+
include Alba::Resource
|
195
|
+
|
196
|
+
attribute :some_other_name, &:name
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
#### Params
|
201
|
+
|
202
|
+
You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class UserResource
|
206
|
+
include Alba::Resource
|
207
|
+
attribute :name do |user|
|
208
|
+
params[:upcase] ? user.name.upcase : user.name
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
user = User.new(1, 'Masa', 'test@example.com')
|
213
|
+
UserResource.new(user).serialize # => "{\"name\":\"foo\"}"
|
214
|
+
UserResource.new(user, params: {upcase: true}).serialize # => "{\"name\":\"FOO\"}"
|
163
215
|
```
|
164
216
|
|
165
217
|
### Serialization with associations
|
166
218
|
|
219
|
+
Associations can be defined using the `association` macro, which is also aliased as `one`, `many`, `has_one`, and `has_many` for convenience.
|
220
|
+
|
167
221
|
```ruby
|
168
222
|
class User
|
169
223
|
attr_reader :id, :created_at, :updated_at
|
@@ -240,6 +294,208 @@ class AnotherUserResource
|
|
240
294
|
end
|
241
295
|
```
|
242
296
|
|
297
|
+
You can "filter" association using second proc argument. This proc takes association object, `params` and initial object.
|
298
|
+
|
299
|
+
This feature is useful when you want to modify association, such as adding `includes` or `order` to ActiveRecord relations.
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class User
|
303
|
+
attr_reader :id, :banned
|
304
|
+
attr_accessor :articles
|
305
|
+
|
306
|
+
def initialize(id, banned = false)
|
307
|
+
@id = id
|
308
|
+
@banned = banned
|
309
|
+
@articles = []
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Article
|
314
|
+
attr_accessor :id, :title, :body
|
315
|
+
|
316
|
+
def initialize(id, title, body)
|
317
|
+
@id = id
|
318
|
+
@title = title
|
319
|
+
@body = body
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
class ArticleResource
|
324
|
+
include Alba::Resource
|
325
|
+
|
326
|
+
attributes :title
|
327
|
+
end
|
328
|
+
|
329
|
+
class UserResource
|
330
|
+
include Alba::Resource
|
331
|
+
|
332
|
+
attributes :id
|
333
|
+
|
334
|
+
# Second proc works as a filter
|
335
|
+
many :articles,
|
336
|
+
proc { |articles, params, user|
|
337
|
+
filter = params[:filter] || :odd?
|
338
|
+
articles.select {|a| a.id.send(filter) && !user.banned }
|
339
|
+
},
|
340
|
+
resource: ArticleResource
|
341
|
+
end
|
342
|
+
|
343
|
+
user = User.new(1)
|
344
|
+
article1 = Article.new(1, 'Hello World!', 'Hello World!!!')
|
345
|
+
user.articles << article1
|
346
|
+
article2 = Article.new(2, 'Super nice', 'Really nice!')
|
347
|
+
user.articles << article2
|
348
|
+
|
349
|
+
UserResource.new(user).serialize
|
350
|
+
# => '{"id":1,"articles":[{"title":"Hello World!"}]}'
|
351
|
+
UserResource.new(user, params: {filter: :even?}).serialize
|
352
|
+
# => '{"id":1,"articles":[{"title":"Super nice"}]}'
|
353
|
+
```
|
354
|
+
|
355
|
+
You can change a key for association with `key` option.
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
class UserResource
|
359
|
+
include Alba::Resource
|
360
|
+
|
361
|
+
attributes :id
|
362
|
+
|
363
|
+
many :articles,
|
364
|
+
key: 'my_articles', # Set key here
|
365
|
+
resource: ArticleResource
|
366
|
+
end
|
367
|
+
UserResource.new(user).serialize
|
368
|
+
# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
|
369
|
+
```
|
370
|
+
|
371
|
+
You can omit resource option if you enable Alba's inference feature.
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
class UserResource
|
375
|
+
include Alba::Resource
|
376
|
+
|
377
|
+
attributes :id
|
378
|
+
|
379
|
+
many :articles # Using `ArticleResource`
|
380
|
+
end
|
381
|
+
UserResource.new(user).serialize
|
382
|
+
# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
|
383
|
+
```
|
384
|
+
|
385
|
+
If you need complex logic to determine what resource to use for association, you can use a Proc for resource option.
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
class UserResource
|
389
|
+
include Alba::Resource
|
390
|
+
|
391
|
+
attributes :id
|
392
|
+
|
393
|
+
many :articles, ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
|
394
|
+
end
|
395
|
+
```
|
396
|
+
|
397
|
+
Note that using a Proc slows down serialization if there are too `many` associated objects.
|
398
|
+
|
399
|
+
#### Params override
|
400
|
+
|
401
|
+
Associations can override params. This is useful when associations are deeply nested.
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
class BazResource
|
405
|
+
include Alba::Resource
|
406
|
+
|
407
|
+
attributes :data
|
408
|
+
attributes :secret, if: proc { params[:expose_secret] }
|
409
|
+
end
|
410
|
+
|
411
|
+
class BarResource
|
412
|
+
include Alba::Resource
|
413
|
+
|
414
|
+
one :baz, resource: BazResource
|
415
|
+
end
|
416
|
+
|
417
|
+
class FooResource
|
418
|
+
include Alba::Resource
|
419
|
+
|
420
|
+
root_key :foo
|
421
|
+
|
422
|
+
one :bar, resource: BarResource
|
423
|
+
end
|
424
|
+
|
425
|
+
class FooResourceWithParamsOverride
|
426
|
+
include Alba::Resource
|
427
|
+
|
428
|
+
root_key :foo
|
429
|
+
|
430
|
+
one :bar, resource: BarResource, params: { expose_secret: false }
|
431
|
+
end
|
432
|
+
|
433
|
+
Baz = Struct.new(:data, :secret)
|
434
|
+
Bar = Struct.new(:baz)
|
435
|
+
Foo = Struct.new(:bar)
|
436
|
+
|
437
|
+
foo = Foo.new(Bar.new(Baz.new(1, 'secret')))
|
438
|
+
FooResource.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1,"secret":"secret"}}}}'
|
439
|
+
FooResourceWithParamsOverride.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1}}}}'
|
440
|
+
```
|
441
|
+
|
442
|
+
### Nested Attribute
|
443
|
+
|
444
|
+
Alba supports nested attributes that makes it easy to build complex data structure from single object.
|
445
|
+
|
446
|
+
In order to define nested attributes, you can use `nested` or `nested_attribute` (alias of `nested`).
|
447
|
+
|
448
|
+
```ruby
|
449
|
+
class User
|
450
|
+
attr_accessor :id, :name, :email, :city, :zipcode
|
451
|
+
|
452
|
+
def initialize(id, name, email, city, zipcode)
|
453
|
+
@id = id
|
454
|
+
@name = name
|
455
|
+
@email = email
|
456
|
+
@city = city
|
457
|
+
@zipcode = zipcode
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
class UserResource
|
462
|
+
include Alba::Resource
|
463
|
+
|
464
|
+
root_key :user
|
465
|
+
|
466
|
+
attributes :id
|
467
|
+
|
468
|
+
nested_attribute :address do
|
469
|
+
attributes :city, :zipcode
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com', 'Tokyo', '0000000')
|
474
|
+
UserResource.new(user).serialize
|
475
|
+
# => '{"user":{"id":1,"address":{"city":"Tokyo","zipcode":"0000000"}}}'
|
476
|
+
```
|
477
|
+
|
478
|
+
Nested attributes can be nested deeply.
|
479
|
+
|
480
|
+
```ruby
|
481
|
+
class FooResource
|
482
|
+
include Alba::Resource
|
483
|
+
|
484
|
+
root_key :foo
|
485
|
+
|
486
|
+
nested :bar do
|
487
|
+
nested :baz do
|
488
|
+
attribute :deep do
|
489
|
+
42
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
FooResource.new(nil).serialize
|
496
|
+
# => '{"foo":{"bar":{"baz":{"deep":42}}}}'
|
497
|
+
```
|
498
|
+
|
243
499
|
### Inline definition with `Alba.serialize`
|
244
500
|
|
245
501
|
`Alba.serialize` method is a shortcut to define everything inline.
|
@@ -263,9 +519,61 @@ Alba.serialize(something)
|
|
263
519
|
|
264
520
|
Although this might be useful sometimes, it's generally recommended to define a class for Resource.
|
265
521
|
|
266
|
-
###
|
522
|
+
### Serializable Hash
|
523
|
+
|
524
|
+
Instead of serializing to JSON, you can also output a Hash by calling `serializable_hash` or the `to_h` alias. Note also that the `serialize` method is aliased as `to_json`.
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
# These are equivalent and will return serialized JSON
|
528
|
+
UserResource.new(user).serialize
|
529
|
+
UserResource.new(user).to_json
|
530
|
+
|
531
|
+
# These are equivalent and will return a Hash
|
532
|
+
UserResource.new(user).serializable_hash
|
533
|
+
UserResource.new(user).to_h
|
534
|
+
```
|
535
|
+
|
536
|
+
If you want a Hash that corresponds to a JSON String returned by `serialize` method, you can use `as_json`.
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
# These are equivalent and will return the same result
|
540
|
+
UserResource.new(user).serialize
|
541
|
+
UserResource.new(user).to_json
|
542
|
+
JSON.generate(UserResource.new(user).as_json)
|
543
|
+
```
|
544
|
+
|
545
|
+
### Inheritance
|
546
|
+
|
547
|
+
When you include `Alba::Resource` in your class, it's just a class so you can define any class that inherits from it. You can add new attributes to inherited class like below:
|
548
|
+
|
549
|
+
```ruby
|
550
|
+
class FooResource
|
551
|
+
include Alba::Resource
|
552
|
+
|
553
|
+
root_key :foo
|
554
|
+
|
555
|
+
attributes :bar
|
556
|
+
end
|
557
|
+
|
558
|
+
class ExtendedFooResource < FooResource
|
559
|
+
root_key :foofoo
|
560
|
+
|
561
|
+
attributes :baz
|
562
|
+
end
|
563
|
+
|
564
|
+
Foo = Struct.new(:bar, :baz)
|
565
|
+
foo = Foo.new(1, 2)
|
566
|
+
FooResource.new(foo).serialize # => '{"foo":{"bar":1}}'
|
567
|
+
ExtendedFooResource.new(foo).serialize # => '{"foo":{"bar":1,"baz":2}}'
|
568
|
+
```
|
569
|
+
|
570
|
+
In this example we add `baz` attribute and change `root_key`. This way, you can extend existing resource classes just like normal OOP. Don't forget that when your inheritance structure is too deep it'll become difficult to modify existing classes.
|
571
|
+
|
572
|
+
### Filtering attributes
|
267
573
|
|
268
|
-
You can
|
574
|
+
You can filter out certain attributes by overriding `attributes` instance method. This is useful when you want to customize existing resource with inheritance.
|
575
|
+
|
576
|
+
You can access raw attributes via `super` call. It returns a Hash whose keys are the name of the attribute and whose values are the body. Usually you need only keys to filter out, like below.
|
269
577
|
|
270
578
|
```ruby
|
271
579
|
class Foo
|
@@ -285,7 +593,9 @@ class GenericFooResource
|
|
285
593
|
end
|
286
594
|
|
287
595
|
class RestrictedFooResource < GenericFooResource
|
288
|
-
|
596
|
+
def attributes
|
597
|
+
super.select { |key, _| key.to_sym == :name }
|
598
|
+
end
|
289
599
|
end
|
290
600
|
|
291
601
|
RestrictedFooResource.new(foo).serialize
|
@@ -324,14 +634,24 @@ UserResourceCamel.new(user).serialize
|
|
324
634
|
# => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}'
|
325
635
|
```
|
326
636
|
|
637
|
+
Possible values for `transform_keys` argument are:
|
638
|
+
|
639
|
+
* `:camel` for CamelCase
|
640
|
+
* `:lower_camel` for lowerCamelCase
|
641
|
+
* `:dash` for dash-case
|
642
|
+
* `:snake` for snake_case
|
643
|
+
* `:none` for not transforming keys
|
644
|
+
|
645
|
+
#### Root key transformation
|
646
|
+
|
327
647
|
You can also transform root key when:
|
328
648
|
|
329
649
|
* `Alba.enable_inference!` is called
|
330
650
|
* `root_key!` is called in Resource class
|
331
|
-
* `root` option of `transform_keys` is set to true
|
651
|
+
* `root` option of `transform_keys` is set to true
|
332
652
|
|
333
653
|
```ruby
|
334
|
-
Alba.enable_inference!
|
654
|
+
Alba.enable_inference!(with: :active_support) # with :dry also works
|
335
655
|
|
336
656
|
class BankAccount
|
337
657
|
attr_reader :account_number
|
@@ -357,75 +677,81 @@ BankAccountResource.new(bank_account).serialize
|
|
357
677
|
|
358
678
|
This behavior to transform root key will become default at version 2.
|
359
679
|
|
360
|
-
|
680
|
+
#### Key transformation cascading
|
361
681
|
|
362
|
-
|
682
|
+
When you use `transform_keys` with inline association, it automatically applies the same transformation type to those inline association.
|
683
|
+
|
684
|
+
This is the default behavior from version 2, but you can do the same thing with adding `transform_keys` to each association.
|
685
|
+
|
686
|
+
You can also turn it off by setting `cascade: false` option to `transform_keys`.
|
363
687
|
|
364
|
-
A custom inflector can be plugged in as follows...
|
365
|
-
```ruby
|
366
|
-
Alba.inflector = MyCustomInflector
|
367
|
-
```
|
368
|
-
...and has to implement following interface (the parameter `key` is of type `String`):
|
369
688
|
```ruby
|
370
|
-
|
371
|
-
|
372
|
-
|
689
|
+
class User
|
690
|
+
attr_reader :id, :first_name, :last_name
|
691
|
+
|
692
|
+
def initialize(id, first_name, last_name)
|
693
|
+
@id = id
|
694
|
+
@first_name = first_name
|
695
|
+
@last_name = last_name
|
696
|
+
@bank_account = BankAccount.new(1234)
|
373
697
|
end
|
698
|
+
end
|
374
699
|
|
375
|
-
|
376
|
-
|
700
|
+
class BankAccount
|
701
|
+
attr_reader :account_number
|
702
|
+
|
703
|
+
def initialize(account_number)
|
704
|
+
@account_number = account_number
|
377
705
|
end
|
706
|
+
end
|
378
707
|
|
379
|
-
|
380
|
-
|
708
|
+
class UserResource
|
709
|
+
include Alba::Resource
|
710
|
+
|
711
|
+
attributes :id, :first_name, :last_name
|
712
|
+
|
713
|
+
transform_keys :lower_camel # Default is cascade: true
|
714
|
+
|
715
|
+
one :bank_account do
|
716
|
+
attributes :account_number
|
381
717
|
end
|
382
718
|
end
|
383
719
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
Alba.inflector = Dry::Inflector.new
|
720
|
+
user = User.new(1, 'Masafumi', 'Okura')
|
721
|
+
UserResource.new(user).serialize
|
722
|
+
# => '{"id":1,"firstName":"Masafumi","lastName":"Okura","bankAccount":{"accountNumber":1234}}'
|
388
723
|
```
|
389
724
|
|
390
|
-
|
725
|
+
#### Custom inflector
|
391
726
|
|
392
|
-
|
727
|
+
A custom inflector can be plugged in as follows.
|
393
728
|
|
394
729
|
```ruby
|
395
|
-
|
396
|
-
|
730
|
+
module CustomInflector
|
731
|
+
module_function
|
397
732
|
|
398
|
-
def
|
399
|
-
@id = id
|
400
|
-
@name = name
|
401
|
-
@email = email
|
733
|
+
def camelize(string)
|
402
734
|
end
|
403
|
-
end
|
404
735
|
|
405
|
-
|
406
|
-
|
736
|
+
def camelize_lower(string)
|
737
|
+
end
|
407
738
|
|
408
|
-
|
739
|
+
def dasherize(string)
|
740
|
+
end
|
409
741
|
|
410
|
-
|
742
|
+
def underscore(string)
|
743
|
+
end
|
411
744
|
|
412
|
-
|
413
|
-
def converter
|
414
|
-
super >> proc { |hash| hash.compact }
|
745
|
+
def classify(string)
|
415
746
|
end
|
416
747
|
end
|
417
748
|
|
418
|
-
|
419
|
-
UserResource.new(user).serialize # => '{"id":1}'
|
749
|
+
Alba.enable_inference!(with: CustomInflector)
|
420
750
|
```
|
421
751
|
|
422
|
-
The key part is the use of `Proc#>>` since `Alba::Resource#converter` returns a `Proc` which contains the basic logic and it's impossible to change its behavior by just overriding the method.
|
423
|
-
|
424
|
-
It's not recommended to swap the whole conversion logic. It's recommended to always call `super` when you override `converter`.
|
425
|
-
|
426
752
|
### Conditional attributes
|
427
753
|
|
428
|
-
Filtering attributes with overriding `
|
754
|
+
Filtering attributes with overriding `attributes` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
|
429
755
|
|
430
756
|
In these cases, conditional attributes works well. We can pass `if` option to `attributes`, `attribute`, `one` and `many`. Below is an example for the same effect as [filtering attributes section](#filtering-attributes).
|
431
757
|
|
@@ -469,7 +795,7 @@ We believe this is clearer than using some (not implemented yet) DSL such as `de
|
|
469
795
|
After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
|
470
796
|
|
471
797
|
```ruby
|
472
|
-
Alba.enable_inference!
|
798
|
+
Alba.enable_inference!(with: :active_support) # with :dry also works
|
473
799
|
|
474
800
|
class User
|
475
801
|
attr_reader :id
|
@@ -517,8 +843,6 @@ This resource automatically sets its root key to either "users" or "user", depen
|
|
517
843
|
|
518
844
|
Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
|
519
845
|
|
520
|
-
Note that to enable this feature you must install `ActiveSupport` gem.
|
521
|
-
|
522
846
|
### Error handling
|
523
847
|
|
524
848
|
You can set error handler globally or per resource using `on_error`.
|
@@ -562,12 +886,14 @@ There are four possible arguments `on_error` method accepts.
|
|
562
886
|
The block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example.
|
563
887
|
|
564
888
|
```ruby
|
565
|
-
|
566
|
-
Alba
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
889
|
+
class ExampleResource
|
890
|
+
include Alba::Resource
|
891
|
+
on_error do |error, object, key, attribute, resource_class|
|
892
|
+
if resource_class == MyResource
|
893
|
+
['error_fallback', object.error_fallback]
|
894
|
+
else
|
895
|
+
[key, error.message]
|
896
|
+
end
|
571
897
|
end
|
572
898
|
end
|
573
899
|
```
|
@@ -624,43 +950,6 @@ UserResource.new(User.new(1)).serialize
|
|
624
950
|
# => '{"user":{"id":1,"name":"User1","age":20}}'
|
625
951
|
```
|
626
952
|
|
627
|
-
You can also set global nil handler.
|
628
|
-
|
629
|
-
```ruby
|
630
|
-
Alba.on_nil { 'default name' }
|
631
|
-
|
632
|
-
class Foo
|
633
|
-
attr_reader :name
|
634
|
-
def initialize(name)
|
635
|
-
@name = name
|
636
|
-
end
|
637
|
-
end
|
638
|
-
|
639
|
-
class FooResource
|
640
|
-
include Alba::Resource
|
641
|
-
|
642
|
-
key :foo
|
643
|
-
|
644
|
-
attributes :name
|
645
|
-
end
|
646
|
-
|
647
|
-
FooResource.new(Foo.new).serialize
|
648
|
-
# => '{"foo":{"name":"default name"}}'
|
649
|
-
|
650
|
-
class FooResource2
|
651
|
-
include Alba::Resource
|
652
|
-
|
653
|
-
key :foo
|
654
|
-
|
655
|
-
on_nil { '' } # This is applied instead of global handler
|
656
|
-
|
657
|
-
attributes :name
|
658
|
-
end
|
659
|
-
|
660
|
-
FooResource2.new(Foo.new).serialize
|
661
|
-
# => '{"foo":{"name":""}}'
|
662
|
-
```
|
663
|
-
|
664
953
|
### Metadata
|
665
954
|
|
666
955
|
You can set a metadata with `meta` DSL or `meta` option.
|
@@ -715,7 +1004,7 @@ Note that setting root key is required when setting a metadata.
|
|
715
1004
|
|
716
1005
|
You can control circular associations with `within` option. `within` option is a nested Hash such as `{book: {authors: books}}`. In this example, Alba serializes a book's authors' books. This means you can reference `BookResource` from `AuthorResource` and vice versa. This is really powerful when you have a complex data structure and serialize certain parts of it.
|
717
1006
|
|
718
|
-
For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/
|
1007
|
+
For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/main/test/usecases/circular_association_test.rb)
|
719
1008
|
|
720
1009
|
### Experimental support of types
|
721
1010
|
|
@@ -758,6 +1047,37 @@ UserResource.new(user).serialize
|
|
758
1047
|
|
759
1048
|
Note that this feature is experimental and interfaces are subject to change.
|
760
1049
|
|
1050
|
+
### Collection serialization into Hash
|
1051
|
+
|
1052
|
+
Sometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.
|
1053
|
+
|
1054
|
+
```ruby
|
1055
|
+
class User
|
1056
|
+
attr_reader :id, :name
|
1057
|
+
def initialize(id, name)
|
1058
|
+
@id, @name = id, name
|
1059
|
+
end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
class UserResource
|
1063
|
+
include Alba::Resource
|
1064
|
+
|
1065
|
+
collection_key :id # This line is important
|
1066
|
+
|
1067
|
+
attributes :id, :name
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
user1 = User.new(1, 'John')
|
1071
|
+
user2 = User.new(2, 'Masafumi')
|
1072
|
+
|
1073
|
+
UserResource.new([user1, user2]).serialize
|
1074
|
+
# => '{"1":{"id":1,"name":"John"},"2":{"id":2,"name":"Masafumi"}}'
|
1075
|
+
```
|
1076
|
+
|
1077
|
+
In the snippet above, `collection_key :id` specifies the key used for the key of the collection hash. In this example it's `:id`.
|
1078
|
+
|
1079
|
+
Make sure that collection key is unique for the collection.
|
1080
|
+
|
761
1081
|
### Layout
|
762
1082
|
|
763
1083
|
Sometimes we'd like to serialize JSON into a template. In other words, we need some structure OUTSIDE OF serialized JSON. IN HTML world, we call it a "layout".
|
@@ -824,6 +1144,156 @@ Also note that we use percentage notation here to use double quotes. Using singl
|
|
824
1144
|
|
825
1145
|
Currently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).
|
826
1146
|
|
1147
|
+
### Extend Alba
|
1148
|
+
|
1149
|
+
Sometimes we have shared behaviors across resources. In such cases we can have a module for common logic.
|
1150
|
+
|
1151
|
+
In `attribute` block we can call instance method so we can improve the code below:
|
1152
|
+
|
1153
|
+
```ruby
|
1154
|
+
class FooResource
|
1155
|
+
include Alba::Resource
|
1156
|
+
# other attributes
|
1157
|
+
attribute :created_at do |foo|
|
1158
|
+
foo.created_at.strftime('%m/%d/%Y')
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
attribute :updated_at do |foo|
|
1162
|
+
foo.updated_at.strftime('%m/%d/%Y')
|
1163
|
+
end
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
class BarResource
|
1167
|
+
include Alba::Resource
|
1168
|
+
# other attributes
|
1169
|
+
attribute :created_at do |bar|
|
1170
|
+
bar.created_at.strftime('%m/%d/%Y')
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
attribute :updated_at do |bar|
|
1174
|
+
bar.updated_at.strftime('%m/%d/%Y')
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
```
|
1178
|
+
|
1179
|
+
to:
|
1180
|
+
|
1181
|
+
```ruby
|
1182
|
+
module SharedLogic
|
1183
|
+
def format_time(time)
|
1184
|
+
time.strftime('%m/%d/%Y')
|
1185
|
+
end
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
class FooResource
|
1189
|
+
include Alba::Resource
|
1190
|
+
include SharedLogic
|
1191
|
+
# other attributes
|
1192
|
+
attribute :created_at do |foo|
|
1193
|
+
format_time(foo.created_at)
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
attribute :updated_at do |foo|
|
1197
|
+
format_time(foo.updated_at)
|
1198
|
+
end
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
class BarResource
|
1202
|
+
include Alba::Resource
|
1203
|
+
include SharedLogic
|
1204
|
+
# other attributes
|
1205
|
+
attribute :created_at do |bar|
|
1206
|
+
format_time(bar.created_at)
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
attribute :updated_at do |bar|
|
1210
|
+
format_time(bar.updated_at)
|
1211
|
+
end
|
1212
|
+
end
|
1213
|
+
```
|
1214
|
+
|
1215
|
+
We can even add our own DSL to serialize attributes for readability and removing code duplications.
|
1216
|
+
|
1217
|
+
To do so, we need to `extend` our module. Let's see how we can achieve the same goal with this approach.
|
1218
|
+
|
1219
|
+
```ruby
|
1220
|
+
module AlbaExtension
|
1221
|
+
# Here attrs are an Array of Symbol
|
1222
|
+
def formatted_time_attributes(*attrs)
|
1223
|
+
attrs.each do |attr|
|
1224
|
+
attribute attr do |object|
|
1225
|
+
time = object.send(attr)
|
1226
|
+
time.strftime('%m/%d/%Y')
|
1227
|
+
end
|
1228
|
+
end
|
1229
|
+
end
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
class FooResource
|
1233
|
+
include Alba::Resource
|
1234
|
+
extend AlbaExtension
|
1235
|
+
# other attributes
|
1236
|
+
formatted_time_attributes :created_at, :updated_at
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
class BarResource
|
1240
|
+
include Alba::Resource
|
1241
|
+
extend AlbaExtension
|
1242
|
+
# other attributes
|
1243
|
+
formatted_time_attributes :created_at, :updated_at
|
1244
|
+
end
|
1245
|
+
```
|
1246
|
+
|
1247
|
+
In this way we have shorter and cleaner code. Note that we need to use `send` or `public_send` in `attribute` block to get attribute data.
|
1248
|
+
|
1249
|
+
### Debugging
|
1250
|
+
|
1251
|
+
Debugging is not easy. If you find Alba not working as you expect, there are a few things to do:
|
1252
|
+
|
1253
|
+
1. Inspect
|
1254
|
+
|
1255
|
+
The typical code looks like this:
|
1256
|
+
|
1257
|
+
```ruby
|
1258
|
+
class FooResource
|
1259
|
+
include Alba::Resource
|
1260
|
+
attributes :id
|
1261
|
+
end
|
1262
|
+
FooResource.new(foo).serialize
|
1263
|
+
```
|
1264
|
+
|
1265
|
+
Notice that we instantiate `FooResource` and then call `serialize` method. We can get various information by calling `inspect` method on it.
|
1266
|
+
|
1267
|
+
```ruby
|
1268
|
+
puts FooResource.new(foo).inspect # or: p class FooResource.new(foo)
|
1269
|
+
# => "#<FooResource:0x000000010e21f408 @object=[#<Foo:0x000000010e3470d8 @id=1>], @params={}, @within=#<Object:0x000000010df2eac8>, @method_existence={}, @_attributes={:id=>:id}, @_key=nil, @_key_for_collection=nil, @_meta=nil, @_transform_type=:none, @_transforming_root_key=false, @_on_error=nil, @_on_nil=nil, @_layout=nil, @_collection_key=nil>"
|
1270
|
+
```
|
1271
|
+
|
1272
|
+
The output might be different depending on the version of Alba or the object you give, but the concepts are the same. `@object` represents the object you gave as an argument to `new` method, `@_attributes` represents the attributes you defined in `FooResource` class using `attributes` DSL.
|
1273
|
+
|
1274
|
+
Other things are not so important, but you need to take care of corresponding part when you use additional features such as `root_key`, `transform_keys` and adding params.
|
1275
|
+
|
1276
|
+
2. Logging
|
1277
|
+
|
1278
|
+
Alba currently doesn't support logging directly, but you can add your own logging module to Alba easily.
|
1279
|
+
|
1280
|
+
```ruby
|
1281
|
+
module Logging
|
1282
|
+
def serialize(...) # `...` was added in Ruby 2.7
|
1283
|
+
puts serializable_hash
|
1284
|
+
super(...)
|
1285
|
+
end
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
FooResource.prepend Logging
|
1289
|
+
FooResource.new(foo).serialize
|
1290
|
+
# => "{:id=>1}" is printed
|
1291
|
+
```
|
1292
|
+
|
1293
|
+
Here, we override `serialize` method with `prepend`. In overridden method we print the result of `serializable_hash` that gives the basic hash for serialization to `serialize` method. Using `...` allows us to override without knowing method signiture of `serialize`.
|
1294
|
+
|
1295
|
+
Don't forget calling `super` in this way.
|
1296
|
+
|
827
1297
|
## Rails
|
828
1298
|
|
829
1299
|
When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.
|
@@ -834,6 +1304,8 @@ Alba.backend = :active_support
|
|
834
1304
|
Alba.backend = :oj_rails
|
835
1305
|
```
|
836
1306
|
|
1307
|
+
To find out more details, please see https://github.com/okuramasafumi/alba/blob/main/docs/rails.md
|
1308
|
+
|
837
1309
|
## Why named "Alba"?
|
838
1310
|
|
839
1311
|
The name "Alba" comes from "albatross", a kind of birds. In Japanese, this bird is called "Aho-dori", which means "stupid bird". I find it funny because in fact albatrosses fly really fast. I hope Alba looks stupid but in fact it does its job quick.
|
@@ -847,14 +1319,13 @@ There are great pioneers in Ruby's ecosystem which does basically the same thing
|
|
847
1319
|
|
848
1320
|
## Development
|
849
1321
|
|
850
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
1322
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
851
1323
|
|
852
1324
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
853
1325
|
|
854
1326
|
## Contributing
|
855
1327
|
|
856
|
-
|
857
|
-
|
1328
|
+
Thank you for begin interested in contributing to Alba! Please see [contributors guide](https://github.com/okuramasafumi/alba/blob/main/CONTRIBUTING.md) before start contributing. If you have any questions, please feel free to ask in [Discussions](https://github.com/okuramasafumi/alba/discussions).
|
858
1329
|
|
859
1330
|
## License
|
860
1331
|
|
@@ -862,4 +1333,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
862
1333
|
|
863
1334
|
## Code of Conduct
|
864
1335
|
|
865
|
-
Everyone interacting in the Alba project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/okuramasafumi/alba/blob/
|
1336
|
+
Everyone interacting in the Alba project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/okuramasafumi/alba/blob/main/CODE_OF_CONDUCT.md).
|