alba 0.11.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,10 +1,13 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("gemfiles/all.gemfile") if ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") || ENV["BUNDLE_GEMFILE"].empty? || ENV["BUNDLE_GEMFILE"].nil?
5
+
4
6
  Rake::TestTask.new(:test) do |t|
5
7
  t.libs << "test"
6
8
  t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
9
+ file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("gemfiles/all.gemfile") ? FileList["test/**/*_test.rb"] : FileList["test/dependencies/test_dependencies.rb"]
10
+ t.test_files = file_list
8
11
  end
9
12
 
10
13
  task :default => :test
data/alba.gemspec CHANGED
@@ -10,11 +10,11 @@ Gem::Specification.new do |spec|
10
10
  spec.description = "Alba is designed to be a simple, easy to use and fast alternative to existing JSON serializers. Its performance is better than almost all gems which do similar things. The internal is so simple that it's easy to hack and maintain."
11
11
  spec.homepage = 'https://github.com/okuramasafumi/alba'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5.7')
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = 'https://github.com/okuramasafumi/alba'
17
- spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/CHANGELOG.md'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md'
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -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
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', require: false # For backend
4
+ gem 'minitest', '~> 5.14' # For test
5
+ gem 'rake', '~> 13.0' # For test and automation
6
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
7
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
8
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
9
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
10
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
11
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
12
+ gem 'simplecov-cobertura', require: false # For test coverage
13
+ gem 'yard', require: false
14
+
15
+ platforms :ruby do
16
+ gem 'oj', '~> 3.11', require: false # For backend
17
+ gem 'ruby-prof', require: false # For performance profiling
18
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'minitest', '~> 5.14' # For test
4
+ gem 'rake', '~> 13.0' # For test and automation
5
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
6
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
7
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
8
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
9
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
10
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
11
+ gem 'simplecov-cobertura', require: false # For test coverage
12
+ gem 'yard', require: false
13
+
14
+ platforms :ruby do
15
+ gem 'oj', '~> 3.11', require: false # For backend
16
+ gem 'ruby-prof', require: false # For performance profiling
17
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', require: false # For backend
4
+ gem 'minitest', '~> 5.14' # For test
5
+ gem 'rake', '~> 13.0' # For test and automation
6
+ gem 'rubocop', '>= 0.79.0', require: false # For lint
7
+ gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
8
+ gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
9
+ gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
10
+ gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
11
+ gem 'simplecov', '~> 0.21.0', require: false # For test coverage
12
+ gem 'simplecov-cobertura', require: false # For test coverage
13
+ gem 'yard', require: false
14
+
15
+ platforms :ruby do
16
+ gem 'ruby-prof', require: false # For performance profiling
17
+ end
data/lib/alba.rb CHANGED
@@ -1,17 +1,16 @@
1
1
  require_relative 'alba/version'
2
- require_relative 'alba/serializer'
3
2
  require_relative 'alba/resource'
4
3
 
5
4
  # Core module
6
5
  module Alba
7
6
  # Base class for Errors
8
7
  class Error < StandardError; end
8
+
9
9
  # Error class for backend which is not supported
10
10
  class UnsupportedBackend < Error; end
11
11
 
12
12
  class << self
13
- attr_reader :backend, :encoder
14
- attr_accessor :default_serializer
13
+ attr_reader :backend, :encoder, :inferring, :_on_error
15
14
 
16
15
  # Set the backend, which actually serializes object into JSON
17
16
  #
@@ -27,17 +26,44 @@ module Alba
27
26
  # Serialize the object with inline definitions
28
27
  #
29
28
  # @param object [Object] the object to be serialized
30
- # @param with [nil, Proc, Alba::Serializer] selializer
29
+ # @param key [Symbol]
31
30
  # @param block [Block] resource block
32
31
  # @return [String] serialized JSON string
33
32
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
34
- def serialize(object, with: nil, &block)
33
+ def serialize(object, key: nil, &block)
35
34
  raise ArgumentError, 'Block required' unless block
36
35
 
37
- resource_class.class_eval(&block)
38
- resource = resource_class.new(object)
39
- with ||= @default_serializer
40
- resource.serialize(with: with)
36
+ klass = Class.new
37
+ klass.include(Alba::Resource)
38
+ klass.class_eval(&block)
39
+ resource = klass.new(object)
40
+ resource.serialize(key: key)
41
+ end
42
+
43
+ # Enable inference for key and resource name
44
+ def enable_inference!
45
+ begin
46
+ require 'active_support/inflector'
47
+ rescue LoadError
48
+ raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
49
+ end
50
+ @inferring = true
51
+ end
52
+
53
+ # Disable inference for key and resource name
54
+ def disable_inference!
55
+ @inferring = false
56
+ end
57
+
58
+ # Set error handler
59
+ #
60
+ # @param [Symbol] handler
61
+ # @param [Block]
62
+ def on_error(handler = nil, &block)
63
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
64
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
65
+
66
+ @_on_error = handler || block
41
67
  end
42
68
 
43
69
  private
@@ -59,6 +85,7 @@ module Alba
59
85
  require 'oj'
60
86
  ->(hash) { Oj.dump(hash, mode: :strict) }
61
87
  rescue LoadError
88
+ Kernel.warn '`Oj` is not installed, falling back to default JSON encoder.'
62
89
  default_encoder
63
90
  end
64
91
 
@@ -66,6 +93,7 @@ module Alba
66
93
  require 'active_support/json'
67
94
  ->(hash) { ActiveSupport::JSON.encode(hash) }
68
95
  rescue LoadError
96
+ Kernel.warn '`ActiveSupport` is not installed, falling back to default JSON encoder.'
69
97
  default_encoder
70
98
  end
71
99
 
@@ -75,14 +103,8 @@ module Alba
75
103
  JSON.dump(hash)
76
104
  end
77
105
  end
78
-
79
- def resource_class
80
- @resource_class ||= begin
81
- klass = Class.new
82
- klass.include(Alba::Resource)
83
- end
84
- end
85
106
  end
86
107
 
87
108
  @encoder = default_encoder
109
+ @_on_error = :raise
88
110
  end
@@ -2,25 +2,40 @@ module Alba
2
2
  # Base class for `One` and `Many`
3
3
  # Child class should implement `to_hash` method
4
4
  class Association
5
+ attr_reader :object
6
+
5
7
  # @param name [Symbol] name of the method to fetch association
6
8
  # @param condition [Proc] a proc filtering data
7
9
  # @param resource [Class<Alba::Resource>] a resource class for the association
8
10
  # @param block [Block] used to define resource when resource arg is absent
9
- def initialize(name:, condition: nil, resource: nil, &block)
11
+ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
10
12
  @name = name
11
13
  @condition = condition
12
14
  @block = block
13
- @resource = resource || resource_class
14
- raise ArgumentError, 'resource or block is required' if @resource.nil? && @block.nil?
15
- end
15
+ @resource = resource
16
+ return if @resource
16
17
 
17
- # @abstract
18
- def to_hash
19
- :not_implemented
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
20
26
  end
21
27
 
22
28
  private
23
29
 
30
+ def constantize(resource)
31
+ case resource # rubocop:disable Style/MissingElse
32
+ when Class
33
+ resource
34
+ when Symbol, String
35
+ Object.const_get(resource)
36
+ end
37
+ end
38
+
24
39
  def resource_class
25
40
  klass = Class.new
26
41
  klass.include(Alba::Resource)
@@ -0,0 +1,32 @@
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