alba 1.1.0 → 1.5.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,5 +1,7 @@
1
+ require 'json'
1
2
  require_relative 'alba/version'
2
3
  require_relative 'alba/resource'
4
+ require_relative 'alba/deprecation'
3
5
 
4
6
  # Core module
5
7
  module Alba
@@ -9,8 +11,14 @@ module Alba
9
11
  # Error class for backend which is not supported
10
12
  class UnsupportedBackend < Error; end
11
13
 
14
+ # Error class for type which is not supported
15
+ class UnsupportedType < Error; end
16
+
12
17
  class << self
13
- attr_reader :backend, :encoder, :inferring, :_on_error
18
+ attr_reader :backend, :encoder, :inferring, :_on_error, :_on_nil, :transforming_root_key
19
+
20
+ # Accessor for inflector, a module responsible for incflecting strings
21
+ attr_accessor :inflector
14
22
 
15
23
  # Set the backend, which actually serializes object into JSON
16
24
  #
@@ -20,24 +28,35 @@ module Alba
20
28
  # @raise [Alba::UnsupportedBackend] if backend is not supported
21
29
  def backend=(backend)
22
30
  @backend = backend&.to_sym
23
- set_encoder
31
+ set_encoder_from_backend
32
+ end
33
+
34
+ # Set encoder, a Proc object that accepts an object and generates JSON from it
35
+ # Set backend as `:custom` which indicates no preset encoder is used
36
+ #
37
+ # @param encoder [Proc]
38
+ # @raise [ArgumentError] if given encoder is not a Proc or its arity is not one
39
+ def encoder=(encoder)
40
+ raise ArgumentError, 'Encoder must be a Proc accepting one argument' unless encoder.is_a?(Proc) && encoder.arity == 1
41
+
42
+ @encoder = encoder
43
+ @backend = :custom
24
44
  end
25
45
 
26
46
  # Serialize the object with inline definitions
27
47
  #
28
48
  # @param object [Object] the object to be serialized
29
- # @param key [Symbol]
49
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
50
+ # @param root_key [Symbol, nil, true]
30
51
  # @param block [Block] resource block
31
52
  # @return [String] serialized JSON string
32
53
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
33
- def serialize(object, key: nil, &block)
34
- raise ArgumentError, 'Block required' unless block
54
+ def serialize(object, key: nil, root_key: nil, &block)
55
+ Alba::Deprecation.warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
56
+ klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
35
57
 
36
- klass = Class.new
37
- klass.include(Alba::Resource)
38
- klass.class_eval(&block)
39
58
  resource = klass.new(object)
40
- resource.serialize(key: key)
59
+ resource.serialize(root_key: root_key || key)
41
60
  end
42
61
 
43
62
  # Enable inference for key and resource name
@@ -59,6 +78,9 @@ module Alba
59
78
  #
60
79
  # @param [Symbol] handler
61
80
  # @param [Block]
81
+ # @raise [ArgumentError] if both handler and block params exist
82
+ # @raise [ArgumentError] if both handler and block params don't exist
83
+ # @return [void]
62
84
  def on_error(handler = nil, &block)
63
85
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
64
86
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
@@ -66,18 +88,59 @@ module Alba
66
88
  @_on_error = handler || block
67
89
  end
68
90
 
91
+ # Set nil handler
92
+ #
93
+ # @param block [Block]
94
+ # @return [void]
95
+ def on_nil(&block)
96
+ @_on_nil = block
97
+ end
98
+
99
+ # Enable root key transformation
100
+ def enable_root_key_transformation!
101
+ @transforming_root_key = true
102
+ end
103
+
104
+ # Disable root key transformation
105
+ def disable_root_key_transformation!
106
+ @transforming_root_key = false
107
+ end
108
+
109
+ # @param block [Block] resource body
110
+ # @return [Class<Alba::Resource>] resource class
111
+ def resource_class(&block)
112
+ klass = Class.new
113
+ klass.include(Alba::Resource)
114
+ klass.class_eval(&block)
115
+ klass
116
+ end
117
+
118
+ # @param name [String] a String Alba infers resource name with
119
+ # @param nesting [String, nil] namespace Alba tries to find resource class in
120
+ # @return [Class<Alba::Resource>] resource class
121
+ def infer_resource_class(name, nesting: nil)
122
+ enable_inference!
123
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
124
+ const_parent.const_get("#{ActiveSupport::Inflector.classify(name)}Resource")
125
+ end
126
+
127
+ # Reset config variables
128
+ # Useful for test cleanup
129
+ def reset!
130
+ @encoder = default_encoder
131
+ @_on_error = :raise
132
+ @_on_nil = nil
133
+ @transforming_root_key = false # TODO: This will be true since 2.0
134
+ end
135
+
69
136
  private
70
137
 
71
- def set_encoder
138
+ def set_encoder_from_backend
72
139
  @encoder = case @backend
73
- when :oj, :oj_strict
74
- try_oj
75
- when :oj_rails
76
- try_oj(mode: :rails)
77
- when :active_support
78
- try_active_support
79
- when nil, :default, :json
80
- default_encoder
140
+ when :oj, :oj_strict then try_oj
141
+ when :oj_rails then try_oj(mode: :rails)
142
+ when :active_support then try_active_support
143
+ when nil, :default, :json then default_encoder
81
144
  else
82
145
  raise Alba::UnsupportedBackend, "Unsupported backend, #{backend}"
83
146
  end
@@ -101,12 +164,10 @@ module Alba
101
164
 
102
165
  def default_encoder
103
166
  lambda do |hash|
104
- require 'json'
105
167
  JSON.dump(hash)
106
168
  end
107
169
  end
108
170
  end
109
171
 
110
- @encoder = default_encoder
111
- @_on_error = :raise
172
+ reset!
112
173
  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)
data/sider.yml CHANGED
@@ -49,10 +49,8 @@ linter:
49
49
  # norc: true
50
50
 
51
51
  # # https://help.sider.review/getting-started/custom-configuration#ignore
52
- # ignore:
53
- # - "*.pdf"
54
- # - "*.mp4"
55
- # - "images/**"
52
+ ignore:
53
+ - 'test/**/*'
56
54
 
57
55
  # # https://help.sider.review/getting-started/custom-configuration#branchesexclude
58
56
  # branches:
metadata CHANGED
@@ -1,25 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.5.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-04-23 00:00:00.000000000 Z
11
+ date: 2021-11-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Alba is designed to be a simple, easy to use and fast alternative to
14
- existing JSON serializers. Its performance is better than almost all gems which
15
- do similar things. The internal is so simple that it's easy to hack and maintain.
13
+ description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
14
+ flexibility and usability.
16
15
  email:
17
16
  - masafumi.o1988@gmail.com
18
17
  executables: []
19
18
  extensions: []
20
19
  extra_rdoc_files: []
21
20
  files:
21
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
22
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
23
+ - ".github/dependabot.yml"
22
24
  - ".github/workflows/main.yml"
25
+ - ".github/workflows/perf.yml"
23
26
  - ".gitignore"
24
27
  - ".rubocop.yml"
25
28
  - ".yardopts"
@@ -29,21 +32,29 @@ files:
29
32
  - LICENSE.txt
30
33
  - README.md
31
34
  - Rakefile
35
+ - SECURITY.md
32
36
  - alba.gemspec
33
- - benchmark/local.rb
37
+ - benchmark/collection.rb
38
+ - benchmark/single_resource.rb
34
39
  - bin/console
35
40
  - bin/setup
36
41
  - codecov.yml
42
+ - docs/migrate_from_active_model_serializers.md
43
+ - docs/migrate_from_jbuilder.md
37
44
  - gemfiles/all.gemfile
38
45
  - gemfiles/without_active_support.gemfile
39
46
  - gemfiles/without_oj.gemfile
40
47
  - lib/alba.rb
41
48
  - lib/alba/association.rb
42
- - lib/alba/key_transformer.rb
49
+ - lib/alba/default_inflector.rb
50
+ - lib/alba/deprecation.rb
51
+ - lib/alba/key_transform_factory.rb
43
52
  - lib/alba/many.rb
44
53
  - lib/alba/one.rb
45
54
  - lib/alba/resource.rb
55
+ - lib/alba/typed_attribute.rb
46
56
  - lib/alba/version.rb
57
+ - script/perf_check.rb
47
58
  - sider.yml
48
59
  homepage: https://github.com/okuramasafumi/alba
49
60
  licenses:
@@ -51,7 +62,7 @@ licenses:
51
62
  metadata:
52
63
  homepage_uri: https://github.com/okuramasafumi/alba
53
64
  source_code_uri: https://github.com/okuramasafumi/alba
54
- changelog_uri: https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md
65
+ changelog_uri: https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md
55
66
  post_install_message:
56
67
  rdoc_options: []
57
68
  require_paths:
@@ -67,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
78
  - !ruby/object:Gem::Version
68
79
  version: '0'
69
80
  requirements: []
70
- rubygems_version: 3.2.16
81
+ rubygems_version: 3.2.22
71
82
  signing_key:
72
83
  specification_version: 4
73
84
  summary: Alba is the fastest JSON serializer for Ruby.
@@ -1,32 +0,0 @@
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
- key = key.to_s
20
- case transform_type
21
- when :camel
22
- ActiveSupport::Inflector.camelize(key)
23
- when :lower_camel
24
- ActiveSupport::Inflector.camelize(key, false)
25
- when :dash
26
- ActiveSupport::Inflector.dasherize(key)
27
- else
28
- raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
29
- end
30
- end
31
- end
32
- end