alba 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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/master/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
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 the fastest JSON serializer for Ruby, JRuby, and TruffleRuby.
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, flexible and well-maintained!
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/master/benchmark).
32
+ Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/main/benchmark).
28
33
 
29
- ### Flexible
34
+ ### Easy
30
35
 
31
- Alba provides a small set of DSL to define your serialization logic. It also provides methods you can override to alter and filter serialized hash so that you have full control over the result.
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
- ### Maintained
38
+ ### Feature rich
34
39
 
35
- Alba is well-maintained and adds features quickly. [Coverage Status](https://coveralls.io/github/okuramasafumi/alba?branch=master) and [CodeClimate Maintainability](https://codeclimate.com/github/okuramasafumi/alba/maintainability) show the code base is quite healthy.
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
- * Oj, the fastest. Gem installation required.
95
- * active_support, mostly for Rails. Gem installation required.
96
- * default or json, with no external dependencies.
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 must install `ActiveSupport` to enable inference.
122
+ You can choose which inflector Alba uses for inference. Possible value for `with` option are:
123
123
 
124
- #### Error handling configuration
125
-
126
- You can configure error handling with `on_error` method.
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
- ### Inheritance and Ignorance
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 `exclude` or `ignore` certain attributes using `ignoring`.
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
- ignoring :id, :body
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 or `Alba.enable_root_key_transformation!` is called.
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
- Supported transformation types are :camel, :lower_camel and :dash.
680
+ #### Key transformation cascading
361
681
 
362
- #### Custom inflector
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
- module InflectorInterface
371
- def camelize(key)
372
- raise "Not implemented"
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
- def camelize_lower(key)
376
- raise "Not implemented"
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
- def dasherize(key)
380
- raise "Not implemented"
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
- For example you could use `Dry::Inflector`, which implements exactly the above interface. If you are developing a `Hanami`-Application `Dry::Inflector` is around. In this case the following would be sufficient:
386
- ```ruby
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
- ### Filtering attributes
725
+ #### Custom inflector
391
726
 
392
- You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky.
727
+ A custom inflector can be plugged in as follows.
393
728
 
394
729
  ```ruby
395
- class User
396
- attr_accessor :id, :name, :email, :created_at, :updated_at
730
+ module CustomInflector
731
+ module_function
397
732
 
398
- def initialize(id, name, email)
399
- @id = id
400
- @name = name
401
- @email = email
733
+ def camelize(string)
402
734
  end
403
- end
404
735
 
405
- class UserResource
406
- include Alba::Resource
736
+ def camelize_lower(string)
737
+ end
407
738
 
408
- attributes :id, :name, :email
739
+ def dasherize(string)
740
+ end
409
741
 
410
- private
742
+ def underscore(string)
743
+ end
411
744
 
412
- # Here using `Proc#>>` method to compose a proc from `super`
413
- def converter
414
- super >> proc { |hash| hash.compact }
745
+ def classify(string)
415
746
  end
416
747
  end
417
748
 
418
- user = User.new(1, nil, nil)
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 `convert` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
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
- # Global error handling
566
- Alba.on_error do |error, object, key, attribute, resource_class|
567
- if resource_class == MyResource
568
- ['error_fallback', object.error_fallback]
569
- else
570
- [key, error.message]
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/master/test/usecases/circular_association_test.rb)
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/okuramasafumi/alba. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/okuramasafumi/alba/blob/master/CODE_OF_CONDUCT.md).
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/master/CODE_OF_CONDUCT.md).
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).