alba 0.11.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|