alba 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/main.yml +3 -1
- data/.github/workflows/perf.yml +21 -0
- data/.rubocop.yml +9 -7
- data/CHANGELOG.md +28 -0
- data/Gemfile +4 -3
- data/README.md +469 -60
- data/alba.gemspec +7 -3
- data/benchmark/collection.rb +108 -3
- data/benchmark/single_resource.rb +82 -1
- data/docs/migrate_from_active_model_serializers.md +359 -0
- data/docs/migrate_from_jbuilder.md +223 -0
- data/gemfiles/all.gemfile +2 -1
- data/gemfiles/without_active_support.gemfile +1 -1
- data/gemfiles/without_oj.gemfile +1 -1
- data/lib/alba/association.rb +33 -24
- data/lib/alba/default_inflector.rb +22 -4
- data/lib/alba/deprecation.rb +14 -0
- data/lib/alba/errors.rb +10 -0
- data/lib/alba/resource.rb +260 -100
- data/lib/alba/typed_attribute.rb +3 -6
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +110 -36
- data/script/perf_check.rb +174 -0
- metadata +14 -8
- data/lib/alba/key_transform_factory.rb +0 -33
- data/lib/alba/many.rb +0 -21
- data/lib/alba/one.rb +0 -21
data/README.md
CHANGED
@@ -19,19 +19,19 @@ If you have feature requests or interesting ideas, join us with [Ideas](https://
|
|
19
19
|
|
20
20
|
## Why Alba?
|
21
21
|
|
22
|
-
Because it's fast,
|
22
|
+
Because it's fast, easy-to-use and extensible!
|
23
23
|
|
24
24
|
### Fast
|
25
25
|
|
26
26
|
Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark).
|
27
27
|
|
28
|
-
###
|
28
|
+
### Easy to use
|
29
29
|
|
30
|
-
Alba provides
|
30
|
+
Alba provides four DSLs, `attributes` to fetch attribute with its name, `attribute` to execute block for the attribute, `one` to seriaize single attribute with another resource, and `many` to serialize collection attribute with another resource. When you want to do something complex, there are many examples in this README so you can mimic them to get started.
|
31
31
|
|
32
|
-
###
|
32
|
+
### Extensible
|
33
33
|
|
34
|
-
Alba
|
34
|
+
Alba embraces extensibility through common techniques such as class inheritance and module inclusion. Alba provides its capacity with one module so you can still have your own class hierarchy.
|
35
35
|
|
36
36
|
## Installation
|
37
37
|
|
@@ -64,20 +64,13 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
64
64
|
* Key transformation
|
65
65
|
* Root key inference
|
66
66
|
* Error handling
|
67
|
+
* Nil handling
|
67
68
|
* Resource name inflection based on association name
|
68
69
|
* Circular associations control
|
69
70
|
* [Experimental] Types for validation and conversion
|
71
|
+
* Layout
|
70
72
|
* No runtime dependencies
|
71
73
|
|
72
|
-
## Anti features
|
73
|
-
|
74
|
-
* Sorting keys
|
75
|
-
* Class level support of parameters
|
76
|
-
* Supporting all existing JSON encoder/decoder
|
77
|
-
* Cache
|
78
|
-
* [JSON:API](https://jsonapi.org) support
|
79
|
-
* And many others
|
80
|
-
|
81
74
|
## Usage
|
82
75
|
|
83
76
|
### Configuration
|
@@ -98,27 +91,35 @@ You can set a backend like this:
|
|
98
91
|
Alba.backend = :oj
|
99
92
|
```
|
100
93
|
|
101
|
-
####
|
94
|
+
#### Encoder configuration
|
102
95
|
|
103
|
-
You can
|
96
|
+
You can also set JSON encoder directly with a Proc.
|
104
97
|
|
105
98
|
```ruby
|
106
|
-
Alba.
|
99
|
+
Alba.encoder = ->(object) { JSON.generate(object) }
|
107
100
|
```
|
108
101
|
|
109
|
-
You
|
102
|
+
You can consider setting a backend with Symbol as a shortcut to set encoder.
|
110
103
|
|
111
|
-
####
|
104
|
+
#### Inference configuration
|
112
105
|
|
113
|
-
You can
|
106
|
+
You can enable inference feature using `enable_inference!` method.
|
114
107
|
|
115
108
|
```ruby
|
116
|
-
Alba.
|
109
|
+
Alba.enable_inference!(with: :active_support)
|
117
110
|
```
|
118
111
|
|
112
|
+
You can choose which inflector Alba uses for inference. Possible value for `with` option are:
|
113
|
+
|
114
|
+
- `:active_support` for `ActiveSupport::Inflector`
|
115
|
+
- `:dry` for `Dry::Inflector`
|
116
|
+
- any object which responds to some methods (see [below](#custom-inflector))
|
117
|
+
|
119
118
|
For the details, see [Error handling section](#error-handling)
|
120
119
|
|
121
|
-
### Simple serialization with key
|
120
|
+
### Simple serialization with root key
|
121
|
+
|
122
|
+
You can define attributes with (yes) `attributes` macro with attribute names. If your attribute need some calculations, you can use `attribute` with block.
|
122
123
|
|
123
124
|
```ruby
|
124
125
|
class User
|
@@ -135,7 +136,7 @@ end
|
|
135
136
|
class UserResource
|
136
137
|
include Alba::Resource
|
137
138
|
|
138
|
-
|
139
|
+
root_key :user
|
139
140
|
|
140
141
|
attributes :id, :name
|
141
142
|
|
@@ -146,7 +147,34 @@ end
|
|
146
147
|
|
147
148
|
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
148
149
|
UserResource.new(user).serialize
|
149
|
-
# => "{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}"
|
150
|
+
# => "{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}}"
|
151
|
+
```
|
152
|
+
|
153
|
+
You can define instance methods on resources so that you can use it as attribute name in `attributes`.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# The serialization result is the same as above
|
157
|
+
class UserResource
|
158
|
+
include Alba::Resource
|
159
|
+
|
160
|
+
root_key :user, :users # Later is for plural
|
161
|
+
|
162
|
+
attributes :id, :name, :name_with_email
|
163
|
+
|
164
|
+
# Attribute methods must accept one argument for each serialized object
|
165
|
+
def name_with_email(user)
|
166
|
+
"#{user.name}: #{user.email}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
This even works with users collection.
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
user1 = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
175
|
+
user2 = User.new(2, 'Test User', 'test@example.com')
|
176
|
+
UserResource.new([user1, user2]).serialize
|
177
|
+
# => "{\"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\"}]}"
|
150
178
|
```
|
151
179
|
|
152
180
|
### Serialization with associations
|
@@ -198,12 +226,98 @@ UserResource.new(user).serialize
|
|
198
226
|
# => '{"id":1,"articles":[{"title":"Hello World!"},{"title":"Super nice"}]}'
|
199
227
|
```
|
200
228
|
|
229
|
+
You can define associations inline if you don't need a class for association.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
class ArticleResource
|
233
|
+
include Alba::Resource
|
234
|
+
|
235
|
+
attributes :title
|
236
|
+
end
|
237
|
+
|
238
|
+
class UserResource
|
239
|
+
include Alba::Resource
|
240
|
+
|
241
|
+
attributes :id
|
242
|
+
|
243
|
+
many :articles, resource: ArticleResource
|
244
|
+
end
|
245
|
+
|
246
|
+
# This class works the same as `UserResource`
|
247
|
+
class AnotherUserResource
|
248
|
+
include Alba::Resource
|
249
|
+
|
250
|
+
attributes :id
|
251
|
+
|
252
|
+
many :articles do
|
253
|
+
attributes :title
|
254
|
+
end
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
You can "filter" association using second proc argument. This proc takes association object and `params`.
|
259
|
+
|
260
|
+
This feature is useful when you want to modify association, such as adding `includes` or `order` to ActiveRecord relations.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
class User
|
264
|
+
attr_reader :id
|
265
|
+
attr_accessor :articles
|
266
|
+
|
267
|
+
def initialize(id)
|
268
|
+
@id = id
|
269
|
+
@articles = []
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
class Article
|
274
|
+
attr_accessor :id, :title, :body
|
275
|
+
|
276
|
+
def initialize(id, title, body)
|
277
|
+
@id = id
|
278
|
+
@title = title
|
279
|
+
@body = body
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class ArticleResource
|
284
|
+
include Alba::Resource
|
285
|
+
|
286
|
+
attributes :title
|
287
|
+
end
|
288
|
+
|
289
|
+
class UserResource
|
290
|
+
include Alba::Resource
|
291
|
+
|
292
|
+
attributes :id
|
293
|
+
|
294
|
+
# Second proc works as a filter
|
295
|
+
many :articles,
|
296
|
+
proc { |articles, params|
|
297
|
+
filter = params[:filter] || :odd?
|
298
|
+
articles.select {|a| a.id.send(filter) }
|
299
|
+
},
|
300
|
+
resource: ArticleResource
|
301
|
+
end
|
302
|
+
|
303
|
+
user = User.new(1)
|
304
|
+
article1 = Article.new(1, 'Hello World!', 'Hello World!!!')
|
305
|
+
user.articles << article1
|
306
|
+
article2 = Article.new(2, 'Super nice', 'Really nice!')
|
307
|
+
user.articles << article2
|
308
|
+
|
309
|
+
UserResource.new(user).serialize
|
310
|
+
# => '{"id":1,"articles":[{"title":"Hello World!"}]}'
|
311
|
+
UserResource.new(user, params: {filter: :even?}).serialize
|
312
|
+
# => '{"id":1,"articles":[{"title":"Super nice"}]}'
|
313
|
+
```
|
314
|
+
|
201
315
|
### Inline definition with `Alba.serialize`
|
202
316
|
|
203
317
|
`Alba.serialize` method is a shortcut to define everything inline.
|
204
318
|
|
205
319
|
```ruby
|
206
|
-
Alba.serialize(user,
|
320
|
+
Alba.serialize(user, root_key: :foo) do
|
207
321
|
attributes :id
|
208
322
|
many :articles do
|
209
323
|
attributes :title, :body
|
@@ -212,11 +326,20 @@ end
|
|
212
326
|
# => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
|
213
327
|
```
|
214
328
|
|
329
|
+
`Alba.serialize` can be used when you don't know what kind of object you serialize. For example:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
Alba.serialize(something)
|
333
|
+
# => Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.
|
334
|
+
```
|
335
|
+
|
215
336
|
Although this might be useful sometimes, it's generally recommended to define a class for Resource.
|
216
337
|
|
217
|
-
### Inheritance and
|
338
|
+
### Inheritance and attributes filter
|
218
339
|
|
219
|
-
You can
|
340
|
+
You can filter out certain attributes by overriding `attributes` instance method. This is useful when you want to customize existing resource with inheritance.
|
341
|
+
|
342
|
+
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.
|
220
343
|
|
221
344
|
```ruby
|
222
345
|
class Foo
|
@@ -235,13 +358,14 @@ class GenericFooResource
|
|
235
358
|
attributes :id, :name, :body
|
236
359
|
end
|
237
360
|
|
238
|
-
class
|
239
|
-
|
361
|
+
class RestrictedFooResource < GenericFooResource
|
362
|
+
def attributes
|
363
|
+
super.select { |key, _| key.to_sym == :name }
|
364
|
+
end
|
240
365
|
end
|
241
366
|
|
242
|
-
|
367
|
+
RestrictedFooResource.new(foo).serialize
|
243
368
|
# => '{"name":"my foo"}'
|
244
|
-
end
|
245
369
|
```
|
246
370
|
|
247
371
|
### Key transformation
|
@@ -276,14 +400,22 @@ UserResourceCamel.new(user).serialize
|
|
276
400
|
# => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}'
|
277
401
|
```
|
278
402
|
|
403
|
+
Possible values for `transform_keys` argument are:
|
404
|
+
|
405
|
+
* `:camel` for CamelCase
|
406
|
+
* `:lower_camel` for lowerCamelCase
|
407
|
+
* `:dash` for dash-case
|
408
|
+
* `:snake` for snake_case
|
409
|
+
* `:none` for not transforming keys
|
410
|
+
|
279
411
|
You can also transform root key when:
|
280
412
|
|
281
413
|
* `Alba.enable_inference!` is called
|
282
|
-
* `
|
283
|
-
* `root` option of `transform_keys` is set to true
|
414
|
+
* `root_key!` is called in Resource class
|
415
|
+
* `root` option of `transform_keys` is set to true
|
284
416
|
|
285
417
|
```ruby
|
286
|
-
Alba.enable_inference!
|
418
|
+
Alba.enable_inference!(with: :active_support) # with :dry also works
|
287
419
|
|
288
420
|
class BankAccount
|
289
421
|
attr_reader :account_number
|
@@ -296,7 +428,7 @@ end
|
|
296
428
|
class BankAccountResource
|
297
429
|
include Alba::Resource
|
298
430
|
|
299
|
-
|
431
|
+
root_key!
|
300
432
|
|
301
433
|
attributes :account_number
|
302
434
|
transform_keys :dash, root: true
|
@@ -313,30 +445,29 @@ Supported transformation types are :camel, :lower_camel and :dash.
|
|
313
445
|
|
314
446
|
#### Custom inflector
|
315
447
|
|
316
|
-
A custom inflector can be plugged in as follows
|
317
|
-
|
318
|
-
Alba.inflector = MyCustomInflector
|
319
|
-
```
|
320
|
-
...and has to implement following interface (the parameter `key` is of type `String`):
|
448
|
+
A custom inflector can be plugged in as follows.
|
449
|
+
|
321
450
|
```ruby
|
322
|
-
module
|
323
|
-
|
324
|
-
|
451
|
+
module CustomInflector
|
452
|
+
module_function
|
453
|
+
|
454
|
+
def camelize(string)
|
325
455
|
end
|
326
456
|
|
327
|
-
def camelize_lower(
|
328
|
-
raise "Not implemented"
|
457
|
+
def camelize_lower(string)
|
329
458
|
end
|
330
459
|
|
331
|
-
def dasherize(
|
332
|
-
|
460
|
+
def dasherize(string)
|
461
|
+
end
|
462
|
+
|
463
|
+
def underscore(string)
|
464
|
+
end
|
465
|
+
|
466
|
+
def classify(string)
|
333
467
|
end
|
334
468
|
end
|
335
469
|
|
336
|
-
|
337
|
-
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:
|
338
|
-
```ruby
|
339
|
-
Alba.inflector = Dry::Inflector.new
|
470
|
+
Alba.enable_inference!(with: CustomInflector)
|
340
471
|
```
|
341
472
|
|
342
473
|
### Filtering attributes
|
@@ -402,12 +533,26 @@ user = User.new(1, nil, nil)
|
|
402
533
|
UserResource.new(user).serialize # => '{"id":1}'
|
403
534
|
```
|
404
535
|
|
536
|
+
### Default
|
537
|
+
|
538
|
+
Alba doesn't support default value for attributes, but it's easy to set a default value.
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
class FooResource
|
542
|
+
attribute :bar do |foo|
|
543
|
+
foo.bar || 'default bar'
|
544
|
+
end
|
545
|
+
end
|
546
|
+
```
|
547
|
+
|
548
|
+
We believe this is clearer than using some (not implemented yet) DSL such as `default` because there are some conditions where default values should be applied (`nil`, `blank?`, `empty?` etc.)
|
549
|
+
|
405
550
|
### Inference
|
406
551
|
|
407
552
|
After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
|
408
553
|
|
409
554
|
```ruby
|
410
|
-
Alba.enable_inference!
|
555
|
+
Alba.enable_inference!(with: :active_support) # with :dry also works
|
411
556
|
|
412
557
|
class User
|
413
558
|
attr_reader :id
|
@@ -455,8 +600,6 @@ This resource automatically sets its root key to either "users" or "user", depen
|
|
455
600
|
|
456
601
|
Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
|
457
602
|
|
458
|
-
Note that to enable this feature you must install `ActiveSupport` gem.
|
459
|
-
|
460
603
|
### Error handling
|
461
604
|
|
462
605
|
You can set error handler globally or per resource using `on_error`.
|
@@ -500,16 +643,118 @@ There are four possible arguments `on_error` method accepts.
|
|
500
643
|
The block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example.
|
501
644
|
|
502
645
|
```ruby
|
503
|
-
|
504
|
-
Alba
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
646
|
+
class ExampleResource
|
647
|
+
include Alba::Resource
|
648
|
+
on_error do |error, object, key, attribute, resource_class|
|
649
|
+
if resource_class == MyResource
|
650
|
+
['error_fallback', object.error_fallback]
|
651
|
+
else
|
652
|
+
[key, error.message]
|
653
|
+
end
|
509
654
|
end
|
510
655
|
end
|
511
656
|
```
|
512
657
|
|
658
|
+
### Nil handling
|
659
|
+
|
660
|
+
Sometimes we want to convert `nil` to different values such as empty string. Alba provides a flexible way to handle `nil`.
|
661
|
+
|
662
|
+
```ruby
|
663
|
+
class User
|
664
|
+
attr_reader :id, :name, :age
|
665
|
+
|
666
|
+
def initialize(id, name = nil, age = nil)
|
667
|
+
@id = id
|
668
|
+
@name = name
|
669
|
+
@age = age
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
class UserResource
|
674
|
+
include Alba::Resource
|
675
|
+
|
676
|
+
on_nil { '' }
|
677
|
+
|
678
|
+
root_key :user, :users
|
679
|
+
|
680
|
+
attributes :id, :name, :age
|
681
|
+
end
|
682
|
+
|
683
|
+
UserResource.new(User.new(1)).serialize
|
684
|
+
# => '{"user":{"id":1,"name":"","age":""}}'
|
685
|
+
```
|
686
|
+
|
687
|
+
You can get various information via block parameters.
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
class UserResource
|
691
|
+
include Alba::Resource
|
692
|
+
|
693
|
+
on_nil do |object, key|
|
694
|
+
if key == age
|
695
|
+
20
|
696
|
+
else
|
697
|
+
"User#{object.id}"
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
root_key :user, :users
|
702
|
+
|
703
|
+
attributes :id, :name, :age
|
704
|
+
end
|
705
|
+
|
706
|
+
UserResource.new(User.new(1)).serialize
|
707
|
+
# => '{"user":{"id":1,"name":"User1","age":20}}'
|
708
|
+
```
|
709
|
+
|
710
|
+
### Metadata
|
711
|
+
|
712
|
+
You can set a metadata with `meta` DSL or `meta` option.
|
713
|
+
|
714
|
+
```ruby
|
715
|
+
class UserResource
|
716
|
+
include Alba::Resource
|
717
|
+
|
718
|
+
root_key :user, :users
|
719
|
+
|
720
|
+
attributes :id, :name
|
721
|
+
|
722
|
+
meta do
|
723
|
+
if object.is_a?(Enumerable)
|
724
|
+
{size: object.size}
|
725
|
+
else
|
726
|
+
{foo: :bar}
|
727
|
+
end
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
732
|
+
UserResource.new([user]).serialize
|
733
|
+
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1}}'
|
734
|
+
|
735
|
+
# You can merge metadata with `meta` option
|
736
|
+
|
737
|
+
UserResource.new([user]).serialize(meta: {foo: :bar})
|
738
|
+
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1,"foo":"bar"}}'
|
739
|
+
|
740
|
+
# You can set metadata with `meta` option alone
|
741
|
+
|
742
|
+
class UserResourceWithoutMeta
|
743
|
+
include Alba::Resource
|
744
|
+
|
745
|
+
root_key :user, :users
|
746
|
+
|
747
|
+
attributes :id, :name
|
748
|
+
end
|
749
|
+
|
750
|
+
UserResource.new([user]).serialize(meta: {foo: :bar})
|
751
|
+
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"foo":"bar"}}'
|
752
|
+
```
|
753
|
+
|
754
|
+
You can use `object` method to access the underlying object and `params` to access the params in `meta` block.
|
755
|
+
|
756
|
+
Note that setting root key is required when setting a metadata.
|
757
|
+
|
513
758
|
### Circular associations control
|
514
759
|
|
515
760
|
**Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**
|
@@ -559,10 +804,174 @@ UserResource.new(user).serialize
|
|
559
804
|
|
560
805
|
Note that this feature is experimental and interfaces are subject to change.
|
561
806
|
|
807
|
+
### Layout
|
808
|
+
|
809
|
+
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".
|
810
|
+
|
811
|
+
Alba supports serializing JSON in a layout. You need a file for layout and then to specify file with `layout` method.
|
812
|
+
|
813
|
+
```erb
|
814
|
+
{
|
815
|
+
"header": "my_header",
|
816
|
+
"body": <%= serialized_json %>
|
817
|
+
}
|
818
|
+
```
|
819
|
+
|
820
|
+
```ruby
|
821
|
+
class FooResource
|
822
|
+
include Alba::Resource
|
823
|
+
layout file: 'my_layout.json.erb'
|
824
|
+
end
|
825
|
+
```
|
826
|
+
|
827
|
+
Note that layout files are treated as `json` and `erb` and evaluated in a context of the resource, meaning
|
828
|
+
|
829
|
+
* A layout file must be a valid JSON
|
830
|
+
* You must write `<%= serialized_json %>` in a layout to put serialized JSON string into a layout
|
831
|
+
* You can access `params` in a layout so that you can add virtually any objects to a layout
|
832
|
+
* When you access `params`, it's usually a Hash. You can use `encode` method in a layout to convert `params` Hash into a JSON with the backend you use
|
833
|
+
* You can also access `object`, the underlying object for the resource
|
834
|
+
|
835
|
+
In case you don't want to have a file for layout, Alba lets you define and apply layouts inline:
|
836
|
+
|
837
|
+
```ruby
|
838
|
+
class FooResource
|
839
|
+
include Alba::Resource
|
840
|
+
layout inline: proc do
|
841
|
+
{
|
842
|
+
header: 'my header',
|
843
|
+
body: serializable_hash
|
844
|
+
}
|
845
|
+
end
|
846
|
+
end
|
847
|
+
```
|
848
|
+
|
849
|
+
In the example above, we specify a Proc which returns a Hash as an inline layout. In the Proc we can use `serializable_hash` method to access a Hash right before serialization.
|
850
|
+
|
851
|
+
You can also use a Proc which returns String, not a Hash, for an inline layout.
|
852
|
+
|
853
|
+
```ruby
|
854
|
+
class FooResource
|
855
|
+
include Alba::Resource
|
856
|
+
layout inline: proc do
|
857
|
+
%({
|
858
|
+
"header": "my header",
|
859
|
+
"body": #{serialized_json}
|
860
|
+
})
|
861
|
+
end
|
862
|
+
end
|
863
|
+
```
|
864
|
+
|
865
|
+
It looks similar to file layout but you must use string interpolation for method calls since it's not an ERB.
|
866
|
+
|
867
|
+
Also note that we use percentage notation here to use double quotes. Using single quotes in inline string layout causes the error which might be resolved in other ways.
|
868
|
+
|
562
869
|
### Caching
|
563
870
|
|
564
871
|
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).
|
565
872
|
|
873
|
+
### Extend Alba
|
874
|
+
|
875
|
+
Sometimes we have shared behaviors across resources. In such cases we can have a module for common logic.
|
876
|
+
|
877
|
+
In `attribute` block we can call instance method so we can improve the code below:
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
class FooResource
|
881
|
+
include Alba::Resource
|
882
|
+
# other attributes
|
883
|
+
attribute :created_at do |foo|
|
884
|
+
foo.created_at.strftime('%m/%d/%Y')
|
885
|
+
end
|
886
|
+
|
887
|
+
attribute :updated_at do |foo|
|
888
|
+
foo.updated_at.strftime('%m/%d/%Y')
|
889
|
+
end
|
890
|
+
end
|
891
|
+
|
892
|
+
class BarResource
|
893
|
+
include Alba::Resource
|
894
|
+
# other attributes
|
895
|
+
attribute :created_at do |bar|
|
896
|
+
bar.created_at.strftime('%m/%d/%Y')
|
897
|
+
end
|
898
|
+
|
899
|
+
attribute :updated_at do |bar|
|
900
|
+
bar.updated_at.strftime('%m/%d/%Y')
|
901
|
+
end
|
902
|
+
end
|
903
|
+
```
|
904
|
+
|
905
|
+
to:
|
906
|
+
|
907
|
+
```ruby
|
908
|
+
module SharedLogic
|
909
|
+
def format_time(time)
|
910
|
+
time.strftime('%m/%d/%Y')
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
class FooResource
|
915
|
+
include Alba::Resource
|
916
|
+
include SharedLogic
|
917
|
+
# other attributes
|
918
|
+
attribute :created_at do |foo|
|
919
|
+
format_time(foo.created_at)
|
920
|
+
end
|
921
|
+
|
922
|
+
attribute :updated_at do |foo|
|
923
|
+
format_time(foo.updated_at)
|
924
|
+
end
|
925
|
+
end
|
926
|
+
|
927
|
+
class BarResource
|
928
|
+
include Alba::Resource
|
929
|
+
include SharedLogic
|
930
|
+
# other attributes
|
931
|
+
attribute :created_at do |bar|
|
932
|
+
format_time(bar.created_at)
|
933
|
+
end
|
934
|
+
|
935
|
+
attribute :updated_at do |bar|
|
936
|
+
format_time(bar.updated_at)
|
937
|
+
end
|
938
|
+
end
|
939
|
+
```
|
940
|
+
|
941
|
+
We can even add our own DSL to serialize attributes for readability and removing code duplications.
|
942
|
+
|
943
|
+
To do so, we need to `extend` our module. Let's see how we can achieve the same goal with this approach.
|
944
|
+
|
945
|
+
```ruby
|
946
|
+
module AlbaExtension
|
947
|
+
# Here attrs are an Array of Symbol
|
948
|
+
def formatted_time_attributes(*attrs)
|
949
|
+
attrs.each do |attr|
|
950
|
+
attribute attr do |object|
|
951
|
+
time = object.send(attr)
|
952
|
+
time.strftime('%m/%d/%Y')
|
953
|
+
end
|
954
|
+
end
|
955
|
+
end
|
956
|
+
end
|
957
|
+
|
958
|
+
class FooResource
|
959
|
+
include Alba::Resource
|
960
|
+
extend AlbaExtension
|
961
|
+
# other attributes
|
962
|
+
formatted_time_attributes :created_at, :updated_at
|
963
|
+
end
|
964
|
+
|
965
|
+
class BarResource
|
966
|
+
include Alba::Resource
|
967
|
+
extend AlbaExtension
|
968
|
+
# other attributes
|
969
|
+
formatted_time_attributes :created_at, :updated_at
|
970
|
+
end
|
971
|
+
```
|
972
|
+
|
973
|
+
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.
|
974
|
+
|
566
975
|
## Rails
|
567
976
|
|
568
977
|
When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.
|
data/alba.gemspec
CHANGED
@@ -12,9 +12,13 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.license = 'MIT'
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
14
14
|
|
15
|
-
spec.metadata
|
16
|
-
|
17
|
-
|
15
|
+
spec.metadata = {
|
16
|
+
'bug_tracker_uri' => 'https://github.com/okuramasafumi/issues',
|
17
|
+
'changelog_uri' => 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md',
|
18
|
+
'documentation_uri' => 'https://rubydoc.info/github/okuramasafumi/alba',
|
19
|
+
'source_code_uri' => 'https://github.com/okuramasafumi/alba',
|
20
|
+
'rubygems_mfa_required' => 'true'
|
21
|
+
}
|
18
22
|
|
19
23
|
# Specify which files should be added to the gem when it is released.
|
20
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|