alba 2.2.0 → 2.4.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: c68ec07a949642b11dfd36b3bf748249d0d725178ece9dd331e373fc2339cb8a
4
- data.tar.gz: 7704431d69f96b88d9632c114dae90cfceae018c8372c8736c091efd2050b65a
3
+ metadata.gz: 89ce66083a51c7811468f9435288afbc53f81418f21635af4393eaf1ac635d14
4
+ data.tar.gz: 7344d77ba35c156a6a18abe6f5ff498b7a8cd24b460c5b3f6bf1f0739bd0c8aa
5
5
  SHA512:
6
- metadata.gz: 98ca1b6fa7c6851f3a69d6d1cb54cd782a3142fccfb33391eb08cbc093ad691496a8fdb9e93e5ce89543032a0003ab8b60022ca8bbbba81c73de249b571a3fa6
7
- data.tar.gz: 3756fe21da92358031a722d83b7169ef81e8749ccf6430bef577dfaac7ab4a80e68c07c6e0205497e54692f74b16e66aabad390db6eda09e486a732387541630
6
+ metadata.gz: 33e534a676d589ba88db064b8ba57bbd10a20d268c417d8ed89fbc9b830f2861e4d3c1b46772d9151fce5dcff909e591f26f4fe1be77c247da4e1ceb4998d1cc
7
+ data.tar.gz: 401030ee3033228b11d2b75fa411809e7707b0c8b6791c2fe9da8bf9ce290c290225cb4e23895bc31019cc8cead19ac20335fffb24682c58ebc7f09a8250999a
data/.codeclimate.yml CHANGED
@@ -9,3 +9,4 @@ checks:
9
9
  exclude_patterns:
10
10
  - "benchmark/**/*"
11
11
  - "script/**/*"
12
+ - "test/**/*"
data/.editorconfig ADDED
@@ -0,0 +1,10 @@
1
+ [*]
2
+ end_of_line = lf
3
+ insert_final_newline = true
4
+ charset = utf-8
5
+ trim_trailing_whitespace = true
6
+ indent_style = space
7
+ indent_size = 2
8
+
9
+ [*.{md,markdown}]
10
+ trim_trailing_whitespace = false
data/.rubocop.yml CHANGED
@@ -1,9 +1,14 @@
1
1
  ---
2
2
 
3
3
  inherit_gem:
4
- rubocop-sensible: 'config/rubocop.yml'
4
+ rubocop-gem_dev: 'config/rubocop.yml'
5
+
6
+ inherit_mode:
7
+ merge:
8
+ - Exclude
5
9
 
6
10
  require:
11
+ - rubocop-md
7
12
  - rubocop-minitest
8
13
  - rubocop-performance
9
14
  - rubocop-rake
@@ -13,6 +18,7 @@ AllCops:
13
18
  - 'Rakefile'
14
19
  - 'alba.gemspec'
15
20
  - 'benchmark/**/*.rb'
21
+ - 'docs/**/*'
16
22
  - 'script/**/*.rb'
17
23
  NewCops: enable
18
24
  EnabledByDefault: true
@@ -22,6 +28,15 @@ AllCops:
22
28
  Bundler/GemVersion:
23
29
  Enabled: false
24
30
 
31
+ # Test class is a class, but not really
32
+ Layout/ClassStructure:
33
+ Exclude:
34
+ - 'test/**/*'
35
+
36
+ # LineLength 80 comes from restrictions in good old days.
37
+ Layout/LineLength:
38
+ Max: 160
39
+
25
40
  # We'd like to write something like:
26
41
  # assert_equal(
27
42
  # expected,
@@ -63,6 +78,11 @@ Metrics/ParameterLists:
63
78
  Minitest/EmptyLineBeforeAssertionMethods:
64
79
  Enabled: false
65
80
 
81
+ # By nature of that test
82
+ Minitest/NoTestCases:
83
+ Exclude:
84
+ - 'test/dependencies/test_dependencies.rb'
85
+
66
86
  # We need to eval resource code to test errors on resource classes
67
87
  Security/Eval:
68
88
  Exclude:
@@ -80,14 +100,26 @@ Style/Copyright:
80
100
  Style/DisableCopsWithinSourceCodeDirective:
81
101
  Enabled: false
82
102
 
103
+ Style/Documentation:
104
+ Exclude:
105
+ - 'test/**/*'
106
+
107
+ Style/DocumentationMethod:
108
+ Exclude:
109
+ - 'README.md'
110
+
83
111
  Style/FrozenStringLiteralComment:
84
112
  Enabled: false
85
113
 
114
+ Style/ImplicitRuntimeError:
115
+ Exclude:
116
+ - 'README.md'
117
+
86
118
  Style/InlineComment:
87
119
  Enabled: false
88
120
 
89
121
  Style/MethodCallWithArgsParentheses:
90
- IgnoredMethods: ['require', 'require_relative', 'include', 'extend', 'puts', 'p', 'warn', 'raise', 'send', 'public_send']
122
+ AllowedMethods: ['require', 'require_relative', 'include', 'extend', 'puts', 'p', 'warn', 'raise', 'send', 'public_send', 'alias_method']
91
123
  Exclude:
92
124
  # There are so many `attributes` call without parenthese and that's absolutely fine
93
125
  - 'test/**/*.rb'
@@ -95,3 +127,7 @@ Style/MethodCallWithArgsParentheses:
95
127
  # There are so many cases we just want `if` expression!
96
128
  Style/MissingElse:
97
129
  EnforcedStyle: case
130
+
131
+ Style/OptionalBooleanParameter:
132
+ Exclude:
133
+ - 'README.md'
data/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.4.0] 2023-08-02
10
+
11
+ ### Added
12
+
13
+ - Implement helper [#322](https://github.com/okuramasafumi/alba/pull/322)
14
+ - Add `prefer_resource_method!` [#323](https://github.com/okuramasafumi/alba/pull/323)
15
+
16
+ ### Fixed
17
+
18
+ - Fix the bug of resource name inference [No PR](https://github.com/okuramasafumi/alba/commit/dab7091efa4a76ce9e538e08efa7349c296a705c)
19
+
20
+ ## [2.3.0] 2023-04-24
21
+
22
+ ### Added
23
+
24
+ - Add compatibility option for key [#304](https://github.com/okuramasafumi/alba/pull/304)
25
+ - It now infers resource name from Serializer [#309](https://github.com/okuramasafumi/alba/pull/309)
26
+ - `Alba.serialize` is easier to use for multiple root keys [#311](https://github.com/okuramasafumi/alba/pull/311)
27
+ - Gives access to params in nested_attribute [#312](https://github.com/okuramasafumi/alba/pull/312)
28
+ - Thank you, @GabrielErbetta
29
+
9
30
  ## [2.2.0] 2023-02-17
10
31
 
11
32
  ### Added
data/Gemfile CHANGED
@@ -10,12 +10,13 @@ gem 'minitest', '~> 5.14' # For test
10
10
  gem 'railties', require: false # For Rails integration testing
11
11
  gem 'rake', '~> 13.0' # For test and automation
12
12
  gem 'rubocop', '>= 0.79.0', require: false # For lint
13
+ gem 'rubocop-gem_dev', '>= 0.3.0', require: false # For lint
14
+ gem 'rubocop-md', '~> 1.0', require: false # For lint
13
15
  gem 'rubocop-minitest', '>= 0.25.0', require: false # For lint
14
16
  gem 'rubocop-performance', '>= 1.15.0', require: false # For lint
15
17
  gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
16
- gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
17
18
  gem 'ruby-lsp', require: false # For language server
18
- gem 'simplecov', '~> 0.21.0', require: false # For test coverage
19
+ gem 'simplecov', '~> 0.22.0', require: false # For test coverage
19
20
  gem 'simplecov-cobertura', require: false # For test coverage
20
21
  # gem 'steep', require: false # For language server and typing
21
22
  # gem 'typeprof', require: false # For language server and typing
data/HACKING.md CHANGED
@@ -17,6 +17,7 @@ Class methods (DSL):
17
17
  * `attribute` for block style attribute
18
18
  * `attributes` for symbol style attribute
19
19
  * `association` and its aliases such as `one` for association
20
+ * `nested` for nested attribute
20
21
 
21
22
  Instance methods:
22
23
 
@@ -29,7 +30,7 @@ Other methods are rather trivial. They'll be added to this list when it turned o
29
30
 
30
31
  In `Alba::Resource` module there are some things to note.
31
32
 
32
- `@object` is an object for serialization. It's either singular object or collection.
33
+ `@object` is an object for serialization. It's either a singular object or a collection.
33
34
 
34
35
  Attribute object can be either `Symbol`, `Proc`, `Alba::Association` or `Alba::TypedAttribute`.
35
36
 
data/README.md CHANGED
@@ -11,6 +11,40 @@
11
11
 
12
12
  Alba is a JSON serializer for Ruby, JRuby, and TruffleRuby.
13
13
 
14
+ ## TL;DR
15
+
16
+ Alba allows you to do something like below.
17
+
18
+ ```ruby
19
+ class User
20
+ attr_accessor :id, :name, :email
21
+
22
+ def initialize(id, name, email)
23
+ @id = id
24
+ @name = name
25
+ @email = email
26
+ end
27
+ end
28
+
29
+ class UserResource
30
+ include Alba::Resource
31
+
32
+ root_key :user
33
+
34
+ attributes :id, :name
35
+
36
+ attribute :name_with_email do |resource|
37
+ "#{resource.name}: #{resource.email}"
38
+ end
39
+ end
40
+
41
+ user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
42
+ UserResource.new(user).serialize
43
+ # => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"}}'
44
+ ```
45
+
46
+ Seems useful? Continue reading!
47
+
14
48
  ## Discussions
15
49
 
16
50
  Alba uses [GitHub Discussions](https://github.com/okuramasafumi/alba/discussions) to openly discuss the project.
@@ -43,7 +77,7 @@ Alba is faster than most of the alternatives. We have a [benchmark](https://gith
43
77
 
44
78
  ### Easy
45
79
 
46
- Alba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and short codebase. Finally it's easy to extend since it provides some methods for override to change default behavior of Alba.
80
+ Alba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and small codebase. Finally it's easy to extend since it provides some methods for override to change default behavior of Alba.
47
81
 
48
82
  ### Feature rich
49
83
 
@@ -82,7 +116,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
82
116
  * Error handling
83
117
  * Nil handling
84
118
  * Circular associations control
85
- * [Experimental] Types for validation and conversion
119
+ * Types for validation and conversion
86
120
  * Layout
87
121
  * No runtime dependencies
88
122
 
@@ -143,13 +177,27 @@ Alba.inflector = nil
143
177
  To check if inference is enabled etc, inspect the return value of `inflector`:
144
178
 
145
179
  ```ruby
146
- if Alba.inflector == nil
147
- puts "inflector not set"
180
+ if Alba.inflector.nil?
181
+ puts 'inflector not set'
148
182
  else
149
183
  puts "inflector is set to #{Alba.inflector}"
150
184
  end
151
185
  ```
152
186
 
187
+ ### Naming
188
+
189
+ Alba tries to infer resource name from class name like the following.
190
+
191
+ |Class name|Resource name|
192
+ | --- | --- |
193
+ | FooResource | Foo |
194
+ | FooSerializer | Foo |
195
+ | FooElse | FooElse |
196
+
197
+ Resource name is used as the default name of the root key, so you might want to name it ending with "Resource" or "Serializer"
198
+
199
+ When you use Alba with Rails, it's recommended to put your resource/serializer classes in corresponding directory such as `app/resources` or `app/serializers`.
200
+
153
201
  ### Simple serialization with root key
154
202
 
155
203
  You can define attributes with (yes) `attributes` macro with attribute names. If your attribute need some calculations, you can use `attribute` with block.
@@ -157,6 +205,7 @@ You can define attributes with (yes) `attributes` macro with attribute names. If
157
205
  ```ruby
158
206
  class User
159
207
  attr_accessor :id, :name, :email, :created_at, :updated_at
208
+
160
209
  def initialize(id, name, email)
161
210
  @id = id
162
211
  @name = name
@@ -220,6 +269,72 @@ class UserResource
220
269
  end
221
270
  ```
222
271
 
272
+ #### Prefer methods on resource
273
+
274
+ By default, Alba prefers methods on the object to methods on the resource. This means if you have a following situation:
275
+
276
+ ```ruby
277
+ class User
278
+ attr_accessor :id, :name, :email
279
+
280
+ def initialize(id, name, email)
281
+ @id = id
282
+ @name = name
283
+ @email = email
284
+ end
285
+
286
+ def name_with_email
287
+ "dummy!"
288
+ end
289
+ end
290
+
291
+ class UserResource
292
+ include Alba::Resource
293
+
294
+ root_key :user, :users # Later is for plural
295
+
296
+ attributes :id, :name, :name_with_email
297
+
298
+ # Same method exists in `User` class!
299
+ # This is not called
300
+ def name_with_email(user)
301
+ "#{user.name}: #{user.email}"
302
+ end
303
+ end
304
+
305
+ user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
306
+ UserResource.new(user).serialize
307
+ # => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"dummy!"}}'
308
+ ```
309
+
310
+ You can see that `name_with_email` is now `dummy!` from `User#name_with_email`. You cna change this behavior by using `prefer_resource_method!` DSL in a resource class:
311
+
312
+ ```ruby
313
+ # With the same `User` class
314
+
315
+ class UserResource
316
+ include Alba::Resource
317
+
318
+ prefer_resource_method! # This line is important
319
+
320
+ root_key :user, :users # Later is for plural
321
+
322
+ attributes :id, :name, :name_with_email
323
+
324
+ # Same method exists in `User` class!
325
+ # But now this is called!
326
+ def name_with_email(user)
327
+ "#{user.name}: #{user.email}"
328
+ end
329
+ end
330
+
331
+ user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
332
+ UserResource.new(user).serialize
333
+ # => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"}}'
334
+ ```
335
+
336
+ The next major version of Alba will change this default behavior to prefer resource methods. In case you want to preserve current behavior, there's `prefer_object_method!` DSL, which does that.
337
+
223
338
  #### Params
224
339
 
225
340
  You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.
@@ -356,11 +471,11 @@ class UserResource
356
471
 
357
472
  # Second proc works as a filter
358
473
  many :articles,
359
- proc { |articles, params, user|
360
- filter = params[:filter] || :odd?
361
- articles.select {|a| a.id.send(filter) && !user.banned }
362
- },
363
- resource: ArticleResource
474
+ proc { |articles, params, user|
475
+ filter = params[:filter] || :odd?
476
+ articles.select { |a| a.id.__send__(filter) && !user.banned }
477
+ },
478
+ resource: ArticleResource
364
479
  end
365
480
 
366
481
  user = User.new(1)
@@ -384,8 +499,8 @@ class UserResource
384
499
  attributes :id
385
500
 
386
501
  many :articles,
387
- key: 'my_articles', # Set key here
388
- resource: ArticleResource
502
+ key: 'my_articles', # Set key here
503
+ resource: ArticleResource
389
504
  end
390
505
  UserResource.new(user).serialize
391
506
  # => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
@@ -452,7 +567,7 @@ class FooResourceWithParamsOverride
452
567
 
453
568
  root_key :foo
454
569
 
455
- one :bar, resource: BarResource, params: { expose_secret: false }
570
+ one :bar, resource: BarResource, params: {expose_secret: false}
456
571
  end
457
572
 
458
573
  Baz = Struct.new(:data, :secret)
@@ -544,6 +659,48 @@ Alba.serialize(something)
544
659
 
545
660
  Although this might be useful sometimes, it's generally recommended to define a class for Resource.
546
661
 
662
+ #### Inline definition for multiple root keys
663
+
664
+ While Alba doesn't directly support multiple root keys, you can simulate it with `Alba.serialize`.
665
+
666
+ ```ruby
667
+ # Define foo and bar local variables here
668
+
669
+ Alba.serialize do
670
+ attribute :key1 do
671
+ FooResource.new(foo).to_h
672
+ end
673
+
674
+ attribute :key2 do
675
+ BarResource.new(bar).to_h
676
+ end
677
+ end
678
+ # => JSON containing "key1" and "key2" as root keys
679
+ ```
680
+
681
+ Note that we must use `to_h`, not `serialize`, with resources.
682
+
683
+ We can also generate a JSON with multiple root keys without making any class by the combination of `Alba.serialize` and `Alba.hashify`.
684
+
685
+ ```ruby
686
+ # Define foo and bar local variables here
687
+
688
+ Alba.serialize do
689
+ attribute :foo do
690
+ Alba.hashify(foo) do
691
+ attributes :id, :name # For example
692
+ end
693
+ end
694
+
695
+ attribute :bar do
696
+ Alba.hashify(bar) do
697
+ attributes :id
698
+ end
699
+ end
700
+ end
701
+ # => JSON containing "foo" and "bar" as root keys
702
+ ```
703
+
547
704
  ### Serializable Hash
548
705
 
549
706
  Instead of serializing to JSON, you can also output a Hash by calling `serializable_hash` or the `to_h` alias. Note also that the `serialize` method is aliased as `to_json`.
@@ -796,20 +953,15 @@ A custom inflector can be plugged in as follows.
796
953
  module CustomInflector
797
954
  module_function
798
955
 
799
- def camelize(string)
800
- end
956
+ def camelize(string); end
801
957
 
802
- def camelize_lower(string)
803
- end
958
+ def camelize_lower(string); end
804
959
 
805
- def dasherize(string)
806
- end
960
+ def dasherize(string); end
807
961
 
808
- def underscore(string)
809
- end
962
+ def underscore(string); end
810
963
 
811
- def classify(string)
812
- end
964
+ def classify(string); end
813
965
  end
814
966
 
815
967
  Alba.inflector = CustomInflector
@@ -926,7 +1078,7 @@ class User
926
1078
  end
927
1079
 
928
1080
  def email
929
- raise RuntimeError, 'Error!'
1081
+ raise 'Error!'
930
1082
  end
931
1083
  end
932
1084
 
@@ -1018,6 +1170,11 @@ UserResource.new(User.new(1)).serialize
1018
1170
  # => '{"user":{"id":1,"name":"User1","age":20}}'
1019
1171
  ```
1020
1172
 
1173
+ Note that `on_nil` does NOT work when the given object itself is `nil`. There are a few possible ways to deal with `nil`.
1174
+
1175
+ - Use `if` statement and avoid using Alba when the object is `nil`
1176
+ - Use "Null Object" pattern
1177
+
1021
1178
  ### Metadata
1022
1179
 
1023
1180
  You can set a metadata with `meta` DSL or `meta` option.
@@ -1122,8 +1279,10 @@ Sometimes we want to serialize a collection into a Hash, not an Array. It's poss
1122
1279
  ```ruby
1123
1280
  class User
1124
1281
  attr_reader :id, :name
1282
+
1125
1283
  def initialize(id, name)
1126
- @id, @name = id, name
1284
+ @id = id
1285
+ @name = name
1127
1286
  end
1128
1287
  end
1129
1288
 
@@ -1179,12 +1338,12 @@ In case you don't want to have a file for layout, Alba lets you define and apply
1179
1338
  ```ruby
1180
1339
  class FooResource
1181
1340
  include Alba::Resource
1182
- layout inline: proc do
1341
+ layout inline: proc {
1183
1342
  {
1184
1343
  header: 'my header',
1185
1344
  body: serializable_hash
1186
1345
  }
1187
- end
1346
+ }
1188
1347
  end
1189
1348
  ```
1190
1349
 
@@ -1195,12 +1354,12 @@ You can also use a Proc which returns String, not a Hash, for an inline layout.
1195
1354
  ```ruby
1196
1355
  class FooResource
1197
1356
  include Alba::Resource
1198
- layout inline: proc do
1357
+ layout inline: proc {
1199
1358
  %({
1200
1359
  "header": "my header",
1201
1360
  "body": #{serialized_json}
1202
1361
  })
1203
- end
1362
+ }
1204
1363
  end
1205
1364
  ```
1206
1365
 
@@ -1208,6 +1367,49 @@ It looks similar to file layout but you must use string interpolation for method
1208
1367
 
1209
1368
  Also note that we use percentage notation here to use double quotes. Using single quotes in inline string layout causes the error which might be resolved in other ways.
1210
1369
 
1370
+ ### Helper
1371
+
1372
+ Inheritance works well in most of the cases to share behaviors. One of the exceptions is when you want to shared behaviors with inline association. For example:
1373
+
1374
+ ```ruby
1375
+ class ApplicationResource
1376
+ include Alba::Resource
1377
+
1378
+ def self.with_id
1379
+ attributes :id
1380
+ end
1381
+ end
1382
+
1383
+ class LibraryResource < ApplicationResource
1384
+ with_id
1385
+ attributes :created_at
1386
+
1387
+ with_many :library_books do
1388
+ with_id # This DOES NOT work!
1389
+ attributes :created_at
1390
+ end
1391
+ end
1392
+ ```
1393
+
1394
+ This doesn't work. Technically, inside of `has_many` is a separate class which doesn't inherit from the base class (`LibraryResource` in this example).
1395
+
1396
+ `helper` solves this problem. It's just a mark for methods that should be shared with inline associations.
1397
+
1398
+ ```ruby
1399
+ class ApplicationResource
1400
+ include Alba::Resource
1401
+
1402
+ helper do
1403
+ def with_id
1404
+ attributes :id
1405
+ end
1406
+ end
1407
+ end
1408
+ # Now `LibraryResource` works!
1409
+ ```
1410
+
1411
+ Within `helper` block, all methods should be defined without `self.`.
1412
+
1211
1413
  ### Caching
1212
1414
 
1213
1415
  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).
@@ -1289,8 +1491,8 @@ module AlbaExtension
1289
1491
  # Here attrs are an Array of Symbol
1290
1492
  def formatted_time_attributes(*attrs)
1291
1493
  attrs.each do |attr|
1292
- attribute attr do |object|
1293
- time = object.send(attr)
1494
+ attribute(attr) do |object|
1495
+ time = object.__send__(attr)
1294
1496
  time.strftime('%m/%d/%Y')
1295
1497
  end
1296
1498
  end
@@ -1347,13 +1549,14 @@ Alba currently doesn't support logging directly, but you can add your own loggin
1347
1549
 
1348
1550
  ```ruby
1349
1551
  module Logging
1350
- def serialize(...) # `...` was added in Ruby 2.7
1552
+ # `...` was added in Ruby 2.7
1553
+ def serialize(...)
1351
1554
  puts serializable_hash
1352
1555
  super(...)
1353
1556
  end
1354
1557
  end
1355
1558
 
1356
- FooResource.prepend Logging
1559
+ FooResource.prepend(Logging)
1357
1560
  FooResource.new(foo).serialize
1358
1561
  # => "{:id=>1}" is printed
1359
1562
  ```
data/alba.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
14
14
 
15
15
  spec.metadata = {
16
- 'bug_tracker_uri' => 'https://github.com/okuramasafumi/issues',
16
+ 'bug_tracker_uri' => 'https://github.com/okuramasafumi/alba/issues',
17
17
  'changelog_uri' => 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md',
18
18
  'documentation_uri' => 'https://rubydoc.info/github/okuramasafumi/alba',
19
19
  'source_code_uri' => 'https://github.com/okuramasafumi/alba',
data/benchmark/README.md CHANGED
@@ -10,72 +10,76 @@ Machine spec:
10
10
 
11
11
  |Key|Value|
12
12
  |---|---|
13
- |OS|macOS 12.2.1|
13
+ |OS|macOS 13.2.1|
14
14
  |CPU|Intel Corei7 Quad Core 2.3Ghz|
15
15
  |RAM|32GB|
16
- |Ruby|ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]|
16
+ |Ruby|ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-darwin21]|
17
+
18
+ Library versions:
19
+
20
+ |Library|Version|
21
+ |---|---|
22
+ |alba|2.2.0|
23
+ |blueprinter|0.25.3|
24
+ |fast_serializer_ruby|0.6.9|
25
+ |jserializer|0.2.1|
26
+ |oj|3.14.2|
27
+ |simple_ams|0.2.6|
28
+ |representable|3.2.0|
29
+ |turbostreamer|1.10.0|
30
+ |jbuilder|2.11.5|
31
+ |panko_serializer|0.7.9|
32
+ |active_model_serializers|0.10.13|
17
33
 
18
34
  `benchmark-ips` with `Oj.optimize_rails`:
19
35
 
20
36
  ```
21
37
  Comparison:
22
- panko: 267.6 i/s
23
- rails: 111.2 i/s - 2.41x (± 0.00) slower
24
- jserializer: 106.2 i/s - 2.52x (± 0.00) slower
25
- alba: 102.8 i/s - 2.60x (± 0.00) slower
26
- turbostreamer: 99.9 i/s - 2.68x (± 0.00) slower
27
- jbuilder: 90.7 i/s - 2.95x (± 0.00) slower
28
- alba_inline: 90.0 i/s - 2.97x (± 0.00) slower
29
- primalize: 82.1 i/s - 3.26x (± 0.00) slower
30
- fast_serializer: 62.7 i/s - 4.27x (± 0.00) slower
31
- jsonapi_same_format: 59.5 i/s - 4.50x (± 0.00) slower
32
- jsonapi: 55.3 i/s - 4.84x (± 0.00) slower
33
- blueprinter: 54.1 i/s - 4.95x (± 0.00) slower
34
- representable: 35.9 i/s - 7.46x (± 0.00) slower
35
- simple_ams: 25.4 i/s - 10.53x (± 0.00) slower
36
- ams: 9.1 i/s - 29.39x (± 0.00) slower
38
+ panko: 310.4 i/s
39
+ jserializer: 120.6 i/s - 2.57x slower
40
+ turbostreamer: 117.3 i/s - 2.65x slower
41
+ rails: 114.0 i/s - 2.72x slower
42
+ alba_inline: 99.3 i/s - 3.13x slower
43
+ alba: 94.1 i/s - 3.30x slower
44
+ fast_serializer: 67.8 i/s - 4.58x slower
45
+ blueprinter: 57.6 i/s - 5.39x slower
46
+ representable: 36.3 i/s - 8.56x slower
47
+ simple_ams: 23.3 i/s - 13.32x slower
48
+ ams: 10.9 i/s - 28.53x slower
37
49
  ```
38
50
 
39
51
  `benchmark-ips` without `Oj.optimize_rails`:
40
52
 
41
53
  ```
42
54
  Comparison:
43
- panko: 283.8 i/s
44
- turbostreamer: 102.9 i/s - 2.76x (± 0.00) slower
45
- alba: 102.4 i/s - 2.77x (± 0.00) slower
46
- alba_inline: 98.7 i/s - 2.87x (± 0.00) slower
47
- jserializer: 93.3 i/s - 3.04x (± 0.00) slower
48
- fast_serializer: 60.1 i/s - 4.73x (± 0.00) slower
49
- blueprinter: 53.8 i/s - 5.28x (± 0.00) slower
50
- rails: 37.1 i/s - 7.65x (± 0.00) slower
51
- jbuilder: 37.1 i/s - 7.66x (± 0.00) slower
52
- primalize: 31.2 i/s - 9.08x (± 0.00) slower
53
- jsonapi_same_format: 28.3 i/s - 10.03x (± 0.00) slower
54
- representable: 27.8 i/s - 10.23x (± 0.00) slower
55
- jsonapi: 27.5 i/s - 10.34x (± 0.00) slower
56
- simple_ams: 16.9 i/s - 16.75x (± 0.00) slower
57
- ams: 8.3 i/s - 34.36x (± 0.00) slower
55
+ panko: 326.1 i/s
56
+ turbostreamer: 120.6 i/s - 2.70x slower
57
+ jserializer: 119.2 i/s - 2.74x slower
58
+ alba_inline: 104.3 i/s - 3.13x slower
59
+ alba: 102.2 i/s - 3.19x slower
60
+ fast_serializer: 66.9 i/s - 4.88x slower
61
+ blueprinter: 56.7 i/s - 5.75x slower
62
+ rails: 33.9 i/s - 9.63x slower
63
+ representable: 30.3 i/s - 10.77x slower
64
+ simple_ams: 16.4 i/s - 19.84x slower
65
+ ams: 9.4 i/s - 34.56x slower
58
66
  ```
59
67
 
60
68
  `benchmark-memory`:
61
69
 
62
70
  ```
63
71
  Comparison:
64
- panko: 230418 allocated
65
- alba: 733217 allocated - 3.18x more
66
- alba_inline: 748297 allocated - 3.25x more
67
- turbostreamer: 781008 allocated - 3.39x more
68
- jserializer: 819705 allocated - 3.56x more
69
- primalize: 1195163 allocated - 5.19x more
70
- fast_serializer: 1232385 allocated - 5.35x more
71
- rails: 1236761 allocated - 5.37x more
72
- blueprinter: 1588937 allocated - 6.90x more
73
- jbuilder: 1774157 allocated - 7.70x more
74
- jsonapi_same_format: 2132489 allocated - 9.25x more
75
- jsonapi: 2279958 allocated - 9.89x more
76
- representable: 2869166 allocated - 12.45x more
77
- ams: 4473161 allocated - 19.41x more
78
- simple_ams: 7868345 allocated - 34.15x more
72
+ panko: 242426 allocated
73
+ turbostreamer: 817568 allocated - 3.37x more
74
+ jserializer: 831705 allocated - 3.43x more
75
+ alba: 1072217 allocated - 4.42x more
76
+ alba_inline: 1084889 allocated - 4.48x more
77
+ fast_serializer: 1244385 allocated - 5.13x more
78
+ rails: 1272761 allocated - 5.25x more
79
+ blueprinter: 1680137 allocated - 6.93x more
80
+ representable: 2892425 allocated - 11.93x more
81
+ ams: 4479569 allocated - 18.48x more
82
+ simple_ams: 6957913 allocated - 28.70x more
79
83
  ```
80
84
 
81
- Conclusion: panko is extremely fast but it's a C extension gem. As pure Ruby gems, Alba, turbostreamer and jserializer are notably faster than others. With `Oj.optimize_rails` jbuilder and Rails standard serialization are also fast.
85
+ Conclusion: panko is extremely fast but it's a C extension gem. As pure Ruby gems, Alba, `turbostreamer` and `jserializer` are notably faster than others, but Alba is slightly slower than other two. With `Oj.optimize_rails`, `jbuilder` and Rails standard serialization are also fast.
data/docs/rails.md CHANGED
@@ -14,11 +14,13 @@ You might want to add some configurations to initializer file such as `alba.rb`
14
14
  ```ruby
15
15
  # alba.rb
16
16
  Alba.backend = :active_support
17
- Alba.enable_inference!(with: :active_support)
17
+ Alba.inflector = :active_support
18
18
  ```
19
19
 
20
20
  You can also use `:oj_rails` for backend if you prefer using Oj.
21
21
 
22
+ Alba 2.2 introduced new Rails integration so that you don't have to add initializer file for setting inflector. You still need to add initializer file if you want to set backend or configure inflector with something different from `active_support`.
23
+
22
24
  ## Rendering JSON
23
25
 
24
26
  You can render JSON with Rails in two ways. One way is to pass JSON String.
@@ -1,8 +1,10 @@
1
1
  module Alba
2
2
  # Representing association
3
+ # @api private
3
4
  class Association
4
5
  @const_cache = {}
5
6
  class << self
7
+ # cache for `const_get`
6
8
  attr_reader :const_cache
7
9
  end
8
10
 
@@ -14,15 +16,16 @@ module Alba
14
16
  # @param params [Hash] params override for the association
15
17
  # @param nesting [String] a namespace where source class is inferred with
16
18
  # @param key_transformation [Symbol] key transformation type
19
+ # @param helper [Module] helper module to include
17
20
  # @param block [Block] used to define resource when resource arg is absent
18
- def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, key_transformation: :none, &block)
21
+ def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, key_transformation: :none, helper: nil, &block)
19
22
  @name = name
20
23
  @condition = condition
21
24
  @resource = resource
22
25
  @params = params
23
26
  return if @resource
24
27
 
25
- assign_resource(nesting, key_transformation, block)
28
+ assign_resource(nesting, key_transformation, block, helper)
26
29
  end
27
30
 
28
31
  # Recursively converts an object into a Hash
@@ -32,7 +35,7 @@ module Alba
32
35
  # @param params [Hash] user-given Hash for arbitrary data
33
36
  # @return [Hash]
34
37
  def to_h(target, within: nil, params: {})
35
- params = params.merge(@params) unless @params.empty?
38
+ params = params.merge(@params)
36
39
  @object = target.__send__(@name)
37
40
  @object = @condition.call(object, params, target) if @condition
38
41
  return if @object.nil?
@@ -59,9 +62,10 @@ module Alba
59
62
  end
60
63
  end
61
64
 
62
- def assign_resource(nesting, key_transformation, block)
65
+ def assign_resource(nesting, key_transformation, block, helper) # rubocop:disable Metrics/MethodLength
63
66
  @resource = if block
64
67
  klass = Alba.resource_class
68
+ klass.helper(helper) if helper
65
69
  klass.transform_keys(key_transformation)
66
70
  klass.class_eval(&block)
67
71
  klass
@@ -3,6 +3,7 @@ require_relative 'constants'
3
3
 
4
4
  module Alba
5
5
  # Represents attribute with `if` option
6
+ # @api private
6
7
  class ConditionalAttribute
7
8
  # @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
8
9
  # @param condition [Symbol, Proc] condition to check
@@ -20,7 +21,7 @@ module Alba
20
21
  return Alba::REMOVE_KEY unless condition_passes?(resource, object)
21
22
 
22
23
  fetched_attribute = yield(@body)
23
- return fetched_attribute if !with_two_arity_proc_condition
24
+ return fetched_attribute unless with_two_arity_proc_condition
24
25
 
25
26
  return Alba::REMOVE_KEY unless resource.instance_exec(object, attribute_from_association_body_or(fetched_attribute), &@condition)
26
27
 
@@ -2,7 +2,7 @@ begin
2
2
  require 'active_support/inflector'
3
3
  require 'active_support/core_ext/module/delegation'
4
4
  rescue LoadError
5
- raise ::Alba::Error, 'To use default inflector, please install `ActiveSupport` gem.'
5
+ raise Alba::Error, 'To use default inflector, please install `ActiveSupport` gem.'
6
6
  end
7
7
 
8
8
  module Alba
@@ -11,6 +11,18 @@ module Alba
11
11
  # Another is that `ActiveSupport::Inflector` doesn't have `camelize_lower` method that we want it to have, so this module works as an adapter.
12
12
  module DefaultInflector
13
13
  class << self
14
+ # @!method camelize(key)
15
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-camelize ActiveSupport::Inflector#camelize
16
+ # @!method dasherize(key)
17
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-dasherize ActiveSupport::Inflector#dasherize
18
+ # @!method underscore(key)
19
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-underscore ActiveSupport::Inflector#underscore
20
+ # @!method classify(key)
21
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-classify ActiveSupport::Inflector#classify
22
+ # @!method demodulize(key)
23
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-demodulize ActiveSupport::Inflector#demodulize
24
+ # @!method pluralize(key)
25
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-pluralize ActiveSupport::Inflector#pluralize
14
26
  delegate :camelize, :dasherize, :underscore, :classify, :demodulize, :pluralize, to: ActiveSupport::Inflector
15
27
  end
16
28
 
@@ -1,7 +1,8 @@
1
1
  module Alba
2
2
  # Module for printing deprecation warning
3
+ # @api private
3
4
  module Deprecation
4
- # Similar to {Kernel.warn} but prints caller as well
5
+ # Similar to {#warn} but prints caller as well
5
6
  #
6
7
  # @param message [String] main message to print
7
8
  # @return void
data/lib/alba/layout.rb CHANGED
@@ -3,6 +3,7 @@ require 'forwardable'
3
3
 
4
4
  module Alba
5
5
  # Layout serialization
6
+ # @api private
6
7
  class Layout
7
8
  extend Forwardable
8
9
 
@@ -1,5 +1,6 @@
1
1
  module Alba
2
2
  # Representing nested attribute
3
+ # @api private
3
4
  class NestedAttribute
4
5
  # @param key_transformation [Symbol] determines how to transform keys
5
6
  # @param block [Proc] class body
@@ -8,12 +9,14 @@ module Alba
8
9
  @block = block
9
10
  end
10
11
 
11
- # @return [Hash]
12
- def value(object)
12
+ # @param object [Object] the object being serialized
13
+ # @param params [Hash] params Hash inherited from Resource
14
+ # @return [Hash] hash serialized from running the class body in the object
15
+ def value(object:, params:)
13
16
  resource_class = Alba.resource_class
14
17
  resource_class.transform_keys(@key_transformation)
15
18
  resource_class.class_eval(&@block)
16
- resource_class.new(object).serializable_hash
19
+ resource_class.new(object, params: params).serializable_hash
17
20
  end
18
21
  end
19
22
  end
data/lib/alba/resource.rb CHANGED
@@ -11,21 +11,27 @@ module Alba
11
11
  module Resource
12
12
  # @!parse include InstanceMethods
13
13
  # @!parse extend ClassMethods
14
- DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _key_transformation_cascade: true, _on_error: nil, _on_nil: nil, _layout: nil, _collection_key: nil}.freeze # rubocop:disable Layout/LineLength
14
+ DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _key_transformation_cascade: true, _on_error: nil, _on_nil: nil, _layout: nil, _collection_key: nil, _helper: nil}.freeze # rubocop:disable Layout/LineLength
15
15
  private_constant :DSLS
16
16
 
17
17
  WITHIN_DEFAULT = Object.new.freeze
18
18
  private_constant :WITHIN_DEFAULT
19
19
 
20
+ # `setup` method is meta-programmatically defined here for performance.
20
21
  # @private
21
- def self.included(base)
22
+ def self.included(base) # rubocop:disable Metrics/MethodLength
22
23
  super
24
+ setup_method_body = 'private def _setup;'
23
25
  base.class_eval do
24
26
  # Initialize
25
27
  DSLS.each do |name, initial|
26
28
  instance_variable_set("@#{name}", initial.dup) unless instance_variable_defined?("@#{name}")
29
+ setup_method_body << "@#{name} = self.class.#{name};"
27
30
  end
31
+ base.define_method(:encode, Alba.encoder)
28
32
  end
33
+ setup_method_body << 'end'
34
+ base.class_eval(setup_method_body, __FILE__, __LINE__ + 1)
29
35
  base.include InstanceMethods
30
36
  base.extend ClassMethods
31
37
  end
@@ -41,7 +47,7 @@ module Alba
41
47
  @object = object
42
48
  @params = params
43
49
  @within = within
44
- DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.__send__(name)) }
50
+ _setup
45
51
  end
46
52
 
47
53
  # Serialize object into JSON string
@@ -74,13 +80,14 @@ module Alba
74
80
  end
75
81
  end
76
82
 
77
- # Returns a Hash correspondng {Resource#serialize}
83
+ # Returns a Hash correspondng {#serialize}
78
84
  #
79
85
  # @param root_key [Symbol, nil, true]
80
86
  # @param meta [Hash] metadata for this seialization
81
87
  # @return [Hash]
82
88
  def as_json(root_key: nil, meta: {})
83
- key = root_key.nil? ? fetch_key : root_key.to_s
89
+ key = root_key.nil? ? fetch_key : root_key
90
+ key = Alba.regularize_key(key)
84
91
  if key && !key.empty?
85
92
  h = {key => serializable_hash}
86
93
  hash_with_metadata(h, meta)
@@ -99,15 +106,9 @@ module Alba
99
106
 
100
107
  private
101
108
 
102
- def encode(hash)
103
- Alba.encoder.call(hash)
104
- end
105
-
106
109
  def _to_json(root_key, meta, options)
107
110
  options.reject! { |k, _| %i[layout prefixes template status].include?(k) } # Rails specific guard
108
- # TODO: use `filter_map` after dropping support of Ruby 2.6
109
- names = options.map { |k, v| k unless v.nil? }
110
- names.compact!
111
+ names = options.filter_map { |k, v| k unless v.nil? }
111
112
  unless names.empty?
112
113
  names.sort!
113
114
  names.map! { |s| "\"#{s}\"" }
@@ -134,7 +135,11 @@ module Alba
134
135
 
135
136
  def serializable_hash_for_collection
136
137
  if @_collection_key
137
- @object.to_h { |item| [item.public_send(@_collection_key).to_s, converter.call(item)] }
138
+ @object.to_h do |item|
139
+ k = item.public_send(@_collection_key)
140
+ key = Alba.regularize_key(k)
141
+ [key, converter.call(item)]
142
+ end
138
143
  else
139
144
  @object.each_with_object([], &collection_converter)
140
145
  end
@@ -147,34 +152,26 @@ module Alba
147
152
  end
148
153
 
149
154
  def _key_for_collection
150
- if Alba.inflector
151
- @_key_for_collection == true ? resource_name(pluralized: true) : @_key_for_collection.to_s
152
- else
153
- @_key_for_collection == true ? raise_root_key_inference_error : @_key_for_collection.to_s
154
- end
155
+ k = @_key_for_collection == true ? resource_name(pluralized: true) : @_key_for_collection
156
+ Alba.regularize_key(k)
155
157
  end
156
158
 
157
- # @return [String]
158
159
  def _key
159
- if Alba.inflector
160
- @_key == true ? resource_name(pluralized: false) : @_key.to_s
161
- else
162
- @_key == true ? raise_root_key_inference_error : @_key.to_s
163
- end
160
+ k = @_key == true ? resource_name(pluralized: false) : @_key
161
+ Alba.regularize_key(k)
164
162
  end
165
163
 
166
164
  def resource_name(pluralized: false)
167
- class_name = self.class.name
168
165
  inflector = Alba.inflector
169
- name = inflector.demodulize(class_name).delete_suffix('Resource')
166
+ raise Alba::Error, 'You must set inflector when setting root key as true.' unless inflector
167
+
168
+ class_name = self.class.name
169
+ suffix = class_name.end_with?('Resource') ? 'Resource' : 'Serializer'
170
+ name = inflector.demodulize(class_name).delete_suffix(suffix)
170
171
  underscore_name = inflector.underscore(name)
171
172
  pluralized ? inflector.pluralize(underscore_name) : underscore_name
172
173
  end
173
174
 
174
- def raise_root_key_inference_error
175
- raise Alba::Error, 'You must set inflector when setting root key as true.'
176
- end
177
-
178
175
  def transforming_root_key?
179
176
  @_transforming_root_key
180
177
  end
@@ -237,14 +234,16 @@ module Alba
237
234
  end
238
235
  end
239
236
 
240
- # @return [Symbol]
241
- def transform_key(key) # rubocop:disable Metrics/CyclomaticComplexity
242
- key = key.to_s
243
- return key if @_transform_type == :none || key.empty? # We can skip transformation
237
+ def transform_key(key)
238
+ return Alba.regularize_key(key) if @_transform_type == :none || key.nil? || key.empty? # We can skip transformation
244
239
 
245
240
  inflector = Alba.inflector
246
241
  raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector
247
242
 
243
+ Alba.regularize_key(_transform_key(inflector, key.to_s))
244
+ end
245
+
246
+ def _transform_key(inflector, key)
248
247
  case @_transform_type # rubocop:disable Style/MissingElse
249
248
  when :camel then inflector.camelize(key)
250
249
  when :lower_camel then inflector.camelize_lower(key)
@@ -258,20 +257,31 @@ module Alba
258
257
  when Symbol then fetch_attribute_from_object_and_resource(obj, attribute)
259
258
  when Proc then instance_exec(obj, &attribute)
260
259
  when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(obj, params: params, within: within) }
261
- when TypedAttribute, NestedAttribute then attribute.value(obj)
260
+ when TypedAttribute then attribute.value(obj)
261
+ when NestedAttribute then attribute.value(object: obj, params: params)
262
262
  when ConditionalAttribute then attribute.with_passing_condition(resource: self, object: obj) { |attr| fetch_attribute(obj, key, attr) }
263
- else
264
- raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
263
+ else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
265
264
  end
266
265
  value.nil? && nil_handler ? instance_exec(obj, key, attribute, &nil_handler) : value
267
266
  end
268
267
 
268
+ # TODO: from version 3, `_fetch_attribute_from_resource_first` is default
269
269
  def fetch_attribute_from_object_and_resource(obj, attribute)
270
+ _fetch_attribute_from_object_first(obj, attribute)
271
+ end
272
+
273
+ def _fetch_attribute_from_object_first(obj, attribute)
270
274
  obj.__send__(attribute)
271
275
  rescue NoMethodError
272
276
  __send__(attribute, obj)
273
277
  end
274
278
 
279
+ def _fetch_attribute_from_resource_first(obj, attribute)
280
+ __send__(attribute, obj)
281
+ rescue NoMethodError
282
+ obj.__send__(attribute)
283
+ end
284
+
275
285
  def nil_handler
276
286
  @_on_nil
277
287
  end
@@ -327,8 +337,9 @@ module Alba
327
337
 
328
338
  def assign_attributes(attrs, if_value)
329
339
  attrs.each do |attr_name|
330
- attr = if_value ? ConditionalAttribute.new(body: attr_name.to_sym, condition: if_value) : attr_name.to_sym
331
- @_attributes[attr_name.to_sym] = attr
340
+ attr_name = attr_name.to_sym
341
+ attr = if_value ? ConditionalAttribute.new(body: attr_name, condition: if_value) : attr_name
342
+ @_attributes[attr_name] = attr
332
343
  end
333
344
  end
334
345
  private :assign_attributes
@@ -373,8 +384,7 @@ module Alba
373
384
  def association(name, condition = nil, resource: nil, key: nil, params: {}, **options, &block)
374
385
  key_transformation = @_key_transformation_cascade ? @_transform_type : :none
375
386
  assoc = Association.new(
376
- name: name, condition: condition, resource: resource, params: params, nesting: nesting, key_transformation: key_transformation,
377
- &block
387
+ name: name, condition: condition, resource: resource, params: params, nesting: nesting, key_transformation: key_transformation, helper: @_helper, &block
378
388
  )
379
389
  @_attributes[key&.to_sym || name.to_sym] = options[:if] ? ConditionalAttribute.new(body: assoc, condition: options[:if]) : assoc
380
390
  end
@@ -387,7 +397,7 @@ module Alba
387
397
  if name.nil?
388
398
  nil
389
399
  else
390
- name.rpartition('::').first.tap { |n| n.empty? ? nil : n }
400
+ name.rpartition('::').first.then { |n| n.empty? ? nil : n }
391
401
  end
392
402
  end
393
403
  private :nesting
@@ -502,6 +512,25 @@ module Alba
502
512
  def on_nil(&block)
503
513
  @_on_nil = block
504
514
  end
515
+
516
+ # Define helper methods
517
+ #
518
+ # @param mod [Module] a module to extend
519
+ def helper(mod = @_helper || Module.new, &block)
520
+ mod.module_eval(&block) if block
521
+ extend mod
522
+ @_helper = mod
523
+ end
524
+
525
+ # DSL for alias, purely for readability
526
+ def prefer_resource_method!
527
+ alias_method :fetch_attribute_from_object_and_resource, :_fetch_attribute_from_resource_first
528
+ end
529
+
530
+ # DSL for alias, purely for readability
531
+ def prefer_object_method!
532
+ alias_method :fetch_attribute_from_object_and_resource, :_fetch_attribute_from_object_first
533
+ end
505
534
  end
506
535
  end
507
536
  end
@@ -1,5 +1,6 @@
1
1
  module Alba
2
2
  # Representing typed attributes to encapsulate logic about types
3
+ # @api private
3
4
  class TypedAttribute
4
5
  # @param name [Symbol, String]
5
6
  # @param type [Symbol, Class]
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '2.2.0'.freeze
2
+ VERSION = '2.4.0'.freeze
3
3
  end
data/lib/alba.rb CHANGED
@@ -44,19 +44,29 @@ module Alba
44
44
  # @param block [Block] resource block
45
45
  # @return [String] serialized JSON string
46
46
  # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
47
- def serialize(object, root_key: nil, &block)
48
- klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
49
-
50
- resource = klass.new(object)
47
+ def serialize(object = nil, root_key: nil, &block)
48
+ resource = resource_with(object, &block)
51
49
  resource.serialize(root_key: root_key)
52
50
  end
53
51
 
52
+ # Hashify the object with inline definitions
53
+ #
54
+ # @param object [Object] the object to be serialized
55
+ # @param root_key [Symbol, nil, true]
56
+ # @param block [Block] resource block
57
+ # @return [String] serialized JSON string
58
+ # @raise [ArgumentError] if block is absent or `with` argument's type is wrong
59
+ def hashify(object = nil, root_key: nil, &block)
60
+ resource = resource_with(object, &block)
61
+ resource.as_json(root_key: root_key)
62
+ end
63
+
54
64
  # Enable inference for key and resource name
55
65
  #
56
66
  # @param with [Symbol, Class, Module] inflector
57
67
  # When it's a Symbol, it sets inflector with given name
58
68
  # When it's a Class or a Module, it sets given object to inflector
59
- # @deprecated Use {#inflector=} instead
69
+ # @deprecated Use {.inflector=} instead
60
70
  def enable_inference!(with:)
61
71
  Alba::Deprecation.warn('Alba.enable_inference! is deprecated. Use `Alba.inflector=` instead.')
62
72
  @inflector = inflector_from(with)
@@ -65,14 +75,14 @@ module Alba
65
75
 
66
76
  # Disable inference for key and resource name
67
77
  #
68
- # @deprecated Use {#inflector=} instead
78
+ # @deprecated Use {.inflector=} instead
69
79
  def disable_inference!
70
80
  Alba::Deprecation.warn('Alba.disable_inference! is deprecated. Use `Alba.inflector = nil` instead.')
71
81
  @inferring = false
72
82
  @inflector = nil
73
83
  end
74
84
 
75
- # @deprecated Use {#inflector} instead
85
+ # @deprecated Use {.inflector} instead
76
86
  # @return [Boolean] whether inference is enabled or not
77
87
  def inferring
78
88
  Alba::Deprecation.warn('Alba.inferring is deprecated. Use `Alba.inflector` instead.')
@@ -107,16 +117,45 @@ module Alba
107
117
  const_parent.const_get("#{inflector.classify(name)}Resource")
108
118
  end
109
119
 
120
+ # Configure Alba to symbolize keys
121
+ def symbolize_keys!
122
+ @symbolize_keys = true
123
+ end
124
+
125
+ # Configure Alba to stringify (not symbolize) keys
126
+ def stringify_keys!
127
+ @symbolize_keys = false
128
+ end
129
+
130
+ # Regularize key to be either Symbol or String depending on @symbolize_keys
131
+ # Returns nil if key is nil
132
+ #
133
+ # @param key [String, Symbol, nil]
134
+ # @return [Symbol, String, nil]
135
+ def regularize_key(key)
136
+ return if key.nil?
137
+
138
+ @symbolize_keys ? key.to_sym : key.to_s
139
+ end
140
+
110
141
  # Reset config variables
111
142
  # Useful for test cleanup
112
143
  def reset!
113
144
  @encoder = default_encoder
145
+ @symbolize_keys = false
114
146
  @_on_error = :raise
115
147
  @_on_nil = nil
116
148
  end
117
149
 
118
150
  private
119
151
 
152
+ # This method could be part of public API, but for now it's private
153
+ def resource_with(object, &block)
154
+ klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
155
+
156
+ klass.new(object)
157
+ end
158
+
120
159
  def inflector_from(name_or_module)
121
160
  case name_or_module
122
161
  when nil then nil
@@ -126,8 +165,7 @@ module Alba
126
165
  when :dry
127
166
  require 'dry/inflector'
128
167
  Dry::Inflector.new
129
- else
130
- validate_inflector(name_or_module)
168
+ else validate_inflector(name_or_module)
131
169
  end
132
170
  end
133
171
 
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: 2.2.0
4
+ version: 2.4.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: 2023-02-17 00:00:00.000000000 Z
11
+ date: 2023-08-01 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.
@@ -19,6 +19,7 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - ".codeclimate.yml"
22
+ - ".editorconfig"
22
23
  - ".github/ISSUE_TEMPLATE/bug_report.md"
23
24
  - ".github/ISSUE_TEMPLATE/feature_request.md"
24
25
  - ".github/dependabot.yml"
@@ -71,7 +72,7 @@ homepage: https://github.com/okuramasafumi/alba
71
72
  licenses:
72
73
  - MIT
73
74
  metadata:
74
- bug_tracker_uri: https://github.com/okuramasafumi/issues
75
+ bug_tracker_uri: https://github.com/okuramasafumi/alba/issues
75
76
  changelog_uri: https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md
76
77
  documentation_uri: https://rubydoc.info/github/okuramasafumi/alba
77
78
  source_code_uri: https://github.com/okuramasafumi/alba
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.4.6
95
+ rubygems_version: 3.4.14
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Alba is the fastest JSON serializer for Ruby.