alba 1.0.0 → 1.4.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/ISSUE_TEMPLATE/bug_report.md +26 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/dependabot.yml +26 -0
- data/.github/workflows/main.yml +10 -1
- data/.github/workflows/perf.yml +21 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +33 -8
- data/.yardopts +2 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +12 -6
- data/README.md +259 -24
- data/Rakefile +4 -1
- data/SECURITY.md +12 -0
- data/alba.gemspec +3 -3
- data/benchmark/collection.rb +392 -0
- data/benchmark/single_resource.rb +370 -0
- data/codecov.yml +8 -0
- data/gemfiles/all.gemfile +19 -0
- data/gemfiles/without_active_support.gemfile +17 -0
- data/gemfiles/without_oj.gemfile +17 -0
- data/lib/alba.rb +69 -26
- data/lib/alba/association.rb +14 -22
- data/lib/alba/default_inflector.rb +36 -0
- data/lib/alba/key_transform_factory.rb +33 -0
- data/lib/alba/many.rb +3 -2
- data/lib/alba/one.rb +3 -2
- data/lib/alba/resource.rb +171 -62
- data/lib/alba/typed_attribute.rb +61 -0
- data/lib/alba/version.rb +1 -1
- data/script/perf_check.rb +174 -0
- data/sider.yml +2 -4
- metadata +22 -10
- data/benchmark/local.rb +0 -198
- data/lib/alba/key_transformer.rb +0 -31
data/codecov.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activesupport', require: false # For backend
|
4
|
+
gem 'ffaker', require: false # For testing
|
5
|
+
gem 'minitest', '~> 5.14' # For test
|
6
|
+
gem 'rake', '~> 13.0' # For test and automation
|
7
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
8
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
9
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
10
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
11
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
12
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
13
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
14
|
+
gem 'yard', require: false # For documentation
|
15
|
+
|
16
|
+
platforms :ruby do
|
17
|
+
gem 'oj', '~> 3.11', require: false # For backend
|
18
|
+
gem 'ruby-prof', require: false # For performance profiling
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'minitest', '~> 5.14' # For test
|
4
|
+
gem 'rake', '~> 13.0' # For test and automation
|
5
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
6
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
7
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
8
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
9
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
10
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
11
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
12
|
+
gem 'yard', require: false # For documentation
|
13
|
+
|
14
|
+
platforms :ruby do
|
15
|
+
gem 'oj', '~> 3.11', require: false # For backend
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activesupport', require: false # For backend
|
4
|
+
gem 'minitest', '~> 5.14' # For test
|
5
|
+
gem 'rake', '~> 13.0' # For test and automation
|
6
|
+
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
7
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
8
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
9
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
10
|
+
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
11
|
+
gem 'simplecov', '~> 0.21.0', require: false # For test coverage
|
12
|
+
gem 'simplecov-cobertura', require: false # For test coverage
|
13
|
+
gem 'yard', require: false # For documentation
|
14
|
+
|
15
|
+
platforms :ruby do
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
17
|
+
end
|
data/lib/alba.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'json'
|
1
2
|
require_relative 'alba/version'
|
2
3
|
require_relative 'alba/resource'
|
3
4
|
|
@@ -9,8 +10,14 @@ module Alba
|
|
9
10
|
# Error class for backend which is not supported
|
10
11
|
class UnsupportedBackend < Error; end
|
11
12
|
|
13
|
+
# Error class for type which is not supported
|
14
|
+
class UnsupportedType < Error; end
|
15
|
+
|
12
16
|
class << self
|
13
|
-
attr_reader :backend, :encoder, :inferring, :_on_error
|
17
|
+
attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
|
18
|
+
|
19
|
+
# Accessor for inflector, a module responsible for incflecting strings
|
20
|
+
attr_accessor :inflector
|
14
21
|
|
15
22
|
# Set the backend, which actually serializes object into JSON
|
16
23
|
#
|
@@ -20,22 +27,35 @@ module Alba
|
|
20
27
|
# @raise [Alba::UnsupportedBackend] if backend is not supported
|
21
28
|
def backend=(backend)
|
22
29
|
@backend = backend&.to_sym
|
23
|
-
|
30
|
+
set_encoder_from_backend
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set encoder, a Proc object that accepts an object and generates JSON from it
|
34
|
+
# Set backend as `:custom` which indicates no preset encoder is used
|
35
|
+
#
|
36
|
+
# @param encoder [Proc]
|
37
|
+
# @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
|
38
|
+
def encoder=(encoder)
|
39
|
+
raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
|
40
|
+
|
41
|
+
@encoder = encoder
|
42
|
+
@backend = :custom
|
24
43
|
end
|
25
44
|
|
26
45
|
# Serialize the object with inline definitions
|
27
46
|
#
|
28
47
|
# @param object [Object] the object to be serialized
|
29
|
-
# @param key [Symbol]
|
48
|
+
# @param key [Symbol, nil, true] DEPRECATED, use root_key instead
|
49
|
+
# @param root_key [Symbol, nil, true]
|
30
50
|
# @param block [Block] resource block
|
31
51
|
# @return [String] serialized JSON string
|
32
52
|
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
|
33
|
-
def serialize(object, key: nil, &block)
|
34
|
-
|
53
|
+
def serialize(object, key: nil, root_key: nil, &block)
|
54
|
+
warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
|
55
|
+
klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
|
35
56
|
|
36
|
-
|
37
|
-
resource
|
38
|
-
resource.serialize(key: key)
|
57
|
+
resource = klass.new(object)
|
58
|
+
resource.serialize(root_key: root_key || key)
|
39
59
|
end
|
40
60
|
|
41
61
|
# Enable inference for key and resource name
|
@@ -57,33 +77,62 @@ module Alba
|
|
57
77
|
#
|
58
78
|
# @param [Symbol] handler
|
59
79
|
# @param [Block]
|
80
|
+
# @raise [ArgumentError] if both handler and block params exist
|
81
|
+
# @raise [ArgumentError] if both handler and block params don't exist
|
82
|
+
# @return [void]
|
60
83
|
def on_error(handler = nil, &block)
|
61
84
|
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
62
85
|
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
63
86
|
|
64
|
-
p block if block
|
65
87
|
@_on_error = handler || block
|
66
88
|
end
|
67
89
|
|
90
|
+
# Enable root key transformation
|
91
|
+
def enable_root_key_transformation!
|
92
|
+
@transforming_root_key = true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Disable root key transformation
|
96
|
+
def disable_root_key_transformation!
|
97
|
+
@transforming_root_key = false
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param block [Block] resource body
|
101
|
+
# @return [Class<Alba::Resource>] resource class
|
102
|
+
def resource_class(&block)
|
103
|
+
klass = Class.new
|
104
|
+
klass.include(Alba::Resource)
|
105
|
+
klass.class_eval(&block)
|
106
|
+
klass
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param name [String] a String Alba infers resource name with
|
110
|
+
# @param nesting [String, nil] namespace Alba tries to find resource class in
|
111
|
+
# @return [Class<Alba::Resource>] resource class
|
112
|
+
def infer_resource_class(name, nesting: nil)
|
113
|
+
enable_inference!
|
114
|
+
const_parent = nesting.nil? ? Object : Object.const_get(nesting)
|
115
|
+
const_parent.const_get("#{ActiveSupport::Inflector.classify(name)}Resource")
|
116
|
+
end
|
117
|
+
|
68
118
|
private
|
69
119
|
|
70
|
-
def
|
120
|
+
def set_encoder_from_backend
|
71
121
|
@encoder = case @backend
|
72
|
-
when :oj
|
73
|
-
|
74
|
-
when :active_support
|
75
|
-
|
76
|
-
when nil, :default, :json
|
77
|
-
default_encoder
|
122
|
+
when :oj, :oj_strict then try_oj
|
123
|
+
when :oj_rails then try_oj(mode: :rails)
|
124
|
+
when :active_support then try_active_support
|
125
|
+
when nil, :default, :json then default_encoder
|
78
126
|
else
|
79
127
|
raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
|
80
128
|
end
|
81
129
|
end
|
82
130
|
|
83
|
-
def try_oj
|
131
|
+
def try_oj(mode: :strict)
|
84
132
|
require 'oj'
|
85
|
-
->(hash) { Oj.dump(hash, mode:
|
133
|
+
->(hash) { Oj.dump(hash, mode: mode) }
|
86
134
|
rescue LoadError
|
135
|
+
Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
|
87
136
|
default_encoder
|
88
137
|
end
|
89
138
|
|
@@ -91,24 +140,18 @@ module Alba
|
|
91
140
|
require 'active_support/json'
|
92
141
|
->(hash) { ActiveSupport::JSON.encode(hash) }
|
93
142
|
rescue LoadError
|
143
|
+
Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
|
94
144
|
default_encoder
|
95
145
|
end
|
96
146
|
|
97
147
|
def default_encoder
|
98
148
|
lambda do |hash|
|
99
|
-
require 'json'
|
100
149
|
JSON.dump(hash)
|
101
150
|
end
|
102
151
|
end
|
103
|
-
|
104
|
-
def resource_class
|
105
|
-
@resource_class ||= begin
|
106
|
-
klass = Class.new
|
107
|
-
klass.include(Alba::Resource)
|
108
|
-
end
|
109
|
-
end
|
110
152
|
end
|
111
153
|
|
112
154
|
@encoder = default_encoder
|
113
155
|
@_on_error = :raise
|
156
|
+
@transforming_root_key = false # TODO: This will be true since 2.0
|
114
157
|
end
|
data/lib/alba/association.rb
CHANGED
@@ -2,11 +2,12 @@ module Alba
|
|
2
2
|
# Base class for `One` and `Many`
|
3
3
|
# Child class should implement `to_hash` method
|
4
4
|
class Association
|
5
|
-
attr_reader :object
|
5
|
+
attr_reader :object, :name
|
6
6
|
|
7
|
-
# @param name [Symbol] name of the method to fetch association
|
8
|
-
# @param condition [Proc] a proc filtering data
|
9
|
-
# @param resource [Class<Alba::Resource
|
7
|
+
# @param name [Symbol, String] name of the method to fetch association
|
8
|
+
# @param condition [Proc, nil] a proc filtering data
|
9
|
+
# @param resource [Class<Alba::Resource>, nil] a resource class for the association
|
10
|
+
# @param nesting [String] a namespace where source class is inferred with
|
10
11
|
# @param block [Block] used to define resource when resource arg is absent
|
11
12
|
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
12
13
|
@name = name
|
@@ -15,19 +16,7 @@ module Alba
|
|
15
16
|
@resource = resource
|
16
17
|
return if @resource
|
17
18
|
|
18
|
-
|
19
|
-
@resource = resource_class
|
20
|
-
elsif Alba.inferring
|
21
|
-
const_parent = nesting.nil? ? Object : Object.const_get(nesting)
|
22
|
-
@resource = const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
|
23
|
-
else
|
24
|
-
raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
# @abstract
|
29
|
-
def to_hash
|
30
|
-
:not_implemented
|
19
|
+
assign_resource(nesting)
|
31
20
|
end
|
32
21
|
|
33
22
|
private
|
@@ -41,11 +30,14 @@ module Alba
|
|
41
30
|
end
|
42
31
|
end
|
43
32
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
33
|
+
def assign_resource(nesting)
|
34
|
+
@resource = if @block
|
35
|
+
Alba.resource_class(&@block)
|
36
|
+
elsif Alba.inferring
|
37
|
+
Alba.infer_resource_class(@name, nesting: nesting)
|
38
|
+
else
|
39
|
+
raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
|
40
|
+
end
|
49
41
|
end
|
50
42
|
end
|
51
43
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Alba
|
2
|
+
# This module represents the inflector, which is used by default
|
3
|
+
module DefaultInflector
|
4
|
+
begin
|
5
|
+
require 'active_support/inflector'
|
6
|
+
rescue LoadError
|
7
|
+
raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# Camelizes a key
|
13
|
+
#
|
14
|
+
# @param key [String] key to be camelized
|
15
|
+
# @return [String] camelized key
|
16
|
+
def camelize(key)
|
17
|
+
ActiveSupport::Inflector.camelize(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Camelizes a key, 1st letter lowercase
|
21
|
+
#
|
22
|
+
# @param key [String] key to be camelized
|
23
|
+
# @return [String] camelized key
|
24
|
+
def camelize_lower(key)
|
25
|
+
ActiveSupport::Inflector.camelize(key, false)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Dasherizes a key
|
29
|
+
#
|
30
|
+
# @param key [String] key to be dasherized
|
31
|
+
# @return [String] dasherized key
|
32
|
+
def dasherize(key)
|
33
|
+
ActiveSupport::Inflector.dasherize(key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Alba
|
2
|
+
# This module creates key transform functions
|
3
|
+
module KeyTransformFactory
|
4
|
+
class << self
|
5
|
+
# Create key transform function for given transform_type
|
6
|
+
#
|
7
|
+
# @param transform_type [Symbol] transform type
|
8
|
+
# @return [Proc] transform function
|
9
|
+
# @raise [Alba::Error] when transform_type is not supported
|
10
|
+
def create(transform_type)
|
11
|
+
case transform_type
|
12
|
+
when :camel
|
13
|
+
->(key) { _inflector.camelize(key) }
|
14
|
+
when :lower_camel
|
15
|
+
->(key) { _inflector.camelize_lower(key) }
|
16
|
+
when :dash
|
17
|
+
->(key) { _inflector.dasherize(key) }
|
18
|
+
else
|
19
|
+
raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def _inflector
|
26
|
+
Alba.inflector || begin
|
27
|
+
require_relative './default_inflector'
|
28
|
+
Alba::DefaultInflector
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/alba/many.rb
CHANGED
@@ -6,15 +6,16 @@ module Alba
|
|
6
6
|
# Recursively converts objects into an Array of Hashes
|
7
7
|
#
|
8
8
|
# @param target [Object] the object having an association method
|
9
|
+
# @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
|
9
10
|
# @param params [Hash] user-given Hash for arbitrary data
|
10
11
|
# @return [Array<Hash>]
|
11
|
-
def to_hash(target, params: {})
|
12
|
+
def to_hash(target, within: nil, params: {})
|
12
13
|
@object = target.public_send(@name)
|
13
14
|
@object = @condition.call(@object, params) if @condition
|
14
15
|
return if @object.nil?
|
15
16
|
|
16
17
|
@resource = constantize(@resource)
|
17
|
-
@
|
18
|
+
@resource.new(@object, params: params, within: within).to_hash
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
data/lib/alba/one.rb
CHANGED
@@ -6,15 +6,16 @@ module Alba
|
|
6
6
|
# Recursively converts an object into a Hash
|
7
7
|
#
|
8
8
|
# @param target [Object] the object having an association method
|
9
|
+
# @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
|
9
10
|
# @param params [Hash] user-given Hash for arbitrary data
|
10
11
|
# @return [Hash]
|
11
|
-
def to_hash(target, params: {})
|
12
|
+
def to_hash(target, within: nil, params: {})
|
12
13
|
@object = target.public_send(@name)
|
13
14
|
@object = @condition.call(object, params) if @condition
|
14
15
|
return if @object.nil?
|
15
16
|
|
16
17
|
@resource = constantize(@resource)
|
17
|
-
@resource.new(object, params: params).to_hash
|
18
|
+
@resource.new(object, params: params, within: within).to_hash
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
data/lib/alba/resource.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
require_relative 'one'
|
2
2
|
require_relative 'many'
|
3
|
+
require_relative 'key_transform_factory'
|
4
|
+
require_relative 'typed_attribute'
|
3
5
|
|
4
6
|
module Alba
|
5
7
|
# This module represents what should be serialized
|
6
8
|
module Resource
|
7
9
|
# @!parse include InstanceMethods
|
8
10
|
# @!parse extend ClassMethods
|
9
|
-
DSLS = {_attributes: {}, _key: nil,
|
11
|
+
DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze # rubocop:disable Layout/LineLength
|
10
12
|
private_constant :DSLS
|
11
13
|
|
14
|
+
WITHIN_DEFAULT = Object.new.freeze
|
15
|
+
private_constant :WITHIN_DEFAULT
|
16
|
+
|
12
17
|
# @private
|
13
18
|
def self.included(base)
|
14
19
|
super
|
@@ -28,19 +33,29 @@ module Alba
|
|
28
33
|
|
29
34
|
# @param object [Object] the object to be serialized
|
30
35
|
# @param params [Hash] user-given Hash for arbitrary data
|
31
|
-
|
36
|
+
# @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
|
37
|
+
def initialize(object, params: {}, within: WITHIN_DEFAULT)
|
32
38
|
@object = object
|
33
39
|
@params = params.freeze
|
40
|
+
@within = within
|
34
41
|
DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
|
35
42
|
end
|
36
43
|
|
37
44
|
# Serialize object into JSON string
|
38
45
|
#
|
39
|
-
# @param key [Symbol]
|
46
|
+
# @param key [Symbol, nil, true] DEPRECATED, use root_key instead
|
47
|
+
# @param root_key [Symbol, nil, true]
|
48
|
+
# @param meta [Hash] metadata for this seialization
|
40
49
|
# @return [String] serialized JSON string
|
41
|
-
def serialize(key: nil)
|
42
|
-
key
|
43
|
-
|
50
|
+
def serialize(key: nil, root_key: nil, meta: {})
|
51
|
+
warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
|
52
|
+
key = key.nil? && root_key.nil? ? fetch_key : root_key || key
|
53
|
+
hash = if key && key != ''
|
54
|
+
h = {key.to_s => serializable_hash}
|
55
|
+
hash_with_metadata(h, meta)
|
56
|
+
else
|
57
|
+
serializable_hash
|
58
|
+
end
|
44
59
|
Alba.encoder.call(hash)
|
45
60
|
end
|
46
61
|
|
@@ -54,27 +69,44 @@ module Alba
|
|
54
69
|
|
55
70
|
private
|
56
71
|
|
72
|
+
def hash_with_metadata(hash, meta)
|
73
|
+
base = @_meta ? instance_eval(&@_meta) : {}
|
74
|
+
metadata = base.merge(meta)
|
75
|
+
hash[:meta] = metadata unless metadata.empty?
|
76
|
+
hash
|
77
|
+
end
|
78
|
+
|
79
|
+
def fetch_key
|
80
|
+
collection? ? _key_for_collection : _key
|
81
|
+
end
|
82
|
+
|
83
|
+
def _key_for_collection
|
84
|
+
return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring
|
85
|
+
|
86
|
+
key = resource_name.pluralize
|
87
|
+
transforming_root_key? ? transform_key(key) : key
|
88
|
+
end
|
89
|
+
|
57
90
|
# @return [String]
|
58
91
|
def _key
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
92
|
+
return @_key.to_s unless @_key == true && Alba.inferring
|
93
|
+
|
94
|
+
transforming_root_key? ? transform_key(resource_name) : resource_name
|
95
|
+
end
|
96
|
+
|
97
|
+
def resource_name
|
98
|
+
self.class.name.demodulize.delete_suffix('Resource').underscore
|
99
|
+
end
|
100
|
+
|
101
|
+
def transforming_root_key?
|
102
|
+
@_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
|
66
103
|
end
|
67
104
|
|
68
105
|
def converter
|
69
106
|
lambda do |object|
|
70
107
|
arrays = @_attributes.map do |key, attribute|
|
71
|
-
key
|
72
|
-
|
73
|
-
conditional_attribute(object, key, attribute)
|
74
|
-
else
|
75
|
-
[key, fetch_attribute(object, attribute)]
|
76
|
-
end
|
77
|
-
rescue ::Alba::Error, FrozenError
|
108
|
+
key_and_attribute_body_from(object, key, attribute)
|
109
|
+
rescue ::Alba::Error, FrozenError, TypeError
|
78
110
|
raise
|
79
111
|
rescue StandardError => e
|
80
112
|
handle_error(e, object, key, attribute)
|
@@ -83,18 +115,24 @@ module Alba
|
|
83
115
|
end
|
84
116
|
end
|
85
117
|
|
118
|
+
def key_and_attribute_body_from(object, key, attribute)
|
119
|
+
key = transform_key(key)
|
120
|
+
if attribute.is_a?(Array) # Conditional
|
121
|
+
conditional_attribute(object, key, attribute)
|
122
|
+
else
|
123
|
+
[key, fetch_attribute(object, attribute)]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
86
127
|
def conditional_attribute(object, key, attribute)
|
87
128
|
condition = attribute.last
|
88
129
|
arity = condition.arity
|
89
|
-
|
130
|
+
# We can return early to skip fetch_attribute
|
131
|
+
return [] if arity <= 1 && !instance_exec(object, &condition)
|
90
132
|
|
91
133
|
fetched_attribute = fetch_attribute(object, attribute.first)
|
92
|
-
attr =
|
93
|
-
|
94
|
-
else
|
95
|
-
fetched_attribute
|
96
|
-
end
|
97
|
-
return [] if arity >= 2 && !condition.call(object, attr)
|
134
|
+
attr = attribute.first.is_a?(Alba::Association) ? attribute.first.object : fetched_attribute
|
135
|
+
return [] if arity >= 2 && !instance_exec(object, attr, &condition)
|
98
136
|
|
99
137
|
[key, fetched_attribute]
|
100
138
|
end
|
@@ -102,14 +140,10 @@ module Alba
|
|
102
140
|
def handle_error(error, object, key, attribute)
|
103
141
|
on_error = @_on_error || Alba._on_error
|
104
142
|
case on_error
|
105
|
-
when :raise, nil
|
106
|
-
|
107
|
-
when :
|
108
|
-
|
109
|
-
when :ignore
|
110
|
-
[]
|
111
|
-
when Proc
|
112
|
-
on_error.call(error, object, key, attribute, self.class)
|
143
|
+
when :raise, nil then raise
|
144
|
+
when :nullify then [key, nil]
|
145
|
+
when :ignore then []
|
146
|
+
when Proc then on_error.call(error, object, key, attribute, self.class)
|
113
147
|
else
|
114
148
|
raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
|
115
149
|
end
|
@@ -117,25 +151,39 @@ module Alba
|
|
117
151
|
|
118
152
|
# Override this method to supply custom key transform method
|
119
153
|
def transform_key(key)
|
120
|
-
return key
|
154
|
+
return key if @_transform_key_function.nil?
|
121
155
|
|
122
|
-
|
123
|
-
KeyTransformer.transform(key, @_transform_keys)
|
156
|
+
@_transform_key_function.call(key.to_s)
|
124
157
|
end
|
125
158
|
|
126
159
|
def fetch_attribute(object, attribute)
|
127
160
|
case attribute
|
128
|
-
when Symbol
|
129
|
-
|
130
|
-
when
|
131
|
-
|
132
|
-
when Alba::One, Alba::Many
|
133
|
-
attribute.to_hash(object, params: params)
|
161
|
+
when Symbol then object.public_send attribute
|
162
|
+
when Proc then instance_exec(object, &attribute)
|
163
|
+
when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) }
|
164
|
+
when TypedAttribute then attribute.value(object)
|
134
165
|
else
|
135
166
|
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
136
167
|
end
|
137
168
|
end
|
138
169
|
|
170
|
+
def yield_if_within(association_name)
|
171
|
+
within = check_within(association_name)
|
172
|
+
yield(within) if within
|
173
|
+
end
|
174
|
+
|
175
|
+
def check_within(association_name)
|
176
|
+
case @within
|
177
|
+
when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree
|
178
|
+
when Hash then @within.fetch(association_name, nil) # Traverse within tree
|
179
|
+
when Array then @within.find { |item| item.to_sym == association_name }
|
180
|
+
when Symbol then @within == association_name
|
181
|
+
when nil, true, false then false # Stop here
|
182
|
+
else
|
183
|
+
raise Alba::Error, "Unknown type for within option: #{@within.class}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
139
187
|
def collection?
|
140
188
|
@object.is_a?(Enumerable)
|
141
189
|
end
|
@@ -151,23 +199,49 @@ module Alba
|
|
151
199
|
DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
|
152
200
|
end
|
153
201
|
|
202
|
+
# Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
|
203
|
+
# rubocop:disable Metrics/ParameterLists
|
204
|
+
|
154
205
|
# Set multiple attributes at once
|
155
206
|
#
|
156
207
|
# @param attrs [Array<String, Symbol>]
|
157
|
-
# @param
|
158
|
-
|
208
|
+
# @param if [Proc] condition to decide if it should serialize these attributes
|
209
|
+
# @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
|
210
|
+
# attributes with name in its key and type and optional type converter in its value
|
211
|
+
# @return [void]
|
212
|
+
def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
|
213
|
+
if_value = binding.local_variable_get(:if)
|
214
|
+
assign_attributes(attrs, if_value)
|
215
|
+
assign_attributes_with_types(attrs_with_types, if_value)
|
216
|
+
end
|
217
|
+
|
218
|
+
def assign_attributes(attrs, if_value)
|
159
219
|
attrs.each do |attr_name|
|
160
|
-
attr =
|
220
|
+
attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
|
161
221
|
@_attributes[attr_name.to_sym] = attr
|
162
222
|
end
|
163
223
|
end
|
224
|
+
private :assign_attributes
|
225
|
+
|
226
|
+
def assign_attributes_with_types(attrs_with_types, if_value)
|
227
|
+
attrs_with_types.each do |attr_name, type_and_converter|
|
228
|
+
attr_name = attr_name.to_sym
|
229
|
+
type, type_converter = type_and_converter
|
230
|
+
typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
|
231
|
+
attr = if_value ? [typed_attr, if_value] : typed_attr
|
232
|
+
@_attributes[attr_name] = attr
|
233
|
+
end
|
234
|
+
end
|
235
|
+
private :assign_attributes_with_types
|
164
236
|
|
165
237
|
# Set an attribute with the given block
|
166
238
|
#
|
167
239
|
# @param name [String, Symbol] key name
|
168
|
-
# @param options [Hash]
|
240
|
+
# @param options [Hash<Symbol, Proc>]
|
241
|
+
# @option options [Proc] if a condition to decide if this attribute should be serialized
|
169
242
|
# @param block [Block] the block called during serialization
|
170
243
|
# @raise [ArgumentError] if block is absent
|
244
|
+
# @return [void]
|
171
245
|
def attribute(name, **options, &block)
|
172
246
|
raise ArgumentError, 'No block given in attribute method' unless block
|
173
247
|
|
@@ -176,12 +250,14 @@ module Alba
|
|
176
250
|
|
177
251
|
# Set One association
|
178
252
|
#
|
179
|
-
# @param name [String, Symbol]
|
180
|
-
# @param condition [Proc]
|
181
|
-
# @param resource [Class<Alba::Resource
|
182
|
-
# @param key [String, Symbol] used as key when given
|
183
|
-
# @param options [Hash]
|
253
|
+
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
254
|
+
# @param condition [Proc, nil] a Proc to modify the association
|
255
|
+
# @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
|
256
|
+
# @param key [String, Symbol, nil] used as key when given
|
257
|
+
# @param options [Hash<Symbol, Proc>]
|
258
|
+
# @option options [Proc] if a condition to decide if this association should be serialized
|
184
259
|
# @param block [Block]
|
260
|
+
# @return [void]
|
185
261
|
# @see Alba::One#initialize
|
186
262
|
def one(name, condition = nil, resource: nil, key: nil, **options, &block)
|
187
263
|
nesting = self.name&.rpartition('::')&.first
|
@@ -192,12 +268,14 @@ module Alba
|
|
192
268
|
|
193
269
|
# Set Many association
|
194
270
|
#
|
195
|
-
# @param name [String, Symbol]
|
196
|
-
# @param condition [Proc]
|
197
|
-
# @param resource [Class<Alba::Resource
|
198
|
-
# @param key [String, Symbol] used as key when given
|
199
|
-
# @param options [Hash]
|
271
|
+
# @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
|
272
|
+
# @param condition [Proc, nil] a Proc to filter the collection
|
273
|
+
# @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
|
274
|
+
# @param key [String, Symbol, nil] used as key when given
|
275
|
+
# @param options [Hash<Symbol, Proc>]
|
276
|
+
# @option options [Proc] if a condition to decide if this association should be serialized
|
200
277
|
# @param block [Block]
|
278
|
+
# @return [void]
|
201
279
|
# @see Alba::Many#initialize
|
202
280
|
def many(name, condition = nil, resource: nil, key: nil, **options, &block)
|
203
281
|
nesting = self.name&.rpartition('::')&.first
|
@@ -209,14 +287,40 @@ module Alba
|
|
209
287
|
# Set key
|
210
288
|
#
|
211
289
|
# @param key [String, Symbol]
|
290
|
+
# @deprecated Use {#root_key} instead
|
212
291
|
def key(key)
|
292
|
+
warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.'
|
213
293
|
@_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
214
294
|
end
|
215
295
|
|
296
|
+
# Set root key
|
297
|
+
#
|
298
|
+
# @param key [String, Symbol]
|
299
|
+
# @param key_for_collection [String, Symbol]
|
300
|
+
# @raise [NoMethodError] when key doesn't respond to `to_sym` method
|
301
|
+
def root_key(key, key_for_collection = nil)
|
302
|
+
@_key = key.to_sym
|
303
|
+
@_key_for_collection = key_for_collection&.to_sym
|
304
|
+
end
|
305
|
+
|
216
306
|
# Set key to true
|
217
307
|
#
|
308
|
+
# @deprecated Use {#root_key!} instead
|
218
309
|
def key!
|
310
|
+
warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.'
|
219
311
|
@_key = true
|
312
|
+
@_key_for_collection = true
|
313
|
+
end
|
314
|
+
|
315
|
+
# Set root key to true
|
316
|
+
def root_key!
|
317
|
+
@_key = true
|
318
|
+
@_key_for_collection = true
|
319
|
+
end
|
320
|
+
|
321
|
+
# Set metadata
|
322
|
+
def meta(&block)
|
323
|
+
@_meta = block
|
220
324
|
end
|
221
325
|
|
222
326
|
# Delete attributes
|
@@ -232,20 +336,25 @@ module Alba
|
|
232
336
|
# Transform keys as specified type
|
233
337
|
#
|
234
338
|
# @param type [String, Symbol]
|
235
|
-
|
236
|
-
|
339
|
+
# @param root [Boolean] decides if root key also should be transformed
|
340
|
+
def transform_keys(type, root: nil)
|
341
|
+
@_transform_key_function = KeyTransformFactory.create(type.to_sym)
|
342
|
+
@_transforming_root_key = root
|
237
343
|
end
|
238
344
|
|
239
345
|
# Set error handler
|
346
|
+
# If this is set it's used as a error handler overriding global one
|
240
347
|
#
|
241
|
-
# @param [Symbol]
|
242
|
-
# @param [Block]
|
348
|
+
# @param handler [Symbol] `:raise`, `:ignore` or `:nullify`
|
349
|
+
# @param block [Block]
|
243
350
|
def on_error(handler = nil, &block)
|
244
351
|
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
245
352
|
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
246
353
|
|
247
354
|
@_on_error = handler || block
|
248
355
|
end
|
356
|
+
|
357
|
+
# rubocop:enable Metrics/ParameterLists
|
249
358
|
end
|
250
359
|
end
|
251
360
|
end
|