alba 1.5.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/.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