alba 1.3.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.
data/lib/alba.rb CHANGED
@@ -1,19 +1,15 @@
1
+ require 'json'
1
2
  require_relative 'alba/version'
3
+ require_relative 'alba/errors'
2
4
  require_relative 'alba/resource'
5
+ require_relative 'alba/deprecation'
3
6
 
4
7
  # Core module
5
8
  module Alba
6
- # Base class for Errors
7
- class Error < StandardError; end
8
-
9
- # Error class for backend which is not supported
10
- class UnsupportedBackend < Error; end
11
-
12
- # Error class for type which is not supported
13
- class UnsupportedType < Error; end
14
-
15
9
  class << self
16
- attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
10
+ attr_reader :backend, :encoder, :inferring, :_on_error, :_on_nil, :transforming_root_key
11
+
12
+ # Accessor for inflector, a module responsible for incflecting strings
17
13
  attr_accessor :inflector
18
14
 
19
15
  # Set the backend, which actually serializes object into JSON
@@ -24,33 +20,48 @@ module Alba
24
20
  # @raise [Alba::UnsupportedBackend] if backend is not supported
25
21
  def backend=(backend)
26
22
  @backend = backend&.to_sym
27
- set_encoder
23
+ set_encoder_from_backend
24
+ end
25
+
26
+ # Set encoder, a Proc object that accepts an object and generates JSON from it
27
+ # Set backend as `:custom` which indicates no preset encoder is used
28
+ #
29
+ # @param encoder [Proc]
30
+ # @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
31
+ def encoder=(encoder)
32
+ raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
33
+
34
+ @encoder = encoder
35
+ @backend = :custom
28
36
  end
29
37
 
30
38
  # Serialize the object with inline definitions
31
39
  #
32
40
  # @param object [Object] the object to be serialized
33
- # @param key [Symbol]
41
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
42
+ # @param root_key [Symbol, nil, true]
34
43
  # @param block [Block] resource block
35
44
  # @return [String] serialized JSON string
36
45
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
37
- def serialize(object, key: nil, &block)
38
- raise ArgumentError, 'Block required' unless block
46
+ def serialize(object, key: nil, root_key: nil, &block)
47
+ Alba::Deprecation.warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
48
+ klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
39
49
 
40
- klass = Class.new
41
- klass.include(Alba::Resource)
42
- klass.class_eval(&block)
43
50
  resource = klass.new(object)
44
- resource.serialize(key: key)
51
+ resource.serialize(root_key: root_key || key)
45
52
  end
46
53
 
47
54
  # Enable inference for key and resource name
48
- def enable_inference!
49
- begin
50
- require 'active_support/inflector'
51
- rescue LoadError
52
- raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
55
+ #
56
+ # @param with [Symbol, Class, Module] inflector
57
+ # When it's a Symbol, it sets inflector with given name
58
+ # When it's a Class or a Module, it sets given object to inflector
59
+ def enable_inference!(with: :default)
60
+ if with == :default
61
+ Alba::Deprecation.warn 'Calling `enable_inference!` without `with` keyword argument is deprecated. Pass `:active_support` to keep current behavior.'
53
62
  end
63
+
64
+ @inflector = inflector_from(with)
54
65
  @inferring = true
55
66
  end
56
67
 
@@ -63,35 +74,93 @@ module Alba
63
74
  #
64
75
  # @param [Symbol] handler
65
76
  # @param [Block]
77
+ # @raise [ArgumentError] if both handler and block params exist
78
+ # @raise [ArgumentError] if both handler and block params don't exist
79
+ # @deprecated Use `Resource.on_error` instead
80
+ # @return [void]
66
81
  def on_error(handler = nil, &block)
82
+ Alba::Deprecation.warn '`Alba.on_error` is deprecated, use `on_error` on resource class instead.'
67
83
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
68
84
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
69
85
 
70
86
  @_on_error = handler || block
71
87
  end
72
88
 
89
+ # Set nil handler
90
+ #
91
+ # @param block [Block]
92
+ # @return [void]
93
+ # @deprecated Use `Resource.on_nil` instead
94
+ def on_nil(&block)
95
+ Alba::Deprecation.warn '`Alba.on_nil` is deprecated, use `on_nil` on resource class instead.'
96
+ @_on_nil = block
97
+ end
98
+
73
99
  # Enable root key transformation
100
+ #
101
+ # @deprecated Use `Resource.transform_keys` with `root` option instead
74
102
  def enable_root_key_transformation!
103
+ Alba::Deprecation.warn '`Alba.enable_root_key_transformation!` is deprecated, use `transform_keys` on resource class instead.'
75
104
  @transforming_root_key = true
76
105
  end
77
106
 
78
107
  # Disable root key transformation
108
+ #
109
+ # @deprecated Use `Resource.transform_keys` with `root` option instead
79
110
  def disable_root_key_transformation!
111
+ Alba::Deprecation.warn '`Alba.disable_root_key_transformation!` is deprecated, use `transform_keys` on resource class instead.'
80
112
  @transforming_root_key = false
81
113
  end
82
114
 
115
+ # @param block [Block] resource body
116
+ # @return [Class<Alba::Resource>] resource class
117
+ def resource_class(&block)
118
+ klass = Class.new
119
+ klass.include(Alba::Resource)
120
+ klass.class_eval(&block)
121
+ klass
122
+ end
123
+
124
+ # @param name [String] a String Alba infers resource name with
125
+ # @param nesting [String, nil] namespace Alba tries to find resource class in
126
+ # @return [Class<Alba::Resource>] resource class
127
+ def infer_resource_class(name, nesting: nil)
128
+ raise Alba::Error, 'Inference is disabled so Alba cannot infer resource name. Use `Alba.enable_inference!` before use.' unless Alba.inferring
129
+
130
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
131
+ const_parent.const_get("#{inflector.classify(name)}Resource")
132
+ end
133
+
134
+ # Reset config variables
135
+ # Useful for test cleanup
136
+ def reset!
137
+ @encoder = default_encoder
138
+ @_on_error = :raise
139
+ @_on_nil = nil
140
+ @transforming_root_key = false # TODO: This will be true since 2.0
141
+ end
142
+
83
143
  private
84
144
 
85
- def set_encoder
145
+ def inflector_from(name_or_module)
146
+ case name_or_module
147
+ when :default, :active_support
148
+ require_relative 'alba/default_inflector'
149
+ Alba::DefaultInflector
150
+ when :dry
151
+ require 'dry/inflector'
152
+ Dry::Inflector.new
153
+ else
154
+ validate_inflector(name_or_module)
155
+ end
156
+ end
157
+
158
+ def set_encoder_from_backend
86
159
  @encoder = case @backend
87
- when :oj, :oj_strict
88
- try_oj
89
- when :oj_rails
90
- try_oj(mode: :rails)
91
- when :active_support
92
- try_active_support
93
- when nil, :default, :json
94
- default_encoder
160
+ when :oj, :oj_strict then try_oj
161
+ when :oj_rails then try_oj(mode: :rails)
162
+ when :active_support then try_active_support
163
+ when nil, :default, :json then default_encoder
95
164
  else
96
165
  raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
97
166
  end
@@ -115,13 +184,18 @@ module Alba
115
184
 
116
185
  def default_encoder
117
186
  lambda do |hash|
118
- require 'json'
119
187
  JSON.dump(hash)
120
188
  end
121
189
  end
190
+
191
+ def validate_inflector(inflector)
192
+ unless %i[camelize camelize_lower dasherize classify].all? { |m| inflector.respond_to?(m) }
193
+ raise Alba::Error, "Given inflector, #{inflector.inspect} is not valid. It must implement `camelize`, `camelize_lower`, `dasherize` and `classify`."
194
+ end
195
+
196
+ inflector
197
+ end
122
198
  end
123
199
 
124
- @encoder = default_encoder
125
- @_on_error = :raise
126
- @transforming_root_key = false # TODO: This will be true since 2.0
200
+ reset!
127
201
  end
@@ -0,0 +1,174 @@
1
+ # Benchmark script to run varieties of JSON serializers
2
+ # Fetch Alba from local, otherwise fetch latest from RubyGems
3
+ # exit(status)
4
+
5
+ # --- Bundle dependencies ---
6
+
7
+ require "bundler/inline"
8
+
9
+ gemfile(true) do
10
+ source "https://rubygems.org"
11
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
12
+
13
+ gem "activerecord", "~> 6.1.3"
14
+ gem "alba", path: '../'
15
+ gem "benchmark-ips"
16
+ gem "blueprinter"
17
+ gem "jbuilder"
18
+ gem "multi_json"
19
+ gem "oj"
20
+ gem "sqlite3"
21
+ end
22
+
23
+ # --- Test data model setup ---
24
+
25
+ require "active_record"
26
+ require "oj"
27
+ require "sqlite3"
28
+ Oj.optimize_rails
29
+
30
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
31
+
32
+ ActiveRecord::Schema.define do
33
+ create_table :posts, force: true do |t|
34
+ t.string :body
35
+ end
36
+
37
+ create_table :comments, force: true do |t|
38
+ t.integer :post_id
39
+ t.string :body
40
+ t.integer :commenter_id
41
+ end
42
+
43
+ create_table :users, force: true do |t|
44
+ t.string :name
45
+ end
46
+ end
47
+
48
+ class Post < ActiveRecord::Base
49
+ has_many :comments
50
+ has_many :commenters, through: :comments, class_name: 'User', source: :commenter
51
+
52
+ def attributes
53
+ {id: nil, body: nil, commenter_names: commenter_names}
54
+ end
55
+
56
+ def commenter_names
57
+ commenters.pluck(:name)
58
+ end
59
+ end
60
+
61
+ class Comment < ActiveRecord::Base
62
+ belongs_to :post
63
+ belongs_to :commenter, class_name: 'User'
64
+
65
+ def attributes
66
+ {id: nil, body: nil}
67
+ end
68
+ end
69
+
70
+ class User < ActiveRecord::Base
71
+ has_many :comments
72
+ end
73
+
74
+ # --- Alba serializers ---
75
+
76
+ require "alba"
77
+
78
+ class AlbaCommentResource
79
+ include ::Alba::Resource
80
+ attributes :id, :body
81
+ end
82
+
83
+ class AlbaPostResource
84
+ include ::Alba::Resource
85
+ attributes :id, :body
86
+ attribute :commenter_names do |post|
87
+ post.commenters.pluck(:name)
88
+ end
89
+ many :comments, resource: AlbaCommentResource
90
+ end
91
+
92
+ # --- Blueprint serializers ---
93
+
94
+ require "blueprinter"
95
+
96
+ class CommentBlueprint < Blueprinter::Base
97
+ fields :id, :body
98
+ end
99
+
100
+ class PostBlueprint < Blueprinter::Base
101
+ fields :id, :body, :commenter_names
102
+ association :comments, blueprint: CommentBlueprint
103
+
104
+ def commenter_names
105
+ commenters.pluck(:name)
106
+ end
107
+ end
108
+
109
+ # --- JBuilder serializers ---
110
+
111
+ require "jbuilder"
112
+
113
+ class Post
114
+ def to_builder
115
+ Jbuilder.new do |post|
116
+ post.call(self, :id, :body, :commenter_names, :comments)
117
+ end
118
+ end
119
+
120
+ def commenter_names
121
+ commenters.pluck(:name)
122
+ end
123
+ end
124
+
125
+ class Comment
126
+ def to_builder
127
+ Jbuilder.new do |comment|
128
+ comment.call(self, :id, :body)
129
+ end
130
+ end
131
+ end
132
+
133
+ # --- Test data creation ---
134
+
135
+ 100.times do |i|
136
+ post = Post.create!(body: "post#{i}")
137
+ user1 = User.create!(name: "John#{i}")
138
+ user2 = User.create!(name: "Jane#{i}")
139
+ 10.times do |n|
140
+ post.comments.create!(commenter: user1, body: "Comment1_#{i}_#{n}")
141
+ post.comments.create!(commenter: user2, body: "Comment2_#{i}_#{n}")
142
+ end
143
+ end
144
+
145
+ posts = Post.all.to_a
146
+
147
+ # --- Store the serializers in procs ---
148
+
149
+ alba = Proc.new { AlbaPostResource.new(posts).serialize }
150
+ blueprinter = Proc.new { PostBlueprint.render(posts) }
151
+ jbuilder = Proc.new do
152
+ Jbuilder.new do |json|
153
+ json.array!(posts) do |post|
154
+ json.post post.to_builder
155
+ end
156
+ end.target!
157
+ end
158
+
159
+ # --- Run the benchmarks ---
160
+
161
+ require 'benchmark/ips'
162
+ result = Benchmark.ips do |x|
163
+ x.report(:alba, &alba)
164
+ x.report(:blueprinter, &blueprinter)
165
+ x.report(:jbuilder, &jbuilder)
166
+ end
167
+
168
+ entries = result.entries.map {|entry| [entry.label, entry.iterations]}
169
+ alba_ips = entries.find {|e| e.first == :alba }.last
170
+ blueprinter_ips = entries.find {|e| e.first == :blueprinter }.last
171
+ jbuidler_ips = entries.find {|e| e.first == :jbuilder }.last
172
+ # Alba should be as fast as jbuilder and faster than blueprinter
173
+ alba_is_fast_enough = (alba_ips - jbuidler_ips) > -10.0 && (alba_ips - blueprinter_ips) > 10.0
174
+ exit(alba_is_fast_enough)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OKURA Masafumi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-31 00:00:00.000000000 Z
11
+ date: 2022-03-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
14
14
  flexibility and usability.
@@ -21,7 +21,9 @@ files:
21
21
  - ".github/ISSUE_TEMPLATE/bug_report.md"
22
22
  - ".github/ISSUE_TEMPLATE/feature_request.md"
23
23
  - ".github/dependabot.yml"
24
+ - ".github/workflows/codeql-analysis.yml"
24
25
  - ".github/workflows/main.yml"
26
+ - ".github/workflows/perf.yml"
25
27
  - ".gitignore"
26
28
  - ".rubocop.yml"
27
29
  - ".yardopts"
@@ -38,26 +40,30 @@ files:
38
40
  - bin/console
39
41
  - bin/setup
40
42
  - codecov.yml
43
+ - docs/migrate_from_active_model_serializers.md
44
+ - docs/migrate_from_jbuilder.md
41
45
  - gemfiles/all.gemfile
42
46
  - gemfiles/without_active_support.gemfile
43
47
  - gemfiles/without_oj.gemfile
44
48
  - lib/alba.rb
45
49
  - lib/alba/association.rb
46
50
  - lib/alba/default_inflector.rb
47
- - lib/alba/key_transform_factory.rb
48
- - lib/alba/many.rb
49
- - lib/alba/one.rb
51
+ - lib/alba/deprecation.rb
52
+ - lib/alba/errors.rb
50
53
  - lib/alba/resource.rb
51
54
  - lib/alba/typed_attribute.rb
52
55
  - lib/alba/version.rb
56
+ - script/perf_check.rb
53
57
  - sider.yml
54
58
  homepage: https://github.com/okuramasafumi/alba
55
59
  licenses:
56
60
  - MIT
57
61
  metadata:
58
- homepage_uri: https://github.com/okuramasafumi/alba
62
+ bug_tracker_uri: https://github.com/okuramasafumi/issues
63
+ changelog_uri: https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md
64
+ documentation_uri: https://rubydoc.info/github/okuramasafumi/alba
59
65
  source_code_uri: https://github.com/okuramasafumi/alba
60
- changelog_uri: https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md
66
+ rubygems_mfa_required: 'true'
61
67
  post_install_message:
62
68
  rdoc_options: []
63
69
  require_paths:
@@ -73,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
79
  - !ruby/object:Gem::Version
74
80
  version: '0'
75
81
  requirements: []
76
- rubygems_version: 3.2.16
82
+ rubygems_version: 3.3.5
77
83
  signing_key:
78
84
  specification_version: 4
79
85
  summary: Alba is the fastest JSON serializer for Ruby.
@@ -1,33 +0,0 @@
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
- # @params 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 DELETED
@@ -1,21 +0,0 @@
1
- require_relative 'association'
2
-
3
- module Alba
4
- # Representing many association
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 within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
10
- # @param params [Hash] user-given Hash for arbitrary data
11
- # @return [Array<Hash>]
12
- def to_hash(target, within: nil, params: {})
13
- @object = target.public_send(@name)
14
- @object = @condition.call(@object, params) if @condition
15
- return if @object.nil?
16
-
17
- @resource = constantize(@resource)
18
- @resource.new(@object, params: params, within: within).to_hash
19
- end
20
- end
21
- end
data/lib/alba/one.rb DELETED
@@ -1,21 +0,0 @@
1
- require_relative 'association'
2
-
3
- module Alba
4
- # Representing one association
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 within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
10
- # @param params [Hash] user-given Hash for arbitrary data
11
- # @return [Hash]
12
- def to_hash(target, within: nil, params: {})
13
- @object = target.public_send(@name)
14
- @object = @condition.call(object, params) if @condition
15
- return if @object.nil?
16
-
17
- @resource = constantize(@resource)
18
- @resource.new(object, params: params, within: within).to_hash
19
- end
20
- end
21
- end