alba 0.11.0 → 1.0.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/main.yml +25 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +12 -5
- data/.yardopts +2 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +7 -4
- data/README.md +264 -29
- data/benchmark/local.rb +198 -0
- data/lib/alba.rb +52 -11
- data/lib/alba/association.rb +28 -3
- data/lib/alba/key_transformer.rb +31 -0
- data/lib/alba/many.rb +12 -4
- data/lib/alba/one.rb +11 -3
- data/lib/alba/resource.rb +176 -58
- data/lib/alba/version.rb +1 -1
- data/sider.yml +1 -0
- metadata +8 -6
- data/.travis.yml +0 -9
- data/Gemfile.lock +0 -89
- data/lib/alba/serializer.rb +0 -59
data/benchmark/local.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# Benchmark script to run varieties of JSON serializers
|
2
|
+
# Fetch Alba from local, otherwise fetch latest from RubyGems
|
3
|
+
|
4
|
+
require "bundler/inline"
|
5
|
+
|
6
|
+
gemfile(true) do
|
7
|
+
source "https://rubygems.org"
|
8
|
+
|
9
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
10
|
+
|
11
|
+
gem "activerecord", "6.1.3"
|
12
|
+
gem "sqlite3"
|
13
|
+
gem "jbuilder"
|
14
|
+
gem "active_model_serializers"
|
15
|
+
gem "blueprinter"
|
16
|
+
gem "representable"
|
17
|
+
gem "alba", path: '../'
|
18
|
+
gem "oj"
|
19
|
+
gem "multi_json"
|
20
|
+
end
|
21
|
+
|
22
|
+
require "active_record"
|
23
|
+
require "sqlite3"
|
24
|
+
require "logger"
|
25
|
+
require "oj"
|
26
|
+
Oj.optimize_rails
|
27
|
+
|
28
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
29
|
+
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
30
|
+
|
31
|
+
ActiveRecord::Schema.define do
|
32
|
+
create_table :posts, force: true do |t|
|
33
|
+
t.string :body
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :comments, force: true do |t|
|
37
|
+
t.integer :post_id
|
38
|
+
t.string :body
|
39
|
+
t.integer :commenter_id
|
40
|
+
end
|
41
|
+
|
42
|
+
create_table :users, force: true do |t|
|
43
|
+
t.string :name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Post < ActiveRecord::Base
|
48
|
+
has_many :comments
|
49
|
+
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
50
|
+
|
51
|
+
def attributes
|
52
|
+
{id: nil, body: nil, commenter_names: commenter_names}
|
53
|
+
end
|
54
|
+
|
55
|
+
def commenter_names
|
56
|
+
commenters.pluck(:name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Comment < ActiveRecord::Base
|
61
|
+
belongs_to :post
|
62
|
+
belongs_to :commenter, class_name: 'User'
|
63
|
+
|
64
|
+
def attributes
|
65
|
+
{id: nil, body: nil}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
has_many :comments
|
71
|
+
end
|
72
|
+
|
73
|
+
require "alba"
|
74
|
+
Alba.backend = :oj
|
75
|
+
|
76
|
+
class AlbaCommentResource
|
77
|
+
include ::Alba::Resource
|
78
|
+
attributes :id, :body
|
79
|
+
end
|
80
|
+
|
81
|
+
class AlbaPostResource
|
82
|
+
include ::Alba::Resource
|
83
|
+
attributes :id, :body
|
84
|
+
many :comments, resource: AlbaCommentResource
|
85
|
+
attribute :commenter_names do |post|
|
86
|
+
post.commenters.pluck(:name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
require "jbuilder"
|
91
|
+
class Post
|
92
|
+
def to_builder
|
93
|
+
Jbuilder.new do |post|
|
94
|
+
post.call(self, :id, :body, :comments, :commenter_names)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def commenter_names
|
99
|
+
commenters.pluck(:name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Comment
|
104
|
+
def to_builder
|
105
|
+
Jbuilder.new do |comment|
|
106
|
+
comment.call(self, :id, :body)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
require "active_model_serializers"
|
112
|
+
|
113
|
+
class AMSCommentSerializer < ActiveModel::Serializer
|
114
|
+
attributes :id, :body
|
115
|
+
end
|
116
|
+
|
117
|
+
class AMSPostSerializer < ActiveModel::Serializer
|
118
|
+
attributes :id, :body
|
119
|
+
has_many :comments, serializer: AMSCommentSerializer
|
120
|
+
attribute :commenter_names
|
121
|
+
def commenter_names
|
122
|
+
object.commenters.pluck(:name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
require "blueprinter"
|
127
|
+
|
128
|
+
class CommentBlueprint < Blueprinter::Base
|
129
|
+
fields :id, :body
|
130
|
+
end
|
131
|
+
|
132
|
+
class PostBlueprint < Blueprinter::Base
|
133
|
+
fields :id, :body, :commenter_names
|
134
|
+
association :comments, blueprint: CommentBlueprint
|
135
|
+
def commenter_names
|
136
|
+
commenters.pluck(:name)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
require "representable"
|
141
|
+
|
142
|
+
class CommentRepresenter < Representable::Decorator
|
143
|
+
include Representable::JSON
|
144
|
+
|
145
|
+
property :id
|
146
|
+
property :body
|
147
|
+
end
|
148
|
+
|
149
|
+
class PostRepresenter < Representable::Decorator
|
150
|
+
include Representable::JSON
|
151
|
+
|
152
|
+
property :id
|
153
|
+
property :body
|
154
|
+
property :commenter_names
|
155
|
+
collection :comments
|
156
|
+
|
157
|
+
def commenter_names
|
158
|
+
commenters.pluck(:name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
post = Post.create!(body: 'post')
|
163
|
+
user1 = User.create!(name: 'John')
|
164
|
+
user2 = User.create!(name: 'Jane')
|
165
|
+
post.comments.create!(commenter: user1, body: 'Comment1')
|
166
|
+
post.comments.create!(commenter: user2, body: 'Comment2')
|
167
|
+
post.reload
|
168
|
+
|
169
|
+
alba = Proc.new { AlbaPostResource.new(post).serialize }
|
170
|
+
jbuilder = Proc.new { post.to_builder.target! }
|
171
|
+
ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
|
172
|
+
rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
|
173
|
+
blueprinter = Proc.new { PostBlueprint.render(post) }
|
174
|
+
representable = Proc.new { PostRepresenter.new(post).to_json }
|
175
|
+
alba_inline = Proc.new do
|
176
|
+
Alba.serialize(post) do
|
177
|
+
attributes :id, :body
|
178
|
+
attribute :commenter_names do |post|
|
179
|
+
post.commenters.pluck(:name)
|
180
|
+
end
|
181
|
+
many :comments do
|
182
|
+
attributes :id, :body
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
[alba, jbuilder, ams, rails, blueprinter, representable, alba_inline].each {|x| puts x.call }
|
187
|
+
|
188
|
+
require 'benchmark'
|
189
|
+
time = 1000
|
190
|
+
Benchmark.bmbm do |x|
|
191
|
+
x.report(:alba) { time.times(&alba) }
|
192
|
+
x.report(:jbuilder) { time.times(&jbuilder) }
|
193
|
+
x.report(:ams) { time.times(&ams) }
|
194
|
+
x.report(:rails) { time.times(&rails) }
|
195
|
+
x.report(:blueprinter) { time.times(&blueprinter) }
|
196
|
+
x.report(:representable) { time.times(&representable) }
|
197
|
+
x.report(:alba_inline) { time.times(&alba_inline) }
|
198
|
+
end
|
data/lib/alba.rb
CHANGED
@@ -1,28 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'alba/resource'
|
1
|
+
require_relative 'alba/version'
|
2
|
+
require_relative 'alba/resource'
|
4
3
|
|
5
4
|
# Core module
|
6
5
|
module Alba
|
6
|
+
# Base class for Errors
|
7
7
|
class Error < StandardError; end
|
8
|
+
|
9
|
+
# Error class for backend which is not supported
|
8
10
|
class UnsupportedBackend < Error; end
|
9
11
|
|
10
12
|
class << self
|
11
|
-
attr_reader :backend, :encoder
|
12
|
-
attr_accessor :default_serializer
|
13
|
+
attr_reader :backend, :encoder, :inferring, :_on_error
|
13
14
|
|
15
|
+
# Set the backend, which actually serializes object into JSON
|
16
|
+
#
|
17
|
+
# @param backend [#to_sym, nil] the name of the backend
|
18
|
+
# Possible values are `oj`, `active_support`, `default`, `json` and nil
|
19
|
+
# @return [Proc] the proc to encode object into JSON
|
20
|
+
# @raise [Alba::UnsupportedBackend] if backend is not supported
|
14
21
|
def backend=(backend)
|
15
22
|
@backend = backend&.to_sym
|
16
23
|
set_encoder
|
17
24
|
end
|
18
25
|
|
19
|
-
|
26
|
+
# Serialize the object with inline definitions
|
27
|
+
#
|
28
|
+
# @param object [Object] the object to be serialized
|
29
|
+
# @param key [Symbol]
|
30
|
+
# @param block [Block] resource block
|
31
|
+
# @return [String] serialized JSON string
|
32
|
+
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
|
33
|
+
def serialize(object, key: nil, &block)
|
20
34
|
raise ArgumentError, 'Block required' unless block
|
21
35
|
|
22
36
|
resource_class.class_eval(&block)
|
23
37
|
resource = resource_class.new(object)
|
24
|
-
|
25
|
-
|
38
|
+
resource.serialize(key: key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Enable inference for key and resource name
|
42
|
+
def enable_inference!
|
43
|
+
begin
|
44
|
+
require 'active_support/inflector'
|
45
|
+
rescue LoadError
|
46
|
+
raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
|
47
|
+
end
|
48
|
+
@inferring = true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Disable inference for key and resource name
|
52
|
+
def disable_inference!
|
53
|
+
@inferring = false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set error handler
|
57
|
+
#
|
58
|
+
# @param [Symbol] handler
|
59
|
+
# @param [Block]
|
60
|
+
def on_error(handler = nil, &block)
|
61
|
+
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
62
|
+
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
63
|
+
|
64
|
+
p block if block
|
65
|
+
@_on_error = handler || block
|
26
66
|
end
|
27
67
|
|
28
68
|
private
|
@@ -63,11 +103,12 @@ module Alba
|
|
63
103
|
|
64
104
|
def resource_class
|
65
105
|
@resource_class ||= begin
|
66
|
-
|
67
|
-
|
68
|
-
|
106
|
+
klass = Class.new
|
107
|
+
klass.include(Alba::Resource)
|
108
|
+
end
|
69
109
|
end
|
70
110
|
end
|
71
111
|
|
72
112
|
@encoder = default_encoder
|
113
|
+
@_on_error = :raise
|
73
114
|
end
|
data/lib/alba/association.rb
CHANGED
@@ -2,20 +2,45 @@ module Alba
|
|
2
2
|
# Base class for `One` and `Many`
|
3
3
|
# Child class should implement `to_hash` method
|
4
4
|
class Association
|
5
|
-
|
5
|
+
attr_reader :object
|
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>] a resource class for the association
|
10
|
+
# @param block [Block] used to define resource when resource arg is absent
|
11
|
+
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
6
12
|
@name = name
|
7
13
|
@condition = condition
|
8
14
|
@block = block
|
9
|
-
@resource = resource
|
10
|
-
|
15
|
+
@resource = resource
|
16
|
+
return if @resource
|
17
|
+
|
18
|
+
if @block
|
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
|
11
26
|
end
|
12
27
|
|
28
|
+
# @abstract
|
13
29
|
def to_hash
|
14
30
|
:not_implemented
|
15
31
|
end
|
16
32
|
|
17
33
|
private
|
18
34
|
|
35
|
+
def constantize(resource)
|
36
|
+
case resource # rubocop:disable Style/MissingElse
|
37
|
+
when Class
|
38
|
+
resource
|
39
|
+
when Symbol, String
|
40
|
+
Object.const_get(resource)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
19
44
|
def resource_class
|
20
45
|
klass = Class.new
|
21
46
|
klass.include(Alba::Resource)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Alba
|
2
|
+
# Transform keys using `ActiveSupport::Inflector`
|
3
|
+
module KeyTransformer
|
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
|
+
# Transform key as given transform_type
|
13
|
+
#
|
14
|
+
# @params key [String] key to be transformed
|
15
|
+
# @params transform_type [Symbol] transform type
|
16
|
+
# @return [String] transformed key
|
17
|
+
# @raise [Alba::Error] when transform_type is not supported
|
18
|
+
def transform(key, transform_type)
|
19
|
+
case transform_type
|
20
|
+
when :camel
|
21
|
+
ActiveSupport::Inflector.camelize(key)
|
22
|
+
when :lower_camel
|
23
|
+
ActiveSupport::Inflector.camelize(key, false)
|
24
|
+
when :dash
|
25
|
+
ActiveSupport::Inflector.dasherize(key)
|
26
|
+
else
|
27
|
+
raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/alba/many.rb
CHANGED
@@ -1,12 +1,20 @@
|
|
1
|
-
|
1
|
+
require_relative 'association'
|
2
2
|
|
3
3
|
module Alba
|
4
4
|
# Representing many association
|
5
5
|
class Many < Association
|
6
|
+
# Recursively converts objects into an Array of Hashes
|
7
|
+
#
|
8
|
+
# @param target [Object] the object having an association method
|
9
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
10
|
+
# @return [Array<Hash>]
|
6
11
|
def to_hash(target, params: {})
|
7
|
-
|
8
|
-
|
9
|
-
|
12
|
+
@object = target.public_send(@name)
|
13
|
+
@object = @condition.call(@object, params) if @condition
|
14
|
+
return if @object.nil?
|
15
|
+
|
16
|
+
@resource = constantize(@resource)
|
17
|
+
@object.map { |o| @resource.new(o, params: params).to_hash }
|
10
18
|
end
|
11
19
|
end
|
12
20
|
end
|
data/lib/alba/one.rb
CHANGED
@@ -1,11 +1,19 @@
|
|
1
|
-
|
1
|
+
require_relative 'association'
|
2
2
|
|
3
3
|
module Alba
|
4
4
|
# Representing one association
|
5
5
|
class One < Association
|
6
|
+
# Recursively converts an object into a Hash
|
7
|
+
#
|
8
|
+
# @param target [Object] the object having an association method
|
9
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
10
|
+
# @return [Hash]
|
6
11
|
def to_hash(target, params: {})
|
7
|
-
object = target.public_send(@name)
|
8
|
-
object = @condition.call(object, params) if @condition
|
12
|
+
@object = target.public_send(@name)
|
13
|
+
@object = @condition.call(object, params) if @condition
|
14
|
+
return if @object.nil?
|
15
|
+
|
16
|
+
@resource = constantize(@resource)
|
9
17
|
@resource.new(object, params: params).to_hash
|
10
18
|
end
|
11
19
|
end
|
data/lib/alba/resource.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'alba/many'
|
1
|
+
require_relative 'one'
|
2
|
+
require_relative 'many'
|
4
3
|
|
5
4
|
module Alba
|
6
5
|
# This module represents what should be serialized
|
7
6
|
module Resource
|
8
|
-
|
7
|
+
# @!parse include InstanceMethods
|
8
|
+
# @!parse extend ClassMethods
|
9
|
+
DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
|
10
|
+
private_constant :DSLS
|
11
|
+
|
12
|
+
# @private
|
9
13
|
def self.included(base)
|
10
14
|
super
|
11
15
|
base.class_eval do
|
@@ -20,66 +24,116 @@ module Alba
|
|
20
24
|
|
21
25
|
# Instance methods
|
22
26
|
module InstanceMethods
|
23
|
-
attr_reader :object, :
|
27
|
+
attr_reader :object, :params
|
24
28
|
|
29
|
+
# @param object [Object] the object to be serialized
|
30
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
25
31
|
def initialize(object, params: {})
|
26
32
|
@object = object
|
27
|
-
@params = params
|
33
|
+
@params = params.freeze
|
28
34
|
DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
|
29
35
|
end
|
30
36
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
else
|
40
|
-
raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
|
41
|
-
end
|
42
|
-
serializer.new(self).serialize
|
37
|
+
# Serialize object into JSON string
|
38
|
+
#
|
39
|
+
# @param key [Symbol]
|
40
|
+
# @return [String] serialized JSON string
|
41
|
+
def serialize(key: nil)
|
42
|
+
key = key.nil? ? _key : key
|
43
|
+
hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
|
44
|
+
Alba.encoder.call(hash)
|
43
45
|
end
|
44
46
|
|
47
|
+
# A Hash for serialization
|
48
|
+
#
|
49
|
+
# @return [Hash]
|
45
50
|
def serializable_hash
|
46
51
|
collection? ? @object.map(&converter) : converter.call(@object)
|
47
52
|
end
|
48
53
|
alias to_hash serializable_hash
|
49
54
|
|
50
|
-
def key
|
51
|
-
@_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
|
52
|
-
end
|
53
|
-
|
54
55
|
private
|
55
56
|
|
57
|
+
# @return [String]
|
58
|
+
def _key
|
59
|
+
if @_key == true && Alba.inferring
|
60
|
+
demodulized = ActiveSupport::Inflector.demodulize(self.class.name)
|
61
|
+
meth = collection? ? :tableize : :singularize
|
62
|
+
ActiveSupport::Inflector.public_send(meth, demodulized.delete_suffix('Resource').downcase)
|
63
|
+
else
|
64
|
+
@_key.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
56
68
|
def converter
|
57
|
-
lambda do |
|
58
|
-
@_attributes.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
when Proc
|
63
|
-
instance_exec(resource, &attribute)
|
64
|
-
when Alba::One, Alba::Many
|
65
|
-
attribute.to_hash(resource, params: params)
|
69
|
+
lambda do |object|
|
70
|
+
arrays = @_attributes.map do |key, attribute|
|
71
|
+
key = transform_key(key)
|
72
|
+
if attribute.is_a?(Array) # Conditional
|
73
|
+
conditional_attribute(object, key, attribute)
|
66
74
|
else
|
67
|
-
|
75
|
+
[key, fetch_attribute(object, attribute)]
|
68
76
|
end
|
77
|
+
rescue ::Alba::Error, FrozenError
|
78
|
+
raise
|
79
|
+
rescue StandardError => e
|
80
|
+
handle_error(e, object, key, attribute)
|
69
81
|
end
|
82
|
+
arrays.reject(&:empty?).to_h
|
70
83
|
end
|
71
84
|
end
|
72
85
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
86
|
+
def conditional_attribute(object, key, attribute)
|
87
|
+
condition = attribute.last
|
88
|
+
arity = condition.arity
|
89
|
+
return [] if arity <= 1 && !condition.call(object)
|
90
|
+
|
91
|
+
fetched_attribute = fetch_attribute(object, attribute.first)
|
92
|
+
attr = if attribute.first.is_a?(Alba::Association)
|
93
|
+
attribute.first.object
|
94
|
+
else
|
95
|
+
fetched_attribute
|
96
|
+
end
|
97
|
+
return [] if arity >= 2 && !condition.call(object, attr)
|
98
|
+
|
99
|
+
[key, fetched_attribute]
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_error(error, object, key, attribute)
|
103
|
+
on_error = @_on_error || Alba._on_error
|
104
|
+
case on_error
|
105
|
+
when :raise, nil
|
106
|
+
raise
|
107
|
+
when :nullify
|
108
|
+
[key, nil]
|
109
|
+
when :ignore
|
110
|
+
[]
|
111
|
+
when Proc
|
112
|
+
on_error.call(error, object, key, attribute, self.class)
|
113
|
+
else
|
114
|
+
raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
|
115
|
+
end
|
77
116
|
end
|
78
117
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
118
|
+
# Override this method to supply custom key transform method
|
119
|
+
def transform_key(key)
|
120
|
+
return key unless @_transform_keys
|
121
|
+
|
122
|
+
require_relative 'key_transformer'
|
123
|
+
KeyTransformer.transform(key, @_transform_keys)
|
124
|
+
end
|
125
|
+
|
126
|
+
def fetch_attribute(object, attribute)
|
127
|
+
case attribute
|
128
|
+
when Symbol
|
129
|
+
object.public_send attribute
|
130
|
+
when Proc
|
131
|
+
instance_exec(object, &attribute)
|
132
|
+
when Alba::One, Alba::Many
|
133
|
+
attribute.to_hash(object, params: params)
|
134
|
+
else
|
135
|
+
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
136
|
+
end
|
83
137
|
end
|
84
138
|
|
85
139
|
def collection?
|
@@ -91,43 +145,107 @@ module Alba
|
|
91
145
|
module ClassMethods
|
92
146
|
attr_reader(*DSLS.keys)
|
93
147
|
|
148
|
+
# @private
|
94
149
|
def inherited(subclass)
|
95
150
|
super
|
96
151
|
DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
|
97
152
|
end
|
98
153
|
|
99
|
-
|
100
|
-
|
154
|
+
# Set multiple attributes at once
|
155
|
+
#
|
156
|
+
# @param attrs [Array<String, Symbol>]
|
157
|
+
# @param options [Hash] option hash including `if` that is a condition to render these attributes
|
158
|
+
def attributes(*attrs, **options)
|
159
|
+
attrs.each do |attr_name|
|
160
|
+
attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
|
161
|
+
@_attributes[attr_name.to_sym] = attr
|
162
|
+
end
|
101
163
|
end
|
102
164
|
|
103
|
-
|
165
|
+
# Set an attribute with the given block
|
166
|
+
#
|
167
|
+
# @param name [String, Symbol] key name
|
168
|
+
# @param options [Hash] option hash including `if` that is a condition to render
|
169
|
+
# @param block [Block] the block called during serialization
|
170
|
+
# @raise [ArgumentError] if block is absent
|
171
|
+
def attribute(name, **options, &block)
|
104
172
|
raise ArgumentError, 'No block given in attribute method' unless block
|
105
173
|
|
106
|
-
@_attributes[name.to_sym] = block
|
107
|
-
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
174
|
+
@_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
|
175
|
+
end
|
176
|
+
|
177
|
+
# Set One association
|
178
|
+
#
|
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] option hash including `if` that is a condition to render
|
184
|
+
# @param block [Block]
|
185
|
+
# @see Alba::One#initialize
|
186
|
+
def one(name, condition = nil, resource: nil, key: nil, **options, &block)
|
187
|
+
nesting = self.name&.rpartition('::')&.first
|
188
|
+
one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
189
|
+
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
|
190
|
+
end
|
191
|
+
alias has_one one
|
192
|
+
|
193
|
+
# Set Many association
|
194
|
+
#
|
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] option hash including `if` that is a condition to render
|
200
|
+
# @param block [Block]
|
201
|
+
# @see Alba::Many#initialize
|
202
|
+
def many(name, condition = nil, resource: nil, key: nil, **options, &block)
|
203
|
+
nesting = self.name&.rpartition('::')&.first
|
204
|
+
many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
205
|
+
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
|
206
|
+
end
|
207
|
+
alias has_many many
|
208
|
+
|
209
|
+
# Set key
|
210
|
+
#
|
211
|
+
# @param key [String, Symbol]
|
212
|
+
def key(key)
|
213
|
+
@_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
119
214
|
end
|
120
215
|
|
121
|
-
|
122
|
-
|
216
|
+
# Set key to true
|
217
|
+
#
|
218
|
+
def key!
|
219
|
+
@_key = true
|
123
220
|
end
|
124
221
|
|
222
|
+
# Delete attributes
|
125
223
|
# Use this DSL in child class to ignore certain attributes
|
224
|
+
#
|
225
|
+
# @param attributes [Array<String, Symbol>]
|
126
226
|
def ignoring(*attributes)
|
127
227
|
attributes.each do |attr_name|
|
128
228
|
@_attributes.delete(attr_name.to_sym)
|
129
229
|
end
|
130
230
|
end
|
231
|
+
|
232
|
+
# Transform keys as specified type
|
233
|
+
#
|
234
|
+
# @param type [String, Symbol]
|
235
|
+
def transform_keys(type)
|
236
|
+
@_transform_keys = type.to_sym
|
237
|
+
end
|
238
|
+
|
239
|
+
# Set error handler
|
240
|
+
#
|
241
|
+
# @param [Symbol] handler
|
242
|
+
# @param [Block]
|
243
|
+
def on_error(handler = nil, &block)
|
244
|
+
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
245
|
+
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
246
|
+
|
247
|
+
@_on_error = handler || block
|
248
|
+
end
|
131
249
|
end
|
132
250
|
end
|
133
251
|
end
|