alba 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbb5260b5a6f3697f2e9c42ba198e3e17a1b0dc2aa0ea1bd2feb9ed4b18364a2
4
- data.tar.gz: 794c146a493068865d7284de00bedda11c3b2fabd2ce7dccaa781e72181d957e
3
+ metadata.gz: b2c2e8c84ddccf4db9f9f18dd155361567f2a1733c55f817f5b4d97d573675d5
4
+ data.tar.gz: 3f11dd120c8b57aef909d79f6570482b8af810cd19c57ddcaa0d7b2ee70c2d6b
5
5
  SHA512:
6
- metadata.gz: 46d179154f3981879a4cda7f5ea0360d7eeb3481788d895acefcbb5df156fde10ee1b0eb78bd399ab247449ea85100aceaa2dcb757a54ba64cce9450d82d1825
7
- data.tar.gz: a3ee5f28b7bbc626d4dcf92db32017eba85f60df42df02f07d464cdaa8c190f5c07914db5e9f7db21f73fd1356529add373ef2c79ac82038d22f2c5dd5ee273c
6
+ metadata.gz: 8948a681fb88b3d84e3751e6df0f5ca28314d6c9b5e183666780be95b14fd3c7728e6ad1eb13309d4b7114115b56edbb28fe929c2376bbcc5b762846a7d00404
7
+ data.tar.gz: 9a2430efc4cf3b7a24b27bb40228dad67e00bd0cb10c9e0fc3764eb49e9caafda5b6d9fa0cc0335c77edc42e0a60416b20343dee5cee23dcfbfe350bed90e9a6
data/.rubocop.yml CHANGED
@@ -49,7 +49,8 @@ Metrics/MethodLength:
49
49
 
50
50
  # `Resource` module is a core module and its length tends to be long...
51
51
  Metrics/ModuleLength:
52
- Max: 150
52
+ Exclude:
53
+ - 'lib/alba/resource.rb'
53
54
 
54
55
  # Resource class includes DSLs, which tend to accept long list of parameters
55
56
  Metrics/ParameterLists:
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] 2021-05-31
10
+
11
+ - [Perf] Improve performance for `many` [641d8f9]
12
+ - https://github.com/okuramasafumi/alba/pull/125
13
+ - [Feat] Add custom inflector feature (#126) [ad73291]
14
+ - https://github.com/okuramasafumi/alba/pull/126
15
+ - Thank you @wuarmin !
16
+ - [Feat] Support params in if condition [6e9915e]
17
+ - https://github.com/okuramasafumi/alba/pull/128
18
+ - [Fix] fundamentally broken "circular association control" [fbbc9a1]
19
+ - https://github.com/okuramasafumi/alba/pull/130
20
+
9
21
  ## [1.2.0] 2021-05-09
10
22
 
11
23
  - [Fix] multiple word key inference [6c18e73]
data/README.md CHANGED
@@ -66,7 +66,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
66
66
  * Error handling
67
67
  * Resource name inflection based on association name
68
68
  * Circular associations control
69
- * Types for validation and conversion
69
+ * [Experimental] Types for validation and conversion
70
70
  * No runtime dependencies
71
71
 
72
72
  ## Anti features
@@ -246,9 +246,11 @@ end
246
246
 
247
247
  ### Key transformation
248
248
 
249
- ** Note: You need to install `active_support` gem to use `transform_keys` DSL.
249
+ If you want to use `transform_keys` DSL and you already have `active_support` installed, key transformation will work out of the box, using `ActiveSupport::Inflector`. If `active_support` is not around, you have 2 possibilities:
250
+ * install it
251
+ * use a [custom inflector](#custom-inflector)
250
252
 
251
- With `active_support` installed, you can transform attribute keys.
253
+ With `transform_keys` DSL, you can transform attribute keys.
252
254
 
253
255
  ```ruby
254
256
  class User
@@ -309,6 +311,34 @@ This behavior to transform root key will become default at version 2.
309
311
 
310
312
  Supported transformation types are :camel, :lower_camel and :dash.
311
313
 
314
+ #### Custom inflector
315
+
316
+ A custom inflector can be plugged in as follows...
317
+ ```ruby
318
+ Alba.inflector = MyCustomInflector
319
+ ```
320
+ ...and has to implement following interface (the parameter `key` is of type `String`):
321
+ ```ruby
322
+ module InflectorInterface
323
+ def camelize(key)
324
+ raise "Not implemented"
325
+ end
326
+
327
+ def camelize_lower(key)
328
+ raise "Not implemented"
329
+ end
330
+
331
+ def dasherize(key)
332
+ raise "Not implemented"
333
+ end
334
+ end
335
+
336
+ ```
337
+ For example you could use `Dry::Inflector`, which implements exactly the above interface. If you are developing a `Hanami`-Application `Dry::Inflector` is around. In this case the following would be sufficient:
338
+ ```ruby
339
+ Alba.inflector = Dry::Inflector.new
340
+ ```
341
+
312
342
  ### Filtering attributes
313
343
 
314
344
  You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky.
@@ -482,11 +512,13 @@ end
482
512
 
483
513
  ### Circular associations control
484
514
 
515
+ **Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**
516
+
485
517
  You can control circular associations with `within` option. `within` option is a nested Hash such as `{book: {authors: books}}`. In this example, Alba serializes a book's authors' books. This means you can reference `BookResource` from `AuthorResource` and vice versa. This is really powerful when you have a complex data structure and serialize certain parts of it.
486
518
 
487
519
  For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/master/test/usecases/circular_association_test.rb)
488
520
 
489
- ### Types
521
+ ### Experimental support of types
490
522
 
491
523
  You can validate and convert input with types.
492
524
 
@@ -525,6 +557,8 @@ UserResource.new(user).serialize
525
557
  # => TypeError, 'Attribute bio is expected to be String but actually nil.'
526
558
  ```
527
559
 
560
+ Note that this feature is experimental and interfaces are subject to change.
561
+
528
562
  ### Caching
529
563
 
530
564
  Currently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).
data/alba.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ['masafumi.o1988@gmail.com']
8
8
 
9
9
  spec.summary = 'Alba is the fastest JSON serializer for Ruby.'
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."
10
+ spec.description = "Alba is the fastest JSON serializer for Ruby. It focuses on performance, flexibility and usability."
11
11
  spec.homepage = 'https://github.com/okuramasafumi/alba'
12
12
  spec.license = 'MIT'
13
13
  spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
@@ -0,0 +1,392 @@
1
+ # Benchmark script to run varieties of JSON serializers
2
+ # Fetch Alba from local, otherwise fetch latest from RubyGems
3
+
4
+ # --- Bundle dependencies ---
5
+
6
+ require "bundler/inline"
7
+
8
+ gemfile(true) do
9
+ source "https://rubygems.org"
10
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
11
+
12
+ gem "active_model_serializers"
13
+ gem "activerecord", "6.1.3"
14
+ gem "alba", path: '../'
15
+ gem "benchmark-ips"
16
+ gem "benchmark-memory"
17
+ gem "blueprinter"
18
+ gem "jbuilder"
19
+ gem "jsonapi-serializer" # successor of fast_jsonapi
20
+ gem "multi_json"
21
+ gem "primalize"
22
+ gem "oj"
23
+ gem "representable"
24
+ gem "simple_ams"
25
+ gem "sqlite3"
26
+ end
27
+
28
+ # --- Test data model setup ---
29
+
30
+ require "active_record"
31
+ require "logger"
32
+ require "oj"
33
+ require "sqlite3"
34
+ Oj.optimize_rails
35
+
36
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
37
+ # ActiveRecord::Base.logger = Logger.new($stdout)
38
+
39
+ ActiveRecord::Schema.define do
40
+ create_table :posts, force: true do |t|
41
+ t.string :body
42
+ end
43
+
44
+ create_table :comments, force: true do |t|
45
+ t.integer :post_id
46
+ t.string :body
47
+ t.integer :commenter_id
48
+ end
49
+
50
+ create_table :users, force: true do |t|
51
+ t.string :name
52
+ end
53
+ end
54
+
55
+ class Post < ActiveRecord::Base
56
+ has_many :comments
57
+ has_many :commenters, through: :comments, class_name: 'User', source: :commenter
58
+
59
+ def attributes
60
+ {id: nil, body: nil, commenter_names: commenter_names}
61
+ end
62
+
63
+ def commenter_names
64
+ commenters.pluck(:name)
65
+ end
66
+ end
67
+
68
+ class Comment < ActiveRecord::Base
69
+ belongs_to :post
70
+ belongs_to :commenter, class_name: 'User'
71
+
72
+ def attributes
73
+ {id: nil, body: nil}
74
+ end
75
+ end
76
+
77
+ class User < ActiveRecord::Base
78
+ has_many :comments
79
+ end
80
+
81
+ # --- Alba serializers ---
82
+
83
+ require "alba"
84
+
85
+ class AlbaCommentResource
86
+ include ::Alba::Resource
87
+ attributes :id, :body
88
+ end
89
+
90
+ class AlbaPostResource
91
+ include ::Alba::Resource
92
+ attributes :id, :body
93
+ attribute :commenter_names do |post|
94
+ post.commenters.pluck(:name)
95
+ end
96
+ many :comments, resource: AlbaCommentResource
97
+ end
98
+
99
+ # --- ActiveModelSerializer serializers ---
100
+
101
+ require "active_model_serializers"
102
+
103
+ ActiveModelSerializers.logger = Logger.new(nil)
104
+
105
+ class AMSCommentSerializer < ActiveModel::Serializer
106
+ attributes :id, :body
107
+ end
108
+
109
+ class AMSPostSerializer < ActiveModel::Serializer
110
+ attributes :id, :body
111
+ attribute :commenter_names
112
+ has_many :comments, serializer: AMSCommentSerializer
113
+
114
+ def commenter_names
115
+ object.commenters.pluck(:name)
116
+ end
117
+ end
118
+
119
+ # --- Blueprint serializers ---
120
+
121
+ require "blueprinter"
122
+
123
+ class CommentBlueprint < Blueprinter::Base
124
+ fields :id, :body
125
+ end
126
+
127
+ class PostBlueprint < Blueprinter::Base
128
+ fields :id, :body, :commenter_names
129
+ association :comments, blueprint: CommentBlueprint
130
+
131
+ def commenter_names
132
+ commenters.pluck(:name)
133
+ end
134
+ end
135
+
136
+ # --- JBuilder serializers ---
137
+
138
+ require "jbuilder"
139
+
140
+ class Post
141
+ def to_builder
142
+ Jbuilder.new do |post|
143
+ post.call(self, :id, :body, :commenter_names, :comments)
144
+ end
145
+ end
146
+
147
+ def commenter_names
148
+ commenters.pluck(:name)
149
+ end
150
+ end
151
+
152
+ class Comment
153
+ def to_builder
154
+ Jbuilder.new do |comment|
155
+ comment.call(self, :id, :body)
156
+ end
157
+ end
158
+ end
159
+
160
+ # --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
161
+
162
+ class JsonApiStandardCommentSerializer
163
+ include JSONAPI::Serializer
164
+
165
+ attribute :id
166
+ attribute :body
167
+ end
168
+
169
+ class JsonApiStandardPostSerializer
170
+ include JSONAPI::Serializer
171
+
172
+ # set_type :post # optional
173
+ attribute :id
174
+ attribute :body
175
+ attribute :commenter_names
176
+
177
+ attribute :comments do |post|
178
+ post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
179
+ end
180
+ end
181
+
182
+ # --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
183
+
184
+ # code to convert from JSON:API output to "flat" JSON, like the other serializers build
185
+ class JsonApiSameFormatSerializer
186
+ include JSONAPI::Serializer
187
+
188
+ def as_json(*_options)
189
+ hash = serializable_hash
190
+
191
+ if hash[:data].is_a? Hash
192
+ hash[:data][:attributes]
193
+
194
+ elsif hash[:data].is_a? Array
195
+ hash[:data].pluck(:attributes)
196
+
197
+ elsif hash[:data].nil?
198
+ { }
199
+
200
+ else
201
+ raise "unexpected data type #{hash[:data].class}"
202
+ end
203
+ end
204
+ end
205
+
206
+ class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
207
+ attribute :id
208
+ attribute :body
209
+ end
210
+
211
+ class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
212
+ attribute :id
213
+ attribute :body
214
+ attribute :commenter_names
215
+
216
+ attribute :comments do |post|
217
+ post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
218
+ end
219
+ end
220
+
221
+ # --- Primalize serializers ---
222
+ #
223
+ class PrimalizeCommentResource < Primalize::Single
224
+ attributes id: integer, body: string
225
+ end
226
+
227
+ class PrimalizePostResource < Primalize::Single
228
+ alias post object
229
+
230
+ attributes(
231
+ id: integer,
232
+ body: string,
233
+ comments: array(primalize(PrimalizeCommentResource)),
234
+ commenter_names: array(string),
235
+ )
236
+
237
+ def commenter_names
238
+ post.commenters.pluck(:name)
239
+ end
240
+ end
241
+
242
+ class PrimalizePostsResource < Primalize::Many
243
+ attributes posts: enumerable(PrimalizePostResource)
244
+ end
245
+
246
+ # --- Representable serializers ---
247
+
248
+ require "representable"
249
+
250
+ class CommentRepresenter < Representable::Decorator
251
+ include Representable::JSON
252
+
253
+ property :id
254
+ property :body
255
+ end
256
+
257
+ class PostsRepresenter < Representable::Decorator
258
+ include Representable::JSON::Collection
259
+
260
+ items class: Post do
261
+ property :id
262
+ property :body
263
+ property :commenter_names
264
+ collection :comments
265
+ end
266
+
267
+ def commenter_names
268
+ commenters.pluck(:name)
269
+ end
270
+ end
271
+
272
+ # --- SimpleAMS serializers ---
273
+
274
+ require "simple_ams"
275
+
276
+ class SimpleAMSCommentSerializer
277
+ include SimpleAMS::DSL
278
+
279
+ attributes :id, :body
280
+ end
281
+
282
+ class SimpleAMSPostSerializer
283
+ include SimpleAMS::DSL
284
+
285
+ attributes :id, :body
286
+ attribute :commenter_names
287
+ has_many :comments, serializer: SimpleAMSCommentSerializer
288
+
289
+ def commenter_names
290
+ object.commenters.pluck(:name)
291
+ end
292
+ end
293
+
294
+ # --- Test data creation ---
295
+
296
+ 100.times do |i|
297
+ post = Post.create!(body: "post#{i}")
298
+ user1 = User.create!(name: "John#{i}")
299
+ user2 = User.create!(name: "Jane#{i}")
300
+ 10.times do |n|
301
+ post.comments.create!(commenter: user1, body: "Comment1_#{i}_#{n}")
302
+ post.comments.create!(commenter: user2, body: "Comment2_#{i}_#{n}")
303
+ end
304
+ end
305
+
306
+ posts = Post.all.to_a
307
+
308
+ # --- Store the serializers in procs ---
309
+
310
+ alba = Proc.new { AlbaPostResource.new(posts).serialize }
311
+ alba_inline = Proc.new do
312
+ Alba.serialize(posts) do
313
+ attributes :id, :body
314
+ attribute :commenter_names do |post|
315
+ post.commenters.pluck(:name)
316
+ end
317
+ many :comments do
318
+ attributes :id, :body
319
+ end
320
+ end
321
+ end
322
+ ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {}).as_json }
323
+ blueprinter = Proc.new { PostBlueprint.render(posts) }
324
+ jbuilder = Proc.new do
325
+ Jbuilder.new do |json|
326
+ json.array!(posts) do |post|
327
+ json.post post.to_builder
328
+ end
329
+ end.target!
330
+ end
331
+ jsonapi = proc { JsonApiStandardPostSerializer.new(posts).to_json }
332
+ jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(posts).to_json }
333
+ primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
334
+ rails = Proc.new do
335
+ ActiveSupport::JSON.encode(posts.map{ |post| post.serializable_hash(include: :comments) })
336
+ end
337
+ representable = Proc.new { PostsRepresenter.new(posts).to_json }
338
+ simple_ams = Proc.new { SimpleAMS::Renderer::Collection.new(posts, serializer: SimpleAMSPostSerializer).to_json }
339
+
340
+ # --- Execute the serializers to check their output ---
341
+
342
+ puts "Serializer outputs ----------------------------------"
343
+ {
344
+ alba: alba,
345
+ alba_inline: alba_inline,
346
+ ams: ams,
347
+ blueprinter: blueprinter,
348
+ jbuilder: jbuilder, # different order
349
+ jsonapi: jsonapi, # nested JSON:API format
350
+ jsonapi_same_format: jsonapi_same_format,
351
+ primalize: primalize,
352
+ rails: rails,
353
+ representable: representable,
354
+ simple_ams: simple_ams,
355
+ }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
356
+
357
+ # --- Run the benchmarks ---
358
+
359
+ require 'benchmark/ips'
360
+ Benchmark.ips do |x|
361
+ x.report(:alba, &alba)
362
+ x.report(:alba_inline, &alba_inline)
363
+ x.report(:ams, &ams)
364
+ x.report(:blueprinter, &blueprinter)
365
+ x.report(:jbuilder, &jbuilder)
366
+ x.report(:jsonapi, &jsonapi)
367
+ x.report(:jsonapi_same_format, &jsonapi_same_format)
368
+ x.report(:primalize, &primalize)
369
+ x.report(:rails, &rails)
370
+ x.report(:representable, &representable)
371
+ x.report(:simple_ams, &simple_ams)
372
+
373
+ x.compare!
374
+ end
375
+
376
+
377
+ require 'benchmark/memory'
378
+ Benchmark.memory do |x|
379
+ x.report(:alba, &alba)
380
+ x.report(:alba_inline, &alba_inline)
381
+ x.report(:ams, &ams)
382
+ x.report(:blueprinter, &blueprinter)
383
+ x.report(:jbuilder, &jbuilder)
384
+ x.report(:jsonapi, &jsonapi)
385
+ x.report(:jsonapi_same_format, &jsonapi_same_format)
386
+ x.report(:primalize, &primalize)
387
+ x.report(:rails, &rails)
388
+ x.report(:representable, &representable)
389
+ x.report(:simple_ams, &simple_ams)
390
+
391
+ x.compare!
392
+ end
@@ -20,6 +20,7 @@ gemfile(true) do
20
20
  gem "primalize"
21
21
  gem "oj"
22
22
  gem "representable"
23
+ gem "simple_ams"
23
24
  gem "sqlite3"
24
25
  end
25
26
 
@@ -259,6 +260,28 @@ class PostRepresenter < Representable::Decorator
259
260
  end
260
261
  end
261
262
 
263
+ # --- SimpleAMS serializers ---
264
+
265
+ require "simple_ams"
266
+
267
+ class SimpleAMSCommentSerializer
268
+ include SimpleAMS::DSL
269
+
270
+ attributes :id, :body
271
+ end
272
+
273
+ class SimpleAMSPostSerializer
274
+ include SimpleAMS::DSL
275
+
276
+ attributes :id, :body
277
+ attribute :commenter_names
278
+ has_many :comments, serializer: SimpleAMSCommentSerializer
279
+
280
+ def commenter_names
281
+ object.commenters.pluck(:name)
282
+ end
283
+ end
284
+
262
285
  # --- Test data creation ---
263
286
 
264
287
  post = Post.create!(body: 'post')
@@ -290,6 +313,7 @@ jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(post).to_json }
290
313
  primalize = proc { PrimalizePostResource.new(post).to_json }
291
314
  rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
292
315
  representable = Proc.new { PostRepresenter.new(post).to_json }
316
+ simple_ams = Proc.new { SimpleAMS::Renderer.new(post, serializer: SimpleAMSPostSerializer).to_json }
293
317
 
294
318
  # --- Execute the serializers to check their output ---
295
319
 
@@ -304,26 +328,12 @@ puts "Serializer outputs ----------------------------------"
304
328
  jsonapi_same_format: jsonapi_same_format,
305
329
  primalize: primalize,
306
330
  rails: rails,
307
- representable: representable
331
+ representable: representable,
332
+ simple_ams: simple_ams,
308
333
  }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
309
334
 
310
335
  # --- Run the benchmarks ---
311
336
 
312
- require 'benchmark'
313
- time = 1000
314
- Benchmark.bmbm do |x|
315
- x.report(:alba) { time.times(&alba) }
316
- x.report(:alba_inline) { time.times(&alba_inline) }
317
- x.report(:ams) { time.times(&ams) }
318
- x.report(:blueprinter) { time.times(&blueprinter) }
319
- x.report(:jbuilder) { time.times(&jbuilder) }
320
- x.report(:jsonapi) { time.times(&jsonapi) }
321
- x.report(:jsonapi_same_format) { time.times(&jsonapi_same_format) }
322
- x.report(:primalize) { time.times(&primalize) }
323
- x.report(:rails) { time.times(&rails) }
324
- x.report(:representable) { time.times(&representable) }
325
- end
326
-
327
337
  require 'benchmark/ips'
328
338
  Benchmark.ips do |x|
329
339
  x.report(:alba, &alba)
@@ -336,6 +346,25 @@ Benchmark.ips do |x|
336
346
  x.report(:primalize, &primalize)
337
347
  x.report(:rails, &rails)
338
348
  x.report(:representable, &representable)
349
+ x.report(:simple_ams, &simple_ams)
350
+
351
+ x.compare!
352
+ end
353
+
354
+
355
+ require 'benchmark/memory'
356
+ Benchmark.memory do |x|
357
+ x.report(:alba, &alba)
358
+ x.report(:alba_inline, &alba_inline)
359
+ x.report(:ams, &ams)
360
+ x.report(:blueprinter, &blueprinter)
361
+ x.report(:jbuilder, &jbuilder)
362
+ x.report(:jsonapi, &jsonapi)
363
+ x.report(:jsonapi_same_format, &jsonapi_same_format)
364
+ x.report(:primalize, &primalize)
365
+ x.report(:rails, &rails)
366
+ x.report(:representable, &representable)
367
+ x.report(:simple_ams, &simple_ams)
339
368
 
340
369
  x.compare!
341
370
  end
data/lib/alba.rb CHANGED
@@ -14,6 +14,7 @@ module Alba
14
14
 
15
15
  class << self
16
16
  attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
17
+ attr_accessor :inflector
17
18
 
18
19
  # Set the backend, which actually serializes object into JSON
19
20
  #
@@ -2,7 +2,7 @@ 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
5
+ attr_reader :object, :name
6
6
 
7
7
  # @param name [Symbol] name of the method to fetch association
8
8
  # @param condition [Proc] a proc filtering data
@@ -15,14 +15,7 @@ module Alba
15
15
  @resource = resource
16
16
  return if @resource
17
17
 
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
18
+ assign_resource(nesting)
26
19
  end
27
20
 
28
21
  private
@@ -36,11 +29,26 @@ module Alba
36
29
  end
37
30
  end
38
31
 
32
+ def assign_resource(nesting)
33
+ @resource = if @block
34
+ resource_class
35
+ elsif Alba.inferring
36
+ resource_class_with_nesting(nesting)
37
+ else
38
+ raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
39
+ end
40
+ end
41
+
39
42
  def resource_class
40
43
  klass = Class.new
41
44
  klass.include(Alba::Resource)
42
45
  klass.class_eval(&@block)
43
46
  klass
44
47
  end
48
+
49
+ def resource_class_with_nesting(nesting)
50
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
51
+ const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
52
+ end
45
53
  end
46
54
  end
@@ -0,0 +1,36 @@
1
+ module Alba
2
+ # This module represents the inflector, which is used by default
3
+ module DefaultInflector
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
+ # Camelizes a key
13
+ #
14
+ # @params key [String] key to be camelized
15
+ # @return [String] camelized key
16
+ def camelize(key)
17
+ ActiveSupport::Inflector.camelize(key)
18
+ end
19
+
20
+ # Camelizes a key, 1st letter lowercase
21
+ #
22
+ # @params key [String] key to be camelized
23
+ # @return [String] camelized key
24
+ def camelize_lower(key)
25
+ ActiveSupport::Inflector.camelize(key, false)
26
+ end
27
+
28
+ # Dasherizes a key
29
+ #
30
+ # @params key [String] key to be dasherized
31
+ # @return [String] dasherized key
32
+ def dasherize(key)
33
+ ActiveSupport::Inflector.dasherize(key)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
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 CHANGED
@@ -15,7 +15,7 @@ module Alba
15
15
  return if @object.nil?
16
16
 
17
17
  @resource = constantize(@resource)
18
- @object.map { |o| @resource.new(o, params: params, within: within).to_hash }
18
+ @resource.new(@object, params: params, within: within).to_hash
19
19
  end
20
20
  end
21
21
  end
data/lib/alba/resource.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  require_relative 'one'
2
2
  require_relative 'many'
3
+ require_relative 'key_transform_factory'
4
+ require_relative 'typed_attribute'
3
5
 
4
6
  module Alba
5
7
  # This module represents what should be serialized
6
8
  module Resource
7
9
  # @!parse include InstanceMethods
8
10
  # @!parse extend ClassMethods
9
- DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _transforming_root_key: false, _on_error: nil}.freeze
11
+ DSLS = {_attributes: {}, _key: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze
10
12
  private_constant :DSLS
11
13
 
14
+ WITHIN_DEFAULT = Object.new.freeze
15
+ private_constant :WITHIN_DEFAULT
16
+
12
17
  # @private
13
18
  def self.included(base)
14
19
  super
@@ -29,7 +34,7 @@ module Alba
29
34
  # @param object [Object] the object to be serialized
30
35
  # @param params [Hash] user-given Hash for arbitrary data
31
36
  # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
32
- def initialize(object, params: {}, within: true)
37
+ def initialize(object, params: {}, within: WITHIN_DEFAULT)
33
38
  @object = object
34
39
  @params = params.freeze
35
40
  @within = within
@@ -60,21 +65,25 @@ module Alba
60
65
  def _key
61
66
  return @_key.to_s unless @_key == true && Alba.inferring
62
67
 
63
- resource_name = self.class.name.demodulize.delete_suffix('Resource').underscore
64
- key = collection? ? resource_name.pluralize : resource_name
65
- transforming_root_key = @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
66
- transforming_root_key ? transform_key(key) : key
68
+ transforming_root_key? ? transform_key(key_from_resource_name) : key_from_resource_name
69
+ end
70
+
71
+ def key_from_resource_name
72
+ collection? ? resource_name.pluralize : resource_name
73
+ end
74
+
75
+ def resource_name
76
+ self.class.name.demodulize.delete_suffix('Resource').underscore
77
+ end
78
+
79
+ def transforming_root_key?
80
+ @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
67
81
  end
68
82
 
69
83
  def converter
70
84
  lambda do |object|
71
85
  arrays = @_attributes.map do |key, attribute|
72
- key = transform_key(key)
73
- if attribute.is_a?(Array) # Conditional
74
- conditional_attribute(object, key, attribute)
75
- else
76
- [key, fetch_attribute(object, attribute)]
77
- end
86
+ key_and_attribute_body_from(object, key, attribute)
78
87
  rescue ::Alba::Error, FrozenError, TypeError
79
88
  raise
80
89
  rescue StandardError => e
@@ -84,10 +93,19 @@ module Alba
84
93
  end
85
94
  end
86
95
 
96
+ def key_and_attribute_body_from(object, key, attribute)
97
+ key = transform_key(key)
98
+ if attribute.is_a?(Array) # Conditional
99
+ conditional_attribute(object, key, attribute)
100
+ else
101
+ [key, fetch_attribute(object, attribute)]
102
+ end
103
+ end
104
+
87
105
  def conditional_attribute(object, key, attribute)
88
106
  condition = attribute.last
89
107
  arity = condition.arity
90
- return [] if arity <= 1 && !condition.call(object)
108
+ return [] if arity <= 1 && !instance_exec(object, &condition)
91
109
 
92
110
  fetched_attribute = fetch_attribute(object, attribute.first)
93
111
  attr = if attribute.first.is_a?(Alba::Association)
@@ -95,7 +113,7 @@ module Alba
95
113
  else
96
114
  fetched_attribute
97
115
  end
98
- return [] if arity >= 2 && !condition.call(object, attr)
116
+ return [] if arity >= 2 && !instance_exec(object, attr, &condition)
99
117
 
100
118
  [key, fetched_attribute]
101
119
  end
@@ -118,10 +136,9 @@ module Alba
118
136
 
119
137
  # Override this method to supply custom key transform method
120
138
  def transform_key(key)
121
- return key unless @_transform_keys
139
+ return key if @_transform_key_function.nil?
122
140
 
123
- require_relative 'key_transformer'
124
- KeyTransformer.transform(key, @_transform_keys)
141
+ @_transform_key_function.call(key.to_s)
125
142
  end
126
143
 
127
144
  def fetch_attribute(object, attribute)
@@ -131,70 +148,28 @@ module Alba
131
148
  when Proc
132
149
  instance_exec(object, &attribute)
133
150
  when Alba::One, Alba::Many
134
- within = check_within
151
+ within = check_within(attribute.name.to_sym)
135
152
  return unless within
136
153
 
137
154
  attribute.to_hash(object, params: params, within: within)
138
- when Hash # Typed Attribute
139
- typed_attribute(object, attribute)
155
+ when TypedAttribute
156
+ attribute.value(object)
140
157
  else
141
158
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
142
159
  end
143
160
  end
144
161
 
145
- def typed_attribute(object, hash)
146
- attr_name = hash[:attr_name]
147
- type = hash[:type]
148
- type_converter = hash[:type_converter]
149
- value, result = type_check(object, attr_name, type)
150
- return value if result
151
- raise TypeError if !result && !type_converter
152
-
153
- type_converter = type_converter_for(type) if type_converter == true
154
- type_converter.call(value)
155
- rescue TypeError
156
- raise TypeError, "Attribute #{attr_name} is expected to be #{type} but actually #{value.nil? ? 'nil' : value.class.name}."
157
- end
158
-
159
- def type_check(object, attr_name, type)
160
- value = object.public_send(attr_name)
161
- type_correct = case type
162
- when :String, ->(klass) { klass == String }
163
- value.is_a?(String)
164
- when :Integer, ->(klass) { klass == Integer }
165
- value.is_a?(Integer)
166
- when :Boolean
167
- [true, false].include?(attr_name)
168
- else
169
- raise Alba::UnsupportedType, "Unknown type: #{type}"
170
- end
171
- [value, type_correct]
172
- end
173
-
174
- def type_converter_for(type)
175
- case type
176
- when :String, ->(klass) { klass == String }
177
- ->(object) { object.to_s }
178
- when :Integer, ->(klass) { klass == Integer }
179
- ->(object) { Integer(object) }
180
- when :Boolean
181
- ->(object) { !!object }
182
- else
183
- raise Alba::UnsupportedType, "Unknown type: #{type}"
184
- end
185
- end
186
-
187
- def check_within
162
+ def check_within(association_name)
188
163
  case @within
164
+ when WITHIN_DEFAULT # Default value, doesn't check within tree
165
+ WITHIN_DEFAULT
189
166
  when Hash # Traverse within tree
190
- @within.fetch(_key.to_sym, nil)
167
+ @within.fetch(association_name, nil)
191
168
  when Array # within tree ends with Array
192
- @within.find { |item| item.to_sym == _key.to_sym } # Check if at least one item in the array matches current resource
169
+ @within.find { |item| item.to_sym == association_name }
193
170
  when Symbol # within tree could end with Symbol
194
- @within == _key.to_sym # Check if the symbol matches current resource
195
- when true # In this case, Alba serializes all associations.
196
- true
197
- when nil, false # In these cases, Alba stops serialization here.
171
+ @within == association_name
172
+ when nil, true, false # In these cases, Alba stops serialization here.
198
173
  false
199
174
  else
200
175
  raise Alba::Error, "Unknown type for within option: #{@within.class}"
@@ -219,21 +194,32 @@ module Alba
219
194
  # Set multiple attributes at once
220
195
  #
221
196
  # @param attrs [Array<String, Symbol>]
222
- # @param options [Hash] option hash including `if` that is a condition to render these attributes
197
+ # @param if [Boolean] condition to decide if it should render these attributes
198
+ # @param attrs_with_types [Hash] attributes with name in its key and type and optional type converter in its value
223
199
  def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
224
200
  if_value = binding.local_variable_get(:if)
201
+ assign_attributes(attrs, if_value)
202
+ assign_attributes_with_types(attrs_with_types, if_value)
203
+ end
204
+
205
+ def assign_attributes(attrs, if_value)
225
206
  attrs.each do |attr_name|
226
207
  attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
227
208
  @_attributes[attr_name.to_sym] = attr
228
209
  end
210
+ end
211
+ private :assign_attributes
212
+
213
+ def assign_attributes_with_types(attrs_with_types, if_value)
229
214
  attrs_with_types.each do |attr_name, type_and_converter|
230
215
  attr_name = attr_name.to_sym
231
216
  type, type_converter = type_and_converter
232
- typed_attr = {attr_name: attr_name, type: type, type_converter: type_converter}
217
+ typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
233
218
  attr = if_value ? [typed_attr, if_value] : typed_attr
234
219
  @_attributes[attr_name] = attr
235
220
  end
236
221
  end
222
+ private :assign_attributes_with_types
237
223
 
238
224
  # Set an attribute with the given block
239
225
  #
@@ -307,7 +293,7 @@ module Alba
307
293
  # @param type [String, Symbol]
308
294
  # @param root [Boolean] decides if root key also should be transformed
309
295
  def transform_keys(type, root: nil)
310
- @_transform_keys = type.to_sym
296
+ @_transform_key_function = KeyTransformFactory.create(type.to_sym)
311
297
  @_transforming_root_key = root
312
298
  end
313
299
 
@@ -0,0 +1,64 @@
1
+ module Alba
2
+ # Representing typed attributes to encapsulate logic about types
3
+ class TypedAttribute
4
+ # @param name [Symbol, String]
5
+ # @param type [Symbol, Class]
6
+ # @param converter [Proc]
7
+ def initialize(name:, type:, converter:)
8
+ @name = name
9
+ @type = type
10
+ @converter = case converter
11
+ when true then default_converter
12
+ when false, nil then null_converter
13
+ else converter
14
+ end
15
+ end
16
+
17
+ # @param object [Object] target to check and convert type with
18
+ # @return [String, Integer, Boolean] type-checked or type-converted object
19
+ def value(object)
20
+ value, result = check(object)
21
+ result ? value : @converter.call(value)
22
+ rescue TypeError
23
+ raise TypeError, "Attribute #{@name} is expected to be #{@type} but actually #{display_value_for(value)}."
24
+ end
25
+
26
+ private
27
+
28
+ def check(object)
29
+ value = object.public_send(@name)
30
+ type_correct = case @type
31
+ when :String, ->(klass) { klass == String }
32
+ value.is_a?(String)
33
+ when :Integer, ->(klass) { klass == Integer }
34
+ value.is_a?(Integer)
35
+ when :Boolean
36
+ [true, false].include?(value)
37
+ else
38
+ raise Alba::UnsupportedType, "Unknown type: #{@type}"
39
+ end
40
+ [value, type_correct]
41
+ end
42
+
43
+ def default_converter
44
+ case @type
45
+ when :String, ->(klass) { klass == String }
46
+ ->(object) { object.to_s }
47
+ when :Integer, ->(klass) { klass == Integer }
48
+ ->(object) { Integer(object) }
49
+ when :Boolean
50
+ ->(object) { !!object }
51
+ else
52
+ raise Alba::UnsupportedType, "Unknown type: #{@type}"
53
+ end
54
+ end
55
+
56
+ def null_converter
57
+ ->(_) { raise TypeError }
58
+ end
59
+
60
+ def display_value_for(value)
61
+ value.nil? ? 'nil' : value.class.name
62
+ end
63
+ end
64
+ end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '1.2.0'.freeze
2
+ VERSION = '1.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,18 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.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-09 00:00:00.000000000 Z
11
+ date: 2021-05-31 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: []
@@ -34,7 +33,8 @@ files:
34
33
  - Rakefile
35
34
  - SECURITY.md
36
35
  - alba.gemspec
37
- - benchmark/local.rb
36
+ - benchmark/collection.rb
37
+ - benchmark/single_resource.rb
38
38
  - bin/console
39
39
  - bin/setup
40
40
  - codecov.yml
@@ -43,10 +43,12 @@ files:
43
43
  - gemfiles/without_oj.gemfile
44
44
  - lib/alba.rb
45
45
  - lib/alba/association.rb
46
- - lib/alba/key_transformer.rb
46
+ - lib/alba/default_inflector.rb
47
+ - lib/alba/key_transform_factory.rb
47
48
  - lib/alba/many.rb
48
49
  - lib/alba/one.rb
49
50
  - lib/alba/resource.rb
51
+ - lib/alba/typed_attribute.rb
50
52
  - lib/alba/version.rb
51
53
  - sider.yml
52
54
  homepage: https://github.com/okuramasafumi/alba
@@ -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