alba 1.3.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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