alba 1.6.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 +4 -4
- data/.github/workflows/main.yml +4 -6
- data/.github/workflows/perf.yml +2 -2
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +24 -0
- data/CONTRIBUTING.md +30 -0
- data/Gemfile +6 -2
- data/HACKING.md +41 -0
- data/README.md +381 -58
- data/Rakefile +2 -2
- data/alba.gemspec +1 -1
- data/benchmark/README.md +81 -0
- data/benchmark/collection.rb +0 -70
- data/docs/migrate_from_jbuilder.md +18 -4
- data/docs/rails.md +44 -0
- data/lib/alba/association.rb +25 -5
- data/lib/alba/conditional_attribute.rb +54 -0
- data/lib/alba/default_inflector.rb +10 -39
- data/lib/alba/layout.rb +67 -0
- data/lib/alba/nested_attribute.rb +18 -0
- data/lib/alba/resource.rb +201 -173
- data/lib/alba/typed_attribute.rb +1 -1
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +13 -56
- data/logo/alba-card.png +0 -0
- data/logo/alba-sign.png +0 -0
- data/logo/alba-typography.png +0 -0
- metadata +15 -6
- data/gemfiles/all.gemfile +0 -20
- data/sider.yml +0 -60
data/benchmark/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
## Benchmark for json serializers
|
2
|
+
|
3
|
+
This directory contains a few different benchmark scripts. They all use inline Bundler definitions so you can run them by `ruby benchmark/collection.rb` for instance.
|
4
|
+
|
5
|
+
## Result
|
6
|
+
|
7
|
+
As a reference, here's the benchmark result run in my (@okuramasafumi) machine.
|
8
|
+
|
9
|
+
Machine spec:
|
10
|
+
|
11
|
+
|Key|Value|
|
12
|
+
|---|---|
|
13
|
+
|OS|macOS 12.2.1|
|
14
|
+
|CPU|Intel Corei7 Quad Core 2.3Ghz|
|
15
|
+
|RAM|32GB|
|
16
|
+
|Ruby|ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]|
|
17
|
+
|
18
|
+
`benchmark-ips` with `Oj.optimize_rails`:
|
19
|
+
|
20
|
+
```
|
21
|
+
Comparison:
|
22
|
+
panko: 267.6 i/s
|
23
|
+
rails: 111.2 i/s - 2.41x (± 0.00) slower
|
24
|
+
jserializer: 106.2 i/s - 2.52x (± 0.00) slower
|
25
|
+
alba: 102.8 i/s - 2.60x (± 0.00) slower
|
26
|
+
turbostreamer: 99.9 i/s - 2.68x (± 0.00) slower
|
27
|
+
jbuilder: 90.7 i/s - 2.95x (± 0.00) slower
|
28
|
+
alba_inline: 90.0 i/s - 2.97x (± 0.00) slower
|
29
|
+
primalize: 82.1 i/s - 3.26x (± 0.00) slower
|
30
|
+
fast_serializer: 62.7 i/s - 4.27x (± 0.00) slower
|
31
|
+
jsonapi_same_format: 59.5 i/s - 4.50x (± 0.00) slower
|
32
|
+
jsonapi: 55.3 i/s - 4.84x (± 0.00) slower
|
33
|
+
blueprinter: 54.1 i/s - 4.95x (± 0.00) slower
|
34
|
+
representable: 35.9 i/s - 7.46x (± 0.00) slower
|
35
|
+
simple_ams: 25.4 i/s - 10.53x (± 0.00) slower
|
36
|
+
ams: 9.1 i/s - 29.39x (± 0.00) slower
|
37
|
+
```
|
38
|
+
|
39
|
+
`benchmark-ips` without `Oj.optimize_rails`:
|
40
|
+
|
41
|
+
```
|
42
|
+
Comparison:
|
43
|
+
panko: 283.8 i/s
|
44
|
+
turbostreamer: 102.9 i/s - 2.76x (± 0.00) slower
|
45
|
+
alba: 102.4 i/s - 2.77x (± 0.00) slower
|
46
|
+
alba_inline: 98.7 i/s - 2.87x (± 0.00) slower
|
47
|
+
jserializer: 93.3 i/s - 3.04x (± 0.00) slower
|
48
|
+
fast_serializer: 60.1 i/s - 4.73x (± 0.00) slower
|
49
|
+
blueprinter: 53.8 i/s - 5.28x (± 0.00) slower
|
50
|
+
rails: 37.1 i/s - 7.65x (± 0.00) slower
|
51
|
+
jbuilder: 37.1 i/s - 7.66x (± 0.00) slower
|
52
|
+
primalize: 31.2 i/s - 9.08x (± 0.00) slower
|
53
|
+
jsonapi_same_format: 28.3 i/s - 10.03x (± 0.00) slower
|
54
|
+
representable: 27.8 i/s - 10.23x (± 0.00) slower
|
55
|
+
jsonapi: 27.5 i/s - 10.34x (± 0.00) slower
|
56
|
+
simple_ams: 16.9 i/s - 16.75x (± 0.00) slower
|
57
|
+
ams: 8.3 i/s - 34.36x (± 0.00) slower
|
58
|
+
```
|
59
|
+
|
60
|
+
`benchmark-memory`:
|
61
|
+
|
62
|
+
```
|
63
|
+
Comparison:
|
64
|
+
panko: 230418 allocated
|
65
|
+
alba: 733217 allocated - 3.18x more
|
66
|
+
alba_inline: 748297 allocated - 3.25x more
|
67
|
+
turbostreamer: 781008 allocated - 3.39x more
|
68
|
+
jserializer: 819705 allocated - 3.56x more
|
69
|
+
primalize: 1195163 allocated - 5.19x more
|
70
|
+
fast_serializer: 1232385 allocated - 5.35x more
|
71
|
+
rails: 1236761 allocated - 5.37x more
|
72
|
+
blueprinter: 1588937 allocated - 6.90x more
|
73
|
+
jbuilder: 1774157 allocated - 7.70x more
|
74
|
+
jsonapi_same_format: 2132489 allocated - 9.25x more
|
75
|
+
jsonapi: 2279958 allocated - 9.89x more
|
76
|
+
representable: 2869166 allocated - 12.45x more
|
77
|
+
ams: 4473161 allocated - 19.41x more
|
78
|
+
simple_ams: 7868345 allocated - 34.15x more
|
79
|
+
```
|
80
|
+
|
81
|
+
Conclusion: panko is extremely fast but it's a C extension gem. As pure Ruby gems, Alba, turbostreamer and jserializer are notably faster than others. With `Oj.optimize_rails` jbuilder and Rails standard serialization are also fast.
|
data/benchmark/collection.rb
CHANGED
@@ -19,7 +19,6 @@ gemfile(true) do
|
|
19
19
|
gem "jbuilder"
|
20
20
|
gem 'turbostreamer'
|
21
21
|
gem "jserializer"
|
22
|
-
gem "jsonapi-serializer" # successor of fast_jsonapi
|
23
22
|
gem "multi_json"
|
24
23
|
gem "panko_serializer"
|
25
24
|
gem "pg"
|
@@ -201,67 +200,6 @@ class JserializerPostSerializer < Jserializer::Base
|
|
201
200
|
end
|
202
201
|
end
|
203
202
|
|
204
|
-
# --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
|
205
|
-
|
206
|
-
class JsonApiStandardCommentSerializer
|
207
|
-
include JSONAPI::Serializer
|
208
|
-
|
209
|
-
attribute :id
|
210
|
-
attribute :body
|
211
|
-
end
|
212
|
-
|
213
|
-
class JsonApiStandardPostSerializer
|
214
|
-
include JSONAPI::Serializer
|
215
|
-
|
216
|
-
# set_type :post # optional
|
217
|
-
attribute :id
|
218
|
-
attribute :body
|
219
|
-
attribute :commenter_names
|
220
|
-
|
221
|
-
attribute :comments do |post|
|
222
|
-
post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
# --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
|
227
|
-
|
228
|
-
# code to convert from JSON:API output to "flat" JSON, like the other serializers build
|
229
|
-
class JsonApiSameFormatSerializer
|
230
|
-
include JSONAPI::Serializer
|
231
|
-
|
232
|
-
def as_json(*_options)
|
233
|
-
hash = serializable_hash
|
234
|
-
|
235
|
-
if hash[:data].is_a? Hash
|
236
|
-
hash[:data][:attributes]
|
237
|
-
|
238
|
-
elsif hash[:data].is_a? Array
|
239
|
-
hash[:data].pluck(:attributes)
|
240
|
-
|
241
|
-
elsif hash[:data].nil?
|
242
|
-
{ }
|
243
|
-
|
244
|
-
else
|
245
|
-
raise "unexpected data type #{hash[:data].class}"
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
|
251
|
-
attribute :id
|
252
|
-
attribute :body
|
253
|
-
end
|
254
|
-
|
255
|
-
class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
|
256
|
-
attribute :id
|
257
|
-
attribute :body
|
258
|
-
attribute :commenter_names
|
259
|
-
|
260
|
-
attribute :comments do |post|
|
261
|
-
post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
203
|
# --- Panko serializers ---
|
266
204
|
#
|
267
205
|
|
@@ -419,8 +357,6 @@ jbuilder = Proc.new do
|
|
419
357
|
end.target!
|
420
358
|
end
|
421
359
|
jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
|
422
|
-
jsonapi = proc { JsonApiStandardPostSerializer.new(posts).to_json }
|
423
|
-
jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(posts).to_json }
|
424
360
|
panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
|
425
361
|
primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
|
426
362
|
rails = Proc.new do
|
@@ -441,8 +377,6 @@ puts "Serializer outputs ----------------------------------"
|
|
441
377
|
fast_serializer: fast_serializer,
|
442
378
|
jbuilder: jbuilder, # different order
|
443
379
|
jserializer: jserializer,
|
444
|
-
jsonapi: jsonapi, # nested JSON:API format
|
445
|
-
jsonapi_same_format: jsonapi_same_format,
|
446
380
|
panko: panko,
|
447
381
|
primalize: primalize,
|
448
382
|
rails: rails,
|
@@ -462,8 +396,6 @@ Benchmark.ips do |x|
|
|
462
396
|
x.report(:fast_serializer, &fast_serializer)
|
463
397
|
x.report(:jbuilder, &jbuilder)
|
464
398
|
x.report(:jserializer, &jserializer)
|
465
|
-
x.report(:jsonapi, &jsonapi)
|
466
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
467
399
|
x.report(:panko, &panko)
|
468
400
|
x.report(:primalize, &primalize)
|
469
401
|
x.report(:rails, &rails)
|
@@ -484,8 +416,6 @@ Benchmark.memory do |x|
|
|
484
416
|
x.report(:fast_serializer, &fast_serializer)
|
485
417
|
x.report(:jbuilder, &jbuilder)
|
486
418
|
x.report(:jserializer, &jserializer)
|
487
|
-
x.report(:jsonapi, &jsonapi)
|
488
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
489
419
|
x.report(:panko, &panko)
|
490
420
|
x.report(:primalize, &primalize)
|
491
421
|
x.report(:rails, &rails)
|
@@ -4,11 +4,12 @@ title: Upgrading from Jbuilder
|
|
4
4
|
|
5
5
|
<!-- @format -->
|
6
6
|
|
7
|
-
This guide is aimed at helping Jbuilder users transition to Alba, and it consists of
|
7
|
+
This guide is aimed at helping Jbuilder users transition to Alba, and it consists of four parts:
|
8
8
|
|
9
9
|
1. Basic serialization
|
10
10
|
2. Complex serialization
|
11
|
-
3.
|
11
|
+
3. Key transformation
|
12
|
+
4. Unsupported features
|
12
13
|
|
13
14
|
## Example class
|
14
15
|
|
@@ -216,8 +217,21 @@ UserResource.new(user).serialize
|
|
216
217
|
# => '{"user":{"id":id, "created_at": created_at, "updated_at": updated_at, "profile": {"email": email}, articles: [{"title": title, "body": body}]}'
|
217
218
|
```
|
218
219
|
|
219
|
-
## 3.
|
220
|
+
## 3. Key transformation
|
221
|
+
|
222
|
+
See the README for more information, but it's possible to migrate the `Jbuilder.key_format!` behavior with the `transform_keys` macro.
|
223
|
+
|
224
|
+
```rb
|
225
|
+
class UserResource
|
226
|
+
include Alba::Resource
|
227
|
+
root_key :user
|
228
|
+
attributes :id, :created_at, :updated_at
|
229
|
+
|
230
|
+
transform_keys :lower_camel
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
## 4. Unsupported features
|
220
235
|
|
221
236
|
- Jbuilder#ignore_nil!
|
222
237
|
- Jbuilder#cache!
|
223
|
-
- Jbuilder.key_format! and Jbuilder.deep_format_keys!
|
data/docs/rails.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
title: Alba for Rails
|
3
|
+
author: OKURA Masafumi
|
4
|
+
---
|
5
|
+
|
6
|
+
# Alba for Rails
|
7
|
+
|
8
|
+
While Alba is NOT designed for Rails specifically, you can definitely use Alba with Rails. This document describes in detail how to use Alba with Rails to be more productive.
|
9
|
+
|
10
|
+
## Initializer
|
11
|
+
|
12
|
+
You might want to add some configurations to initializer file such as `alba.rb` with something like below:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# alba.rb
|
16
|
+
Alba.backend = :active_support
|
17
|
+
Alba.enable_inference!(:active_support)
|
18
|
+
```
|
19
|
+
|
20
|
+
You can also use `:oj_rails` for backend if you prefer using Oj.
|
21
|
+
|
22
|
+
## Rendering JSON
|
23
|
+
|
24
|
+
You can render JSON with Rails in two ways. One way is to pass JSON String.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
render json: FooResource.new(foo).serialize
|
28
|
+
```
|
29
|
+
|
30
|
+
But you can also render JSON passing `Alba::Resource` object. Rails automatically calls `to_json` on a resource.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
render json: FooResource.new(foo)
|
34
|
+
```
|
35
|
+
|
36
|
+
Note that almost all options given to this `render` are ignored. The only exceptions are `layout`, `prefixes`, `template` and `status`.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# This `only` option is ignored
|
40
|
+
render json: FooResource.new(foo), only: [:id]
|
41
|
+
|
42
|
+
# This is OK
|
43
|
+
render json: FooResource.new(foo), status: 200
|
44
|
+
```
|
data/lib/alba/association.rb
CHANGED
@@ -11,12 +11,16 @@ module Alba
|
|
11
11
|
# @param name [Symbol, String] name of the method to fetch association
|
12
12
|
# @param condition [Proc, nil] a proc filtering data
|
13
13
|
# @param resource [Class<Alba::Resource>, nil] a resource class for the association
|
14
|
+
# @param params [Hash] params override for the association
|
14
15
|
# @param nesting [String] a namespace where source class is inferred with
|
16
|
+
# @param key_transformation [Symbol] key transformation type
|
15
17
|
# @param block [Block] used to define resource when resource arg is absent
|
16
|
-
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
18
|
+
def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, key_transformation: :none, &block)
|
17
19
|
@name = name
|
18
20
|
@condition = condition
|
19
21
|
@resource = resource
|
22
|
+
@params = params
|
23
|
+
@key_transformation = key_transformation
|
20
24
|
return if @resource
|
21
25
|
|
22
26
|
assign_resource(nesting, block)
|
@@ -29,12 +33,16 @@ module Alba
|
|
29
33
|
# @param params [Hash] user-given Hash for arbitrary data
|
30
34
|
# @return [Hash]
|
31
35
|
def to_h(target, within: nil, params: {})
|
32
|
-
|
33
|
-
@object =
|
36
|
+
params = params.merge(@params) unless @params.empty?
|
37
|
+
@object = target.__send__(@name)
|
38
|
+
@object = @condition.call(object, params, target) if @condition
|
34
39
|
return if @object.nil?
|
35
40
|
|
36
|
-
@resource
|
37
|
-
|
41
|
+
if @resource.is_a?(Proc) && @object.is_a?(Enumerable)
|
42
|
+
to_h_with_each_resource(within, params)
|
43
|
+
else
|
44
|
+
to_h_with_constantize_resource(within, params)
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
48
|
private
|
@@ -59,5 +67,17 @@ module Alba
|
|
59
67
|
raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
|
60
68
|
end
|
61
69
|
end
|
70
|
+
|
71
|
+
def to_h_with_each_resource(within, params)
|
72
|
+
@object.map do |item|
|
73
|
+
@resource.call(item).new(item, within: within, params: params).to_h
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_h_with_constantize_resource(within, params)
|
78
|
+
@resource = constantize(@resource)
|
79
|
+
@resource.transform_keys(@key_transformation)
|
80
|
+
@resource.new(object, params: params, within: within).to_h
|
81
|
+
end
|
62
82
|
end
|
63
83
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Alba
|
2
|
+
# Represents attribute with `if` option
|
3
|
+
class ConditionalAttribute
|
4
|
+
CONDITION_UNMET = Object.new.freeze
|
5
|
+
public_constant :CONDITION_UNMET # It's public for use in `Alba::Resource`
|
6
|
+
|
7
|
+
# @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
|
8
|
+
# @param condition [Symbol, Proc] condition to check
|
9
|
+
def initialize(body:, condition:)
|
10
|
+
@body = body
|
11
|
+
@condition = condition
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns attribute body if condition passes
|
15
|
+
#
|
16
|
+
# @param resource [Alba::Resource]
|
17
|
+
# @param object [Object] needed for collection, each object from collection
|
18
|
+
# @return [ConditionalAttribute::CONDITION_UNMET, Object] CONDITION_UNMET if condition is unmet, fetched attribute otherwise
|
19
|
+
def with_passing_condition(resource:, object: nil)
|
20
|
+
return CONDITION_UNMET unless condition_passes?(resource, object)
|
21
|
+
|
22
|
+
fetched_attribute = yield(@body)
|
23
|
+
return fetched_attribute if fetched_attribute.nil? || !with_two_arity_proc_condition
|
24
|
+
|
25
|
+
return CONDITION_UNMET unless resource.instance_exec(object, attribute_from_association_body_or(fetched_attribute), &@condition)
|
26
|
+
|
27
|
+
fetched_attribute
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def condition_passes?(resource, object)
|
33
|
+
if @condition.is_a?(Proc)
|
34
|
+
arity = @condition.arity
|
35
|
+
# We can return early to skip fetch_attribute if arity is 1
|
36
|
+
# When arity is 2, we check the condition later
|
37
|
+
return true if arity >= 2
|
38
|
+
return false if arity <= 1 && !resource.instance_exec(object, &@condition)
|
39
|
+
|
40
|
+
true
|
41
|
+
else # Symbol
|
42
|
+
resource.__send__(@condition)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_two_arity_proc_condition
|
47
|
+
@condition.is_a?(Proc) && @condition.arity >= 2
|
48
|
+
end
|
49
|
+
|
50
|
+
def attribute_from_association_body_or(fetched_attribute)
|
51
|
+
@body.is_a?(Alba::Association) ? @body.object : fetched_attribute
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -1,54 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
rescue LoadError
|
5
|
+
raise ::Alba::Error, 'To use default inflector, please install `ActiveSupport` gem.'
|
6
|
+
end
|
7
|
+
|
1
8
|
module Alba
|
2
9
|
# This module has two purposes.
|
3
10
|
# One is that we require `active_support/inflector` in this module so that we don't do that all over the place.
|
4
11
|
# Another is that `ActiveSupport::Inflector` doesn't have `camelize_lower` method that we want it to have, so this module works as an adapter.
|
5
12
|
module DefaultInflector
|
6
|
-
|
7
|
-
|
8
|
-
rescue LoadError
|
9
|
-
raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
|
10
|
-
end
|
11
|
-
|
12
|
-
module_function
|
13
|
-
|
14
|
-
# Camelizes a key
|
15
|
-
#
|
16
|
-
# @param key [String] key to be camelized
|
17
|
-
# @return [String] camelized key
|
18
|
-
def camelize(key)
|
19
|
-
ActiveSupport::Inflector.camelize(key)
|
13
|
+
class << self
|
14
|
+
delegate :camelize, :dasherize, :underscore, :classify, :demodulize, :pluralize, to: ActiveSupport::Inflector
|
20
15
|
end
|
21
16
|
|
22
17
|
# Camelizes a key, 1st letter lowercase
|
23
18
|
#
|
24
19
|
# @param key [String] key to be camelized
|
25
20
|
# @return [String] camelized key
|
26
|
-
def camelize_lower(key)
|
21
|
+
def self.camelize_lower(key)
|
27
22
|
ActiveSupport::Inflector.camelize(key, false)
|
28
23
|
end
|
29
|
-
|
30
|
-
# Dasherizes a key
|
31
|
-
#
|
32
|
-
# @param key [String] key to be dasherized
|
33
|
-
# @return [String] dasherized key
|
34
|
-
def dasherize(key)
|
35
|
-
ActiveSupport::Inflector.dasherize(key)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Underscore a key
|
39
|
-
#
|
40
|
-
# @param key [String] key to be underscore
|
41
|
-
# @return [String] underscored key
|
42
|
-
def underscore(key)
|
43
|
-
ActiveSupport::Inflector.underscore(key)
|
44
|
-
end
|
45
|
-
|
46
|
-
# Classify a key
|
47
|
-
#
|
48
|
-
# @param key [String] key to be classified
|
49
|
-
# @return [String] classified key
|
50
|
-
def classify(key)
|
51
|
-
ActiveSupport::Inflector.classify(key)
|
52
|
-
end
|
53
24
|
end
|
54
25
|
end
|
data/lib/alba/layout.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Alba
|
5
|
+
# Layout serialization
|
6
|
+
class Layout
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@resource, :object, :params, :serializable_hash, :to_h
|
10
|
+
|
11
|
+
# @params file [String] name of the layout file
|
12
|
+
# @params inline [Proc] a proc returning JSON string or a Hash representing JSON
|
13
|
+
def initialize(file:, inline:)
|
14
|
+
if file
|
15
|
+
raise ArgumentError, 'File layout must be a String representing filename' unless file.is_a?(String)
|
16
|
+
|
17
|
+
@body = file
|
18
|
+
elsif inline
|
19
|
+
raise ArgumentError, 'Inline layout must be a Proc returning a Hash or a String' unless inline.is_a?(Proc)
|
20
|
+
|
21
|
+
@body = inline
|
22
|
+
else
|
23
|
+
raise ArgumentError, 'Layout must be either String or Proc'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Serialize within layout
|
28
|
+
#
|
29
|
+
# @param resource [Alba::Resource] the original resource calling this layout
|
30
|
+
# @param serialized_json [String] JSON string for embedding
|
31
|
+
# @param binding [Binding] context for serialization
|
32
|
+
def serialize(resource:, serialized_json:, binding:)
|
33
|
+
@resource = resource
|
34
|
+
@serialized_json = serialized_json
|
35
|
+
|
36
|
+
if @body.is_a?(String)
|
37
|
+
serialize_within_string_layout(binding)
|
38
|
+
else
|
39
|
+
serialize_within_inline_layout
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :serialized_json
|
46
|
+
|
47
|
+
def serialize_within_string_layout(bnd)
|
48
|
+
ERB.new(File.read(@body)).result(bnd)
|
49
|
+
end
|
50
|
+
|
51
|
+
def serialize_within_inline_layout
|
52
|
+
inline = instance_eval(&@body)
|
53
|
+
case inline
|
54
|
+
when Hash then encode(inline)
|
55
|
+
when String then inline
|
56
|
+
else
|
57
|
+
raise Alba::Error, 'Inline layout must be a Proc returning a Hash or a String'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# This methods exists here instead of delegation because
|
62
|
+
# `Alba::Resource#encode` is private and it prints warning if we use `def_delegators`
|
63
|
+
def encode(hash)
|
64
|
+
@resource.instance_eval { encode(hash) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Alba
|
2
|
+
# Representing nested attribute
|
3
|
+
class NestedAttribute
|
4
|
+
# @param key_transformation [Symbol] determines how to transform keys
|
5
|
+
# @param block [Proc] class body
|
6
|
+
def initialize(key_transformation: :none, &block)
|
7
|
+
@key_transformation = key_transformation
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Hash]
|
12
|
+
def value(object)
|
13
|
+
resource_class = Alba.resource_class(&@block)
|
14
|
+
resource_class.transform_keys(@key_transformation)
|
15
|
+
resource_class.new(object).serializable_hash
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|