alba 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/main.yml +3 -1
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -2
- data/README.md +240 -92
- data/alba.gemspec +7 -3
- data/benchmark/collection.rb +60 -4
- data/benchmark/single_resource.rb +33 -2
- data/gemfiles/all.gemfile +1 -0
- data/lib/alba/association.rb +28 -8
- data/lib/alba/default_inflector.rb +19 -1
- data/lib/alba/errors.rb +10 -0
- data/lib/alba/resource.rb +117 -61
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +44 -16
- metadata +9 -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/benchmark/collection.rb
CHANGED
@@ -15,7 +15,9 @@ gemfile(true) do
|
|
15
15
|
gem "benchmark-ips"
|
16
16
|
gem "benchmark-memory"
|
17
17
|
gem "blueprinter"
|
18
|
+
gem "fast_serializer_ruby"
|
18
19
|
gem "jbuilder"
|
20
|
+
gem 'turbostreamer'
|
19
21
|
gem "jserializer"
|
20
22
|
gem "jsonapi-serializer" # successor of fast_jsonapi
|
21
23
|
gem "multi_json"
|
@@ -138,6 +140,27 @@ class PostBlueprint < Blueprinter::Base
|
|
138
140
|
end
|
139
141
|
end
|
140
142
|
|
143
|
+
# --- Fast Serializer Ruby
|
144
|
+
|
145
|
+
require "fast_serializer"
|
146
|
+
|
147
|
+
class FastSerializerCommentResource
|
148
|
+
include ::FastSerializer::Schema::Mixin
|
149
|
+
attributes :id, :body
|
150
|
+
end
|
151
|
+
|
152
|
+
class FastSerializerPostResource
|
153
|
+
include ::FastSerializer::Schema::Mixin
|
154
|
+
|
155
|
+
attributes :id, :body
|
156
|
+
|
157
|
+
attribute :commenter_names do
|
158
|
+
object.commenters.pluck(:name)
|
159
|
+
end
|
160
|
+
|
161
|
+
has_many :comments, serializer: FastSerializerCommentResource
|
162
|
+
end
|
163
|
+
|
141
164
|
# --- JBuilder serializers ---
|
142
165
|
|
143
166
|
require "jbuilder"
|
@@ -255,7 +278,7 @@ class PankoPostSerializer < Panko::Serializer
|
|
255
278
|
has_many :comments, serializer: PankoCommentSerializer
|
256
279
|
|
257
280
|
def commenter_names
|
258
|
-
object.
|
281
|
+
object.commenters.pluck(:name)
|
259
282
|
end
|
260
283
|
end
|
261
284
|
|
@@ -332,6 +355,31 @@ class SimpleAMSPostSerializer
|
|
332
355
|
end
|
333
356
|
end
|
334
357
|
|
358
|
+
require 'turbostreamer'
|
359
|
+
TurboStreamer.set_default_encoder(:json, :oj)
|
360
|
+
|
361
|
+
class TurbostreamerSerializer
|
362
|
+
def initialize(posts)
|
363
|
+
@posts = posts
|
364
|
+
end
|
365
|
+
|
366
|
+
def to_json
|
367
|
+
TurboStreamer.encode do |json|
|
368
|
+
json.array! @posts do |post|
|
369
|
+
json.object! do
|
370
|
+
json.extract! post, :id, :body, :commenter_names
|
371
|
+
|
372
|
+
json.comments post.comments do |comment|
|
373
|
+
json.object! do
|
374
|
+
json.extract! comment, :id, :body
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
335
383
|
# --- Test data creation ---
|
336
384
|
|
337
385
|
100.times do |i|
|
@@ -344,7 +392,7 @@ end
|
|
344
392
|
end
|
345
393
|
end
|
346
394
|
|
347
|
-
posts = Post.all.
|
395
|
+
posts = Post.all.includes(:comments, :commenters)
|
348
396
|
|
349
397
|
# --- Store the serializers in procs ---
|
350
398
|
|
@@ -360,8 +408,9 @@ alba_inline = Proc.new do
|
|
360
408
|
end
|
361
409
|
end
|
362
410
|
end
|
363
|
-
ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {}).
|
411
|
+
ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {each_serializer: AMSPostSerializer}).to_json }
|
364
412
|
blueprinter = Proc.new { PostBlueprint.render(posts) }
|
413
|
+
fast_serializer = Proc.new { FastSerializerPostResource.new(posts).to_json }
|
365
414
|
jbuilder = Proc.new do
|
366
415
|
Jbuilder.new do |json|
|
367
416
|
json.array!(posts) do |post|
|
@@ -379,15 +428,17 @@ rails = Proc.new do
|
|
379
428
|
end
|
380
429
|
representable = Proc.new { PostsRepresenter.new(posts).to_json }
|
381
430
|
simple_ams = Proc.new { SimpleAMS::Renderer::Collection.new(posts, serializer: SimpleAMSPostSerializer).to_json }
|
431
|
+
turbostreamer = Proc.new { TurbostreamerSerializer.new(posts).to_json }
|
382
432
|
|
383
433
|
# --- Execute the serializers to check their output ---
|
384
|
-
|
434
|
+
GC.disable
|
385
435
|
puts "Serializer outputs ----------------------------------"
|
386
436
|
{
|
387
437
|
alba: alba,
|
388
438
|
alba_inline: alba_inline,
|
389
439
|
ams: ams,
|
390
440
|
blueprinter: blueprinter,
|
441
|
+
fast_serializer: fast_serializer,
|
391
442
|
jbuilder: jbuilder, # different order
|
392
443
|
jserializer: jserializer,
|
393
444
|
jsonapi: jsonapi, # nested JSON:API format
|
@@ -397,6 +448,7 @@ puts "Serializer outputs ----------------------------------"
|
|
397
448
|
rails: rails,
|
398
449
|
representable: representable,
|
399
450
|
simple_ams: simple_ams,
|
451
|
+
turbostreamer: turbostreamer
|
400
452
|
}.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
|
401
453
|
|
402
454
|
# --- Run the benchmarks ---
|
@@ -407,6 +459,7 @@ Benchmark.ips do |x|
|
|
407
459
|
x.report(:alba_inline, &alba_inline)
|
408
460
|
x.report(:ams, &ams)
|
409
461
|
x.report(:blueprinter, &blueprinter)
|
462
|
+
x.report(:fast_serializer, &fast_serializer)
|
410
463
|
x.report(:jbuilder, &jbuilder)
|
411
464
|
x.report(:jserializer, &jserializer)
|
412
465
|
x.report(:jsonapi, &jsonapi)
|
@@ -416,6 +469,7 @@ Benchmark.ips do |x|
|
|
416
469
|
x.report(:rails, &rails)
|
417
470
|
x.report(:representable, &representable)
|
418
471
|
x.report(:simple_ams, &simple_ams)
|
472
|
+
x.report(:turbostreamer, &turbostreamer)
|
419
473
|
|
420
474
|
x.compare!
|
421
475
|
end
|
@@ -427,6 +481,7 @@ Benchmark.memory do |x|
|
|
427
481
|
x.report(:alba_inline, &alba_inline)
|
428
482
|
x.report(:ams, &ams)
|
429
483
|
x.report(:blueprinter, &blueprinter)
|
484
|
+
x.report(:fast_serializer, &fast_serializer)
|
430
485
|
x.report(:jbuilder, &jbuilder)
|
431
486
|
x.report(:jserializer, &jserializer)
|
432
487
|
x.report(:jsonapi, &jsonapi)
|
@@ -436,6 +491,7 @@ Benchmark.memory do |x|
|
|
436
491
|
x.report(:rails, &rails)
|
437
492
|
x.report(:representable, &representable)
|
438
493
|
x.report(:simple_ams, &simple_ams)
|
494
|
+
x.report(:turbostreamer, &turbostreamer)
|
439
495
|
|
440
496
|
x.compare!
|
441
497
|
end
|
@@ -16,6 +16,7 @@ gemfile(true) do
|
|
16
16
|
gem "benchmark-memory"
|
17
17
|
gem "blueprinter"
|
18
18
|
gem "jbuilder"
|
19
|
+
gem 'turbostreamer'
|
19
20
|
gem "jserializer"
|
20
21
|
gem "jsonapi-serializer" # successor of fast_jsonapi
|
21
22
|
gem "multi_json"
|
@@ -253,7 +254,7 @@ class PankoPostSerializer < Panko::Serializer
|
|
253
254
|
has_many :comments, serializer: PankoCommentSerializer
|
254
255
|
|
255
256
|
def commenter_names
|
256
|
-
object.
|
257
|
+
object.commenters.pluck(:name)
|
257
258
|
end
|
258
259
|
end
|
259
260
|
|
@@ -324,6 +325,29 @@ class SimpleAMSPostSerializer
|
|
324
325
|
end
|
325
326
|
end
|
326
327
|
|
328
|
+
require 'turbostreamer'
|
329
|
+
TurboStreamer.set_default_encoder(:json, :oj)
|
330
|
+
|
331
|
+
class TurbostreamerSerializer
|
332
|
+
def initialize(post)
|
333
|
+
@post = post
|
334
|
+
end
|
335
|
+
|
336
|
+
def to_json
|
337
|
+
TurboStreamer.encode do |json|
|
338
|
+
json.object! do
|
339
|
+
json.extract! @post, :id, :body, :commenter_names
|
340
|
+
|
341
|
+
json.comments @post.comments do |comment|
|
342
|
+
json.object! do
|
343
|
+
json.extract! comment, :id, :body
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
327
351
|
# --- Test data creation ---
|
328
352
|
|
329
353
|
post = Post.create!(body: 'post')
|
@@ -347,6 +371,7 @@ alba_inline = Proc.new do
|
|
347
371
|
end
|
348
372
|
end
|
349
373
|
end
|
374
|
+
|
350
375
|
ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
|
351
376
|
blueprinter = Proc.new { PostBlueprint.render(post) }
|
352
377
|
jbuilder = Proc.new { post.to_builder.target! }
|
@@ -358,6 +383,7 @@ primalize = proc { PrimalizePostResource.new(post).to_json }
|
|
358
383
|
rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
|
359
384
|
representable = Proc.new { PostRepresenter.new(post).to_json }
|
360
385
|
simple_ams = Proc.new { SimpleAMS::Renderer.new(post, serializer: SimpleAMSPostSerializer).to_json }
|
386
|
+
turbostreamer = Proc.new { TurbostreamerSerializer.new(post).to_json }
|
361
387
|
|
362
388
|
# --- Execute the serializers to check their output ---
|
363
389
|
|
@@ -376,7 +402,10 @@ puts "Serializer outputs ----------------------------------"
|
|
376
402
|
rails: rails,
|
377
403
|
representable: representable,
|
378
404
|
simple_ams: simple_ams,
|
379
|
-
|
405
|
+
turbostreamer: turbostreamer
|
406
|
+
}.each do |name, serializer|
|
407
|
+
puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}"
|
408
|
+
end
|
380
409
|
|
381
410
|
# --- Run the benchmarks ---
|
382
411
|
|
@@ -395,6 +424,7 @@ Benchmark.ips do |x|
|
|
395
424
|
x.report(:rails, &rails)
|
396
425
|
x.report(:representable, &representable)
|
397
426
|
x.report(:simple_ams, &simple_ams)
|
427
|
+
x.report(:turbostreamer, &turbostreamer)
|
398
428
|
|
399
429
|
x.compare!
|
400
430
|
end
|
@@ -415,6 +445,7 @@ Benchmark.memory do |x|
|
|
415
445
|
x.report(:rails, &rails)
|
416
446
|
x.report(:representable, &representable)
|
417
447
|
x.report(:simple_ams, &simple_ams)
|
448
|
+
x.report(:turbostreamer, &turbostreamer)
|
418
449
|
|
419
450
|
x.compare!
|
420
451
|
end
|
data/gemfiles/all.gemfile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
gem 'activesupport', require: false # For backend
|
4
|
+
gem 'dry-inflector', require: false # For inflector
|
4
5
|
gem 'ffaker', require: false # For testing
|
5
6
|
gem 'minitest', '~> 5.14' # For test
|
6
7
|
gem 'rake', '~> 13.0' # For test and automation
|
data/lib/alba/association.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
module Alba
|
2
|
-
#
|
3
|
-
# Child class should implement `to_hash` method
|
2
|
+
# Representing association
|
4
3
|
class Association
|
4
|
+
@const_cache = {}
|
5
|
+
class << self
|
6
|
+
attr_reader :const_cache
|
7
|
+
end
|
8
|
+
|
5
9
|
attr_reader :object, :name
|
6
10
|
|
7
11
|
# @param name [Symbol, String] name of the method to fetch association
|
@@ -12,11 +16,25 @@ module Alba
|
|
12
16
|
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
13
17
|
@name = name
|
14
18
|
@condition = condition
|
15
|
-
@block = block
|
16
19
|
@resource = resource
|
17
20
|
return if @resource
|
18
21
|
|
19
|
-
assign_resource(nesting)
|
22
|
+
assign_resource(nesting, block)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Recursively converts an object into a Hash
|
26
|
+
#
|
27
|
+
# @param target [Object] the object having an association method
|
28
|
+
# @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
|
29
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
30
|
+
# @return [Hash]
|
31
|
+
def to_h(target, within: nil, params: {})
|
32
|
+
@object = target.public_send(@name)
|
33
|
+
@object = @condition.call(object, params) if @condition
|
34
|
+
return if @object.nil?
|
35
|
+
|
36
|
+
@resource = constantize(@resource)
|
37
|
+
@resource.new(object, params: params, within: within).to_h
|
20
38
|
end
|
21
39
|
|
22
40
|
private
|
@@ -26,13 +44,15 @@ module Alba
|
|
26
44
|
when Class
|
27
45
|
resource
|
28
46
|
when Symbol, String
|
29
|
-
|
47
|
+
self.class.const_cache.fetch(resource) do
|
48
|
+
self.class.const_cache[resource] = Object.const_get(resource)
|
49
|
+
end
|
30
50
|
end
|
31
51
|
end
|
32
52
|
|
33
|
-
def assign_resource(nesting)
|
34
|
-
@resource = if
|
35
|
-
Alba.resource_class(
|
53
|
+
def assign_resource(nesting, block)
|
54
|
+
@resource = if block
|
55
|
+
Alba.resource_class(&block)
|
36
56
|
elsif Alba.inferring
|
37
57
|
Alba.infer_resource_class(@name, nesting: nesting)
|
38
58
|
else
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Alba
|
2
|
-
# This module
|
2
|
+
# This module has two purposes.
|
3
|
+
# One is that we require `active_support/inflector` in this module so that we don't do that all over the place.
|
4
|
+
# Another is that `ActiveSupport::Inflector` doesn't have `camelize_lower` method that we want it to have, so this module works as an adapter.
|
3
5
|
module DefaultInflector
|
4
6
|
begin
|
5
7
|
require 'active_support/inflector'
|
@@ -32,5 +34,21 @@ module Alba
|
|
32
34
|
def dasherize(key)
|
33
35
|
ActiveSupport::Inflector.dasherize(key)
|
34
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
|
35
53
|
end
|
36
54
|
end
|
data/lib/alba/errors.rb
ADDED
data/lib/alba/resource.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative 'many'
|
3
|
-
require_relative 'key_transform_factory'
|
1
|
+
require_relative 'association'
|
4
2
|
require_relative 'typed_attribute'
|
5
3
|
require_relative 'deprecation'
|
6
4
|
|
@@ -9,7 +7,7 @@ module Alba
|
|
9
7
|
module Resource
|
10
8
|
# @!parse include InstanceMethods
|
11
9
|
# @!parse extend ClassMethods
|
12
|
-
DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil,
|
10
|
+
DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _on_error: nil, _on_nil: nil, _layout: nil}.freeze # rubocop:disable Layout/LineLength
|
13
11
|
private_constant :DSLS
|
14
12
|
|
15
13
|
WITHIN_DEFAULT = Object.new.freeze
|
@@ -39,6 +37,7 @@ module Alba
|
|
39
37
|
@object = object
|
40
38
|
@params = params.freeze
|
41
39
|
@within = within
|
40
|
+
@method_existence = {} # Cache for `respond_to?` result
|
42
41
|
DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
|
43
42
|
end
|
44
43
|
|
@@ -67,7 +66,13 @@ module Alba
|
|
67
66
|
def serializable_hash
|
68
67
|
collection? ? @object.map(&converter) : converter.call(@object)
|
69
68
|
end
|
70
|
-
alias
|
69
|
+
alias to_h serializable_hash
|
70
|
+
|
71
|
+
# @deprecated Use {#serializable_hash} instead
|
72
|
+
def to_hash
|
73
|
+
warn '[DEPRECATION] `to_hash` is deprecated, use `serializable_hash` instead.'
|
74
|
+
serializable_hash
|
75
|
+
end
|
71
76
|
|
72
77
|
private
|
73
78
|
|
@@ -78,22 +83,33 @@ module Alba
|
|
78
83
|
end
|
79
84
|
|
80
85
|
def serialize_with(hash)
|
81
|
-
|
82
|
-
|
83
|
-
|
86
|
+
serialized_json = encode(hash)
|
87
|
+
return serialized_json unless @_layout
|
88
|
+
|
89
|
+
@serialized_json = serialized_json
|
90
|
+
if @_layout.is_a?(String) # file
|
84
91
|
ERB.new(File.read(@_layout)).result(binding)
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
92
|
+
|
93
|
+
else # inline
|
94
|
+
serialize_within_inline_layout
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialize_within_inline_layout
|
99
|
+
inline = instance_eval(&@_layout)
|
100
|
+
case inline
|
101
|
+
when Hash then encode(inline)
|
102
|
+
when String then inline
|
103
|
+
else
|
104
|
+
raise Alba::Error, 'Inline layout must be a Proc returning a Hash or a String'
|
90
105
|
end
|
91
106
|
end
|
92
107
|
|
93
108
|
def hash_with_metadata(hash, meta)
|
94
|
-
|
95
|
-
|
96
|
-
|
109
|
+
return hash if meta.empty? && @_meta.nil?
|
110
|
+
|
111
|
+
metadata = @_meta ? instance_eval(&@_meta).merge(meta) : meta
|
112
|
+
hash[:meta] = metadata
|
97
113
|
hash
|
98
114
|
end
|
99
115
|
|
@@ -116,25 +132,34 @@ module Alba
|
|
116
132
|
end
|
117
133
|
|
118
134
|
def resource_name
|
119
|
-
self.class.name.demodulize.delete_suffix('Resource').underscore
|
135
|
+
@resource_name ||= self.class.name.demodulize.delete_suffix('Resource').underscore
|
120
136
|
end
|
121
137
|
|
122
138
|
def transforming_root_key?
|
123
139
|
@_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
|
124
140
|
end
|
125
141
|
|
142
|
+
# rubocop:disable Metrics/MethodLength
|
126
143
|
def converter
|
127
144
|
lambda do |object|
|
128
|
-
arrays =
|
145
|
+
arrays = attributes.map do |key, attribute|
|
129
146
|
key_and_attribute_body_from(object, key, attribute)
|
130
147
|
rescue ::Alba::Error, FrozenError, TypeError
|
131
148
|
raise
|
132
149
|
rescue StandardError => e
|
133
150
|
handle_error(e, object, key, attribute)
|
134
151
|
end
|
135
|
-
arrays.
|
152
|
+
arrays.compact!
|
153
|
+
arrays.to_h
|
136
154
|
end
|
137
155
|
end
|
156
|
+
# rubocop:enable Metrics/MethodLength
|
157
|
+
|
158
|
+
# This is default behavior for getting attributes for serialization
|
159
|
+
# Override this method to filter certain attributes
|
160
|
+
def attributes
|
161
|
+
@_attributes
|
162
|
+
end
|
138
163
|
|
139
164
|
def key_and_attribute_body_from(object, key, attribute)
|
140
165
|
key = transform_key(key)
|
@@ -158,17 +183,17 @@ module Alba
|
|
158
183
|
def conditional_attribute_with_proc(object, key, attribute, condition)
|
159
184
|
arity = condition.arity
|
160
185
|
# We can return early to skip fetch_attribute
|
161
|
-
return
|
186
|
+
return if arity <= 1 && !instance_exec(object, &condition)
|
162
187
|
|
163
188
|
fetched_attribute = fetch_attribute(object, key, attribute)
|
164
189
|
attr = attribute.is_a?(Alba::Association) ? attribute.object : fetched_attribute
|
165
|
-
return
|
190
|
+
return if arity >= 2 && !instance_exec(object, attr, &condition)
|
166
191
|
|
167
192
|
[key, fetched_attribute]
|
168
193
|
end
|
169
194
|
|
170
195
|
def conditional_attribute_with_symbol(object, key, attribute, condition)
|
171
|
-
return
|
196
|
+
return unless __send__(condition)
|
172
197
|
|
173
198
|
[key, fetch_attribute(object, key, attribute)]
|
174
199
|
end
|
@@ -178,25 +203,39 @@ module Alba
|
|
178
203
|
case on_error
|
179
204
|
when :raise, nil then raise
|
180
205
|
when :nullify then [key, nil]
|
181
|
-
when :ignore then
|
206
|
+
when :ignore then nil
|
182
207
|
when Proc then on_error.call(error, object, key, attribute, self.class)
|
183
208
|
else
|
184
209
|
raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
|
185
210
|
end
|
186
211
|
end
|
187
212
|
|
188
|
-
#
|
213
|
+
# rubocop:disable Metrics/MethodLength
|
214
|
+
# @return [Symbol]
|
189
215
|
def transform_key(key)
|
190
|
-
return key if @
|
191
|
-
|
192
|
-
|
216
|
+
return key if @_transform_type == :none
|
217
|
+
|
218
|
+
key = key.to_s
|
219
|
+
# TODO: Using default inflector here is for backward compatibility
|
220
|
+
# From 2.0 it'll raise error when inflector is nil
|
221
|
+
inflector = Alba.inflector || begin
|
222
|
+
require_relative 'default_inflector'
|
223
|
+
Alba::DefaultInflector
|
224
|
+
end
|
225
|
+
case @_transform_type # rubocop:disable Style/MissingElse
|
226
|
+
when :camel then inflector.camelize(key)
|
227
|
+
when :lower_camel then inflector.camelize_lower(key)
|
228
|
+
when :dash then inflector.dasherize(key)
|
229
|
+
when :snake then inflector.underscore(key)
|
230
|
+
end.to_sym
|
193
231
|
end
|
232
|
+
# rubocop:enable Metrics/MethodLength
|
194
233
|
|
195
234
|
def fetch_attribute(object, key, attribute)
|
196
235
|
value = case attribute
|
197
|
-
when Symbol then object
|
236
|
+
when Symbol then fetch_attribute_from_object_and_resource(object, attribute)
|
198
237
|
when Proc then instance_exec(object, &attribute)
|
199
|
-
when Alba::
|
238
|
+
when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(object, params: params, within: within) }
|
200
239
|
when TypedAttribute then attribute.value(object)
|
201
240
|
else
|
202
241
|
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
@@ -204,6 +243,12 @@ module Alba
|
|
204
243
|
value.nil? && nil_handler ? instance_exec(object, key, attribute, &nil_handler) : value
|
205
244
|
end
|
206
245
|
|
246
|
+
def fetch_attribute_from_object_and_resource(object, attribute)
|
247
|
+
has_method = @method_existence[attribute]
|
248
|
+
has_method = @method_existence[attribute] = object.respond_to?(attribute) if has_method.nil?
|
249
|
+
has_method ? object.public_send(attribute) : __send__(attribute, object)
|
250
|
+
end
|
251
|
+
|
207
252
|
def nil_handler
|
208
253
|
@nil_handler ||= (@_on_nil || Alba._on_nil)
|
209
254
|
end
|
@@ -225,8 +270,10 @@ module Alba
|
|
225
270
|
end
|
226
271
|
end
|
227
272
|
|
273
|
+
# Detect if object is a collection or not.
|
274
|
+
# When object is a Struct, it's Enumerable but not a collection
|
228
275
|
def collection?
|
229
|
-
@object.is_a?(Enumerable)
|
276
|
+
@object.is_a?(Enumerable) && !@object.is_a?(Struct)
|
230
277
|
end
|
231
278
|
end
|
232
279
|
|
@@ -241,7 +288,6 @@ module Alba
|
|
241
288
|
end
|
242
289
|
|
243
290
|
# Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
|
244
|
-
# rubocop:disable Metrics/ParameterLists
|
245
291
|
|
246
292
|
# Set multiple attributes at once
|
247
293
|
#
|
@@ -289,7 +335,7 @@ module Alba
|
|
289
335
|
@_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
|
290
336
|
end
|
291
337
|
|
292
|
-
# Set
|
338
|
+
# Set association
|
293
339
|
#
|
294
340
|
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
295
341
|
# @param condition [Proc, nil] a Proc to modify the association
|
@@ -299,31 +345,16 @@ module Alba
|
|
299
345
|
# @option options [Proc] if a condition to decide if this association should be serialized
|
300
346
|
# @param block [Block]
|
301
347
|
# @return [void]
|
302
|
-
# @see Alba::
|
303
|
-
def
|
348
|
+
# @see Alba::Association#initialize
|
349
|
+
def association(name, condition = nil, resource: nil, key: nil, **options, &block)
|
304
350
|
nesting = self.name&.rpartition('::')&.first
|
305
|
-
|
306
|
-
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [
|
351
|
+
assoc = Association.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
352
|
+
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [assoc, options[:if]] : assoc
|
307
353
|
end
|
308
|
-
alias
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
313
|
-
# @param condition [Proc, nil] a Proc to filter the collection
|
314
|
-
# @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
|
315
|
-
# @param key [String, Symbol, nil] used as key when given
|
316
|
-
# @param options [Hash<Symbol, Proc>]
|
317
|
-
# @option options [Proc] if a condition to decide if this association should be serialized
|
318
|
-
# @param block [Block]
|
319
|
-
# @return [void]
|
320
|
-
# @see Alba::Many#initialize
|
321
|
-
def many(name, condition = nil, resource: nil, key: nil, **options, &block)
|
322
|
-
nesting = self.name&.rpartition('::')&.first
|
323
|
-
many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
324
|
-
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
|
325
|
-
end
|
326
|
-
alias has_many many
|
354
|
+
alias one association
|
355
|
+
alias many association
|
356
|
+
alias has_one association
|
357
|
+
alias has_many association
|
327
358
|
|
328
359
|
# Set key
|
329
360
|
#
|
@@ -369,14 +400,33 @@ module Alba
|
|
369
400
|
# @params file [String] name of the layout file
|
370
401
|
# @params inline [Proc] a proc returning JSON string or a Hash representing JSON
|
371
402
|
def layout(file: nil, inline: nil)
|
372
|
-
@_layout = file || inline
|
403
|
+
@_layout = validated_file_layout(file) || validated_inline_layout(inline)
|
404
|
+
end
|
405
|
+
|
406
|
+
def validated_file_layout(filename)
|
407
|
+
case filename
|
408
|
+
when String, nil then filename
|
409
|
+
else
|
410
|
+
raise ArgumentError, 'File layout must be a String representing filename'
|
411
|
+
end
|
373
412
|
end
|
413
|
+
private :validated_file_layout
|
414
|
+
|
415
|
+
def validated_inline_layout(inline_layout)
|
416
|
+
case inline_layout
|
417
|
+
when Proc, nil then inline_layout
|
418
|
+
else
|
419
|
+
raise ArgumentError, 'Inline layout must be a Proc returning a Hash or a String'
|
420
|
+
end
|
421
|
+
end
|
422
|
+
private :validated_inline_layout
|
374
423
|
|
375
424
|
# Delete attributes
|
376
425
|
# Use this DSL in child class to ignore certain attributes
|
377
426
|
#
|
378
427
|
# @param attributes [Array<String, Symbol>]
|
379
428
|
def ignoring(*attributes)
|
429
|
+
Alba::Deprecation.warn '`ignoring` is deprecated now. Instead please use `attributes` instance method to filter out attributes.'
|
380
430
|
attributes.each do |attr_name|
|
381
431
|
@_attributes.delete(attr_name.to_sym)
|
382
432
|
end
|
@@ -384,10 +434,18 @@ module Alba
|
|
384
434
|
|
385
435
|
# Transform keys as specified type
|
386
436
|
#
|
387
|
-
# @param type [String, Symbol]
|
388
|
-
# @param root [Boolean] decides if root key also should be transformed
|
437
|
+
# @param type [String, Symbol] one of `snake`, `:camel`, `:lower_camel`, `:dash` and `none`
|
438
|
+
# @param root [Boolean, nil] decides if root key also should be transformed
|
439
|
+
# When it's `nil`, Alba's default setting will be applied
|
440
|
+
# @raise [Alba::Error] when type is not supported
|
389
441
|
def transform_keys(type, root: nil)
|
390
|
-
|
442
|
+
type = type.to_sym
|
443
|
+
unless %i[none snake camel lower_camel dash].include?(type)
|
444
|
+
# This should be `ArgumentError` but for backward compatibility it raises `Alba::Error`
|
445
|
+
raise ::Alba::Error, "Unknown transform type: #{type}. Supported type are :camel, :lower_camel and :dash."
|
446
|
+
end
|
447
|
+
|
448
|
+
@_transform_type = type
|
391
449
|
@_transforming_root_key = root
|
392
450
|
end
|
393
451
|
|
@@ -409,8 +467,6 @@ module Alba
|
|
409
467
|
def on_nil(&block)
|
410
468
|
@_on_nil = block
|
411
469
|
end
|
412
|
-
|
413
|
-
# rubocop:enable Metrics/ParameterLists
|
414
470
|
end
|
415
471
|
end
|
416
472
|
end
|
data/lib/alba/version.rb
CHANGED