alba 1.6.0 → 2.0.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/.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
|