alba 1.1.0 → 1.5.0

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