alba 0.13.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -1
- data/README.md +164 -28
- data/lib/alba.rb +32 -7
- data/lib/alba/association.rb +23 -3
- data/lib/alba/many.rb +5 -4
- data/lib/alba/one.rb +4 -3
- data/lib/alba/resource.rb +106 -60
- data/lib/alba/version.rb +1 -1
- metadata +4 -5
- data/Gemfile.lock +0 -92
- data/lib/alba/serializer.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3999a63b9564e6bf0a7b00dfeb64a19d6d076c94047f7ee869a4ac4a38c6646
|
4
|
+
data.tar.gz: dd7244bf63111e4795639a62dac91a744e51adb38f6a88d851c9cc6de3043dc0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f93aaadf9b8fae4b9ae334f87647c3b0f1645147fd237b62f12451f4306f5cb6699ed25c935b563b68ab45c965bb1cfb8f2c73e08b23899a477384c37124d325
|
7
|
+
data.tar.gz: adb456a4796782e725bffa5720d1b5e6f831c1e0ec215721a814b331890f4a1d0ebe592e7e906239138729dd81586ad6e1c850ece61c5495ce829551feada9a1
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -15,6 +15,7 @@ AllCops:
|
|
15
15
|
- 'benchmark/**/*.rb'
|
16
16
|
NewCops: enable
|
17
17
|
EnabledByDefault: true
|
18
|
+
TargetRubyVersion: 2.5
|
18
19
|
|
19
20
|
# Oneline comment is not valid so until it gets valid, we disable it
|
20
21
|
Bundler/GemComment:
|
@@ -36,6 +37,11 @@ Metrics/ClassLength:
|
|
36
37
|
Metrics/MethodLength:
|
37
38
|
Max: 15
|
38
39
|
|
40
|
+
# Resource class includes DSLs, which tend to accept long list of parameters
|
41
|
+
Metrics/ParameterLists:
|
42
|
+
Exclude:
|
43
|
+
- 'lib/alba/resource.rb'
|
44
|
+
|
39
45
|
Style/ConstantVisibility:
|
40
46
|
Exclude:
|
41
47
|
- 'lib/alba/version.rb'
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [1.0.0] - 2021-04-07
|
10
|
+
|
11
|
+
This is the first major release of Alba and it includes so many features. To see all the features you can have a look at [README](https://github.com/okuramasafumi/alba/blob/master/README.md#features).
|
data/Gemfile
CHANGED
@@ -9,8 +9,9 @@ gem 'minitest', '~> 5.14' # For test
|
|
9
9
|
gem 'oj', '~> 3.11', platform: :ruby, require: false # For backend
|
10
10
|
gem 'rake', '~> 13.0' # For test and automation
|
11
11
|
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
12
|
-
gem 'rubocop-minitest', '~> 0.
|
12
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
13
13
|
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
14
14
|
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
15
15
|
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
16
17
|
gem 'yard', require: false
|
data/README.md
CHANGED
@@ -19,8 +19,6 @@ Alba has some advantages over other JSON serializers which we've wanted to have.
|
|
19
19
|
|
20
20
|
DSL is great. It makes the coding experience natural and intuitive. However, remembering lots of DSL requires us a lot of effort. Unfortunately, most of the existing libraries have implemented their features via DSL and it's not easy to understand how they behave entirely. Alba's core DSL are only four (`attributes`, `attribute`, `one` and `many`) so it's easy to understand how to use.
|
21
21
|
|
22
|
-
Alba is also understandable internally. The codebase is much smaller than the alternatives. In fact, it's about 330 lines of code. Look at the code on [GitHub](https://github.com/okuramasafumi/alba/tree/master/lib) and you'll be surprised how simple it is!
|
23
|
-
|
24
22
|
### Performance
|
25
23
|
|
26
24
|
Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark).
|
@@ -56,10 +54,12 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
56
54
|
* One and many association with the ability to define them inline
|
57
55
|
* Adding condition and filter to association
|
58
56
|
* Parameters can be injected and used in attributes and associations
|
59
|
-
*
|
60
|
-
* Adding metadata
|
57
|
+
* Conditional attributes and associations
|
61
58
|
* Selectable backend
|
62
59
|
* Key transformation
|
60
|
+
* Root key inference
|
61
|
+
* Error handling
|
62
|
+
* Resource name inflection based on association name
|
63
63
|
* No runtime dependencies
|
64
64
|
|
65
65
|
## Anti features
|
@@ -69,7 +69,6 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
69
69
|
* Supporting all existing JSON encoder/decoder
|
70
70
|
* Cache
|
71
71
|
* [JSON:API](https://jsonapi.org) support
|
72
|
-
* Association name inflection
|
73
72
|
* And many others
|
74
73
|
|
75
74
|
## Usage
|
@@ -78,7 +77,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
78
77
|
|
79
78
|
Alba's configuration is fairly simple.
|
80
79
|
|
81
|
-
#### Backend
|
80
|
+
#### Backend configuration
|
82
81
|
|
83
82
|
Backend is the actual part serializing an object into JSON. Alba supports these backends.
|
84
83
|
|
@@ -92,6 +91,26 @@ You can set a backend like this:
|
|
92
91
|
Alba.backend = :oj
|
93
92
|
```
|
94
93
|
|
94
|
+
#### Inference configuration
|
95
|
+
|
96
|
+
You can enable inference feature using `enable_inference!` method.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Alba.enable_inference!
|
100
|
+
```
|
101
|
+
|
102
|
+
You must install `ActiveSupport` to enable inference.
|
103
|
+
|
104
|
+
#### Error handling configuration
|
105
|
+
|
106
|
+
You can configure error handling with `on_error` method.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Alba.on_error :ignore
|
110
|
+
```
|
111
|
+
|
112
|
+
For the details, see [Error handling section](#error-handling)
|
113
|
+
|
95
114
|
### Simple serialization with key
|
96
115
|
|
97
116
|
```ruby
|
@@ -109,6 +128,8 @@ end
|
|
109
128
|
class UserResource
|
110
129
|
include Alba::Resource
|
111
130
|
|
131
|
+
key :user
|
132
|
+
|
112
133
|
attributes :id, :name
|
113
134
|
|
114
135
|
attribute :name_with_email do |resource|
|
@@ -116,12 +137,6 @@ class UserResource
|
|
116
137
|
end
|
117
138
|
end
|
118
139
|
|
119
|
-
class SerializerWithKey
|
120
|
-
include Alba::Serializer
|
121
|
-
|
122
|
-
set key: :user
|
123
|
-
end
|
124
|
-
|
125
140
|
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
126
141
|
UserResource.new(user).serialize
|
127
142
|
# => "{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}"
|
@@ -181,7 +196,7 @@ UserResource.new(user).serialize
|
|
181
196
|
`Alba.serialize` method is a shortcut to define everything inline.
|
182
197
|
|
183
198
|
```ruby
|
184
|
-
Alba.serialize(user,
|
199
|
+
Alba.serialize(user, key: :foo) do
|
185
200
|
attributes :id
|
186
201
|
many :articles do
|
187
202
|
attributes :title, :body
|
@@ -190,7 +205,7 @@ end
|
|
190
205
|
# => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
|
191
206
|
```
|
192
207
|
|
193
|
-
Although this might be useful sometimes, it's generally recommended to define a class for
|
208
|
+
Although this might be useful sometimes, it's generally recommended to define a class for Resource.
|
194
209
|
|
195
210
|
### Inheritance and Ignorance
|
196
211
|
|
@@ -284,14 +299,147 @@ end
|
|
284
299
|
|
285
300
|
user = User.new(1, nil, nil)
|
286
301
|
UserResource.new(user).serialize # => '{"id":1}'
|
287
|
-
|
288
|
-
|
289
302
|
```
|
290
303
|
|
291
304
|
The key part is the use of `Proc#>>` since `Alba::Resource#converter` returns a `Proc` which contains the basic logic and it's impossible to change its behavior by just overriding the method.
|
292
305
|
|
293
306
|
It's not recommended to swap the whole conversion logic. It's recommended to always call `super` when you override `converter`.
|
294
307
|
|
308
|
+
### Conditional attributes
|
309
|
+
|
310
|
+
Filtering attributes with overriding `convert` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
|
311
|
+
|
312
|
+
In these cases, conditional attributes works well. We can pass `if` option to `attributes`, `attribute`, `one` and `many`. Below is an example for the same effect as [filtering attributes section](#filtering-attributes).
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
class User
|
316
|
+
attr_accessor :id, :name, :email, :created_at, :updated_at
|
317
|
+
|
318
|
+
def initialize(id, name, email)
|
319
|
+
@id = id
|
320
|
+
@name = name
|
321
|
+
@email = email
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
class UserResource
|
326
|
+
include Alba::Resource
|
327
|
+
|
328
|
+
attributes :id, :name, :email, if: proc { |user, attribute| !attribute.nil? }
|
329
|
+
end
|
330
|
+
|
331
|
+
user = User.new(1, nil, nil)
|
332
|
+
UserResource.new(user).serialize # => '{"id":1}'
|
333
|
+
```
|
334
|
+
|
335
|
+
### Inference
|
336
|
+
|
337
|
+
After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
Alba.enable_inference!
|
341
|
+
|
342
|
+
class User
|
343
|
+
attr_reader :id
|
344
|
+
attr_accessor :articles
|
345
|
+
|
346
|
+
def initialize(id)
|
347
|
+
@id = id
|
348
|
+
@articles = []
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
class Article
|
353
|
+
attr_accessor :id, :title
|
354
|
+
|
355
|
+
def initialize(id, title)
|
356
|
+
@id = id
|
357
|
+
@title = title
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
class ArticleResource
|
362
|
+
include Alba::Resource
|
363
|
+
|
364
|
+
attributes :title
|
365
|
+
end
|
366
|
+
|
367
|
+
class UserResource
|
368
|
+
include Alba::Resource
|
369
|
+
|
370
|
+
key!
|
371
|
+
|
372
|
+
attributes :id
|
373
|
+
|
374
|
+
many :articles
|
375
|
+
end
|
376
|
+
|
377
|
+
user = User.new(1)
|
378
|
+
user.articles << Article.new(1, 'The title')
|
379
|
+
|
380
|
+
UserResource.new(user).serialize # => '{"user":{"id":1,"articles":[{"title":"The title"}]}}'
|
381
|
+
UserResource.new([user]).serialize # => '{"users":[{"id":1,"articles":[{"title":"The title"}]}]}'
|
382
|
+
```
|
383
|
+
|
384
|
+
This resource automatically sets its root key to either "users" or "user", depending on the given object is collection or not.
|
385
|
+
|
386
|
+
Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
|
387
|
+
|
388
|
+
Note that to enable this feature you must install `ActiveSupport` gem.
|
389
|
+
|
390
|
+
### Error handling
|
391
|
+
|
392
|
+
You can set error handler globally or per resource using `on_error`.
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
class User
|
396
|
+
attr_accessor :id, :name
|
397
|
+
|
398
|
+
def initialize(id, name, email)
|
399
|
+
@id = id
|
400
|
+
@name = name
|
401
|
+
@email = email
|
402
|
+
end
|
403
|
+
|
404
|
+
def email
|
405
|
+
raise RuntimeError, 'Error!'
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
class UserResource
|
410
|
+
include Alba::Resource
|
411
|
+
|
412
|
+
attributes :id, :name, :email
|
413
|
+
|
414
|
+
on_error :ignore
|
415
|
+
end
|
416
|
+
|
417
|
+
user = User.new(1, 'Test', 'email@example.com')
|
418
|
+
UserResource.new(user).serialize # => '{"id":1,"name":"Test"}'
|
419
|
+
```
|
420
|
+
|
421
|
+
This way you can exclude an entry when fetching an attribute gives an exception.
|
422
|
+
|
423
|
+
There are four possible arguments `on_error` method accepts.
|
424
|
+
|
425
|
+
* `:raise` re-raises an error. This is the default behavior.
|
426
|
+
* `:ignore` ignores the entry with the error.
|
427
|
+
* `:nullify` sets the attribute with the error to `nil`.
|
428
|
+
* Block gives you more control over what to be returned.
|
429
|
+
|
430
|
+
The block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example.
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
# Global error handling
|
434
|
+
Alba.on_error do |error, object, key, attribute, resource_class|
|
435
|
+
if resource_class == MyResource
|
436
|
+
['error_fallback', object.error_fallback]
|
437
|
+
else
|
438
|
+
[key, error.message]
|
439
|
+
end
|
440
|
+
end
|
441
|
+
```
|
442
|
+
|
295
443
|
## Comparison
|
296
444
|
|
297
445
|
Alba is faster than alternatives.
|
@@ -309,18 +457,6 @@ Alba.backend = :active_support
|
|
309
457
|
|
310
458
|
The name "Alba" comes from "albatross", a kind of birds. In Japanese, this bird is called "Aho-dori", which means "stupid bird". I find it funny because in fact albatrosses fly really fast. I hope Alba looks stupid but in fact it does its job quick.
|
311
459
|
|
312
|
-
## Alba internals
|
313
|
-
|
314
|
-
Alba has three component, `Serializer`, `Resource` and `Value` (`Value` is conceptual and not implemented directly).
|
315
|
-
|
316
|
-
`Serializer` is a component responsible for rendering JSON output with `Resource`. `Serializer` can add more data to `Resource` such as `metadata`. Users can define one single `Serializer` and reuse it for all `Resource`s. The main interface is `#serialize`.
|
317
|
-
|
318
|
-
`Resource` is a component responsible for defining how an object (or a collection of objects) is converted into JSON. The difference between `Serializer` and `Resource` is that while `Serializer` can add arbitrary data into JSON, `Resource` can get data only from the object under it. The main interface is `#serializable_hash`.
|
319
|
-
|
320
|
-
`One` and `Many` are the special object fetching other resources and converting them into Hash.
|
321
|
-
|
322
|
-
The main `Alba` module holds config values and one convenience method, `.serialize`.
|
323
|
-
|
324
460
|
## Development
|
325
461
|
|
326
462
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/alba.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require_relative 'alba/version'
|
2
|
-
require_relative 'alba/serializer'
|
3
2
|
require_relative 'alba/resource'
|
4
3
|
|
5
4
|
# Core module
|
@@ -11,8 +10,7 @@ module Alba
|
|
11
10
|
class UnsupportedBackend < Error; end
|
12
11
|
|
13
12
|
class << self
|
14
|
-
attr_reader :backend, :encoder
|
15
|
-
attr_accessor :default_serializer
|
13
|
+
attr_reader :backend, :encoder, :inferring, :_on_error
|
16
14
|
|
17
15
|
# Set the backend, which actually serializes object into JSON
|
18
16
|
#
|
@@ -28,17 +26,43 @@ module Alba
|
|
28
26
|
# Serialize the object with inline definitions
|
29
27
|
#
|
30
28
|
# @param object [Object] the object to be serialized
|
31
|
-
# @param
|
29
|
+
# @param key [Symbol]
|
32
30
|
# @param block [Block] resource block
|
33
31
|
# @return [String] serialized JSON string
|
34
32
|
# @raise [ArgumentError] if block is absent or `with` argument's type is wrong
|
35
|
-
def serialize(object,
|
33
|
+
def serialize(object, key: nil, &block)
|
36
34
|
raise ArgumentError, 'Block required' unless block
|
37
35
|
|
38
36
|
resource_class.class_eval(&block)
|
39
37
|
resource = resource_class.new(object)
|
40
|
-
|
41
|
-
|
38
|
+
resource.serialize(key: key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Enable inference for key and resource name
|
42
|
+
def enable_inference!
|
43
|
+
begin
|
44
|
+
require 'active_support/inflector'
|
45
|
+
rescue LoadError
|
46
|
+
raise ::Alba::Error, 'To enable inference, please install `ActiveSupport` gem.'
|
47
|
+
end
|
48
|
+
@inferring = true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Disable inference for key and resource name
|
52
|
+
def disable_inference!
|
53
|
+
@inferring = false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set error handler
|
57
|
+
#
|
58
|
+
# @param [Symbol] handler
|
59
|
+
# @param [Block]
|
60
|
+
def on_error(handler = nil, &block)
|
61
|
+
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
62
|
+
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
63
|
+
|
64
|
+
p block if block
|
65
|
+
@_on_error = handler || block
|
42
66
|
end
|
43
67
|
|
44
68
|
private
|
@@ -86,4 +110,5 @@ module Alba
|
|
86
110
|
end
|
87
111
|
|
88
112
|
@encoder = default_encoder
|
113
|
+
@_on_error = :raise
|
89
114
|
end
|
data/lib/alba/association.rb
CHANGED
@@ -2,16 +2,27 @@ 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
|
14
|
-
|
15
|
+
@resource = resource
|
16
|
+
return if @resource
|
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
|
15
26
|
end
|
16
27
|
|
17
28
|
# @abstract
|
@@ -21,6 +32,15 @@ module Alba
|
|
21
32
|
|
22
33
|
private
|
23
34
|
|
35
|
+
def constantize(resource)
|
36
|
+
case resource # rubocop:disable Style/MissingElse
|
37
|
+
when Class
|
38
|
+
resource
|
39
|
+
when Symbol, String
|
40
|
+
Object.const_get(resource)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
24
44
|
def resource_class
|
25
45
|
klass = Class.new
|
26
46
|
klass.include(Alba::Resource)
|
data/lib/alba/many.rb
CHANGED
@@ -9,11 +9,12 @@ module Alba
|
|
9
9
|
# @param params [Hash] user-given Hash for arbitrary data
|
10
10
|
# @return [Array<Hash>]
|
11
11
|
def to_hash(target, params: {})
|
12
|
-
|
13
|
-
|
14
|
-
return if
|
12
|
+
@object = target.public_send(@name)
|
13
|
+
@object = @condition.call(@object, params) if @condition
|
14
|
+
return if @object.nil?
|
15
15
|
|
16
|
-
|
16
|
+
@resource = constantize(@resource)
|
17
|
+
@object.map { |o| @resource.new(o, params: params).to_hash }
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
data/lib/alba/one.rb
CHANGED
@@ -9,10 +9,11 @@ module Alba
|
|
9
9
|
# @param params [Hash] user-given Hash for arbitrary data
|
10
10
|
# @return [Hash]
|
11
11
|
def to_hash(target, params: {})
|
12
|
-
object = target.public_send(@name)
|
13
|
-
object = @condition.call(object, params) if @condition
|
14
|
-
return if object.nil?
|
12
|
+
@object = target.public_send(@name)
|
13
|
+
@object = @condition.call(object, params) if @condition
|
14
|
+
return if @object.nil?
|
15
15
|
|
16
|
+
@resource = constantize(@resource)
|
16
17
|
@resource.new(object, params: params).to_hash
|
17
18
|
end
|
18
19
|
end
|
data/lib/alba/resource.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require_relative 'serializer'
|
2
1
|
require_relative 'one'
|
3
2
|
require_relative 'many'
|
4
3
|
|
@@ -7,7 +6,7 @@ module Alba
|
|
7
6
|
module Resource
|
8
7
|
# @!parse include InstanceMethods
|
9
8
|
# @!parse extend ClassMethods
|
10
|
-
DSLS = {_attributes: {},
|
9
|
+
DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
|
11
10
|
private_constant :DSLS
|
12
11
|
|
13
12
|
# @private
|
@@ -25,7 +24,7 @@ module Alba
|
|
25
24
|
|
26
25
|
# Instance methods
|
27
26
|
module InstanceMethods
|
28
|
-
attr_reader :object, :
|
27
|
+
attr_reader :object, :params
|
29
28
|
|
30
29
|
# @param object [Object] the object to be serialized
|
31
30
|
# @param params [Hash] user-given Hash for arbitrary data
|
@@ -35,22 +34,14 @@ module Alba
|
|
35
34
|
DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
|
36
35
|
end
|
37
36
|
|
38
|
-
#
|
37
|
+
# Serialize object into JSON string
|
39
38
|
#
|
40
|
-
# @param
|
39
|
+
# @param key [Symbol]
|
41
40
|
# @return [String] serialized JSON string
|
42
|
-
def serialize(
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer }
|
47
|
-
with
|
48
|
-
when Proc
|
49
|
-
inline_extended_serializer(with)
|
50
|
-
else
|
51
|
-
raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
|
52
|
-
end
|
53
|
-
serializer.new(self).serialize
|
41
|
+
def serialize(key: nil)
|
42
|
+
key = key.nil? ? _key : key
|
43
|
+
hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
|
44
|
+
Alba.encoder.call(hash)
|
54
45
|
end
|
55
46
|
|
56
47
|
# A Hash for serialization
|
@@ -61,22 +52,68 @@ module Alba
|
|
61
52
|
end
|
62
53
|
alias to_hash serializable_hash
|
63
54
|
|
64
|
-
# @return [Symbol]
|
65
|
-
def key
|
66
|
-
@_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
|
67
|
-
end
|
68
|
-
|
69
55
|
private
|
70
56
|
|
71
|
-
#
|
57
|
+
# @return [String]
|
58
|
+
def _key
|
59
|
+
if @_key == true && Alba.inferring
|
60
|
+
demodulized = ActiveSupport::Inflector.demodulize(self.class.name)
|
61
|
+
meth = collection? ? :tableize : :singularize
|
62
|
+
ActiveSupport::Inflector.public_send(meth, demodulized.delete_suffix('Resource').downcase)
|
63
|
+
else
|
64
|
+
@_key.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
72
68
|
def converter
|
73
|
-
lambda do |
|
74
|
-
@_attributes.map do |key, attribute|
|
75
|
-
|
76
|
-
|
69
|
+
lambda do |object|
|
70
|
+
arrays = @_attributes.map do |key, attribute|
|
71
|
+
key = transform_key(key)
|
72
|
+
if attribute.is_a?(Array) # Conditional
|
73
|
+
conditional_attribute(object, key, attribute)
|
74
|
+
else
|
75
|
+
[key, fetch_attribute(object, attribute)]
|
76
|
+
end
|
77
|
+
rescue ::Alba::Error, FrozenError
|
78
|
+
raise
|
79
|
+
rescue StandardError => e
|
80
|
+
handle_error(e, object, key, attribute)
|
81
|
+
end
|
82
|
+
arrays.reject(&:empty?).to_h
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def conditional_attribute(object, key, attribute)
|
87
|
+
condition = attribute.last
|
88
|
+
arity = condition.arity
|
89
|
+
return [] if arity <= 1 && !condition.call(object)
|
90
|
+
|
91
|
+
fetched_attribute = fetch_attribute(object, attribute.first)
|
92
|
+
attr = if attribute.first.is_a?(Alba::Association)
|
93
|
+
attribute.first.object
|
94
|
+
else
|
95
|
+
fetched_attribute
|
96
|
+
end
|
97
|
+
return [] if arity >= 2 && !condition.call(object, attr)
|
98
|
+
|
99
|
+
[key, fetched_attribute]
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_error(error, object, key, attribute)
|
103
|
+
on_error = @_on_error || Alba._on_error
|
104
|
+
case on_error
|
105
|
+
when :raise, nil
|
106
|
+
raise
|
107
|
+
when :nullify
|
108
|
+
[key, nil]
|
109
|
+
when :ignore
|
110
|
+
[]
|
111
|
+
when Proc
|
112
|
+
on_error.call(error, object, key, attribute, self.class)
|
113
|
+
else
|
114
|
+
raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
|
77
115
|
end
|
78
116
|
end
|
79
|
-
# rubocop:enable Style/MethodCalledOnDoEndBlock
|
80
117
|
|
81
118
|
# Override this method to supply custom key transform method
|
82
119
|
def transform_key(key)
|
@@ -86,31 +123,19 @@ module Alba
|
|
86
123
|
KeyTransformer.transform(key, @_transform_keys)
|
87
124
|
end
|
88
125
|
|
89
|
-
def fetch_attribute(
|
126
|
+
def fetch_attribute(object, attribute)
|
90
127
|
case attribute
|
91
128
|
when Symbol
|
92
|
-
|
129
|
+
object.public_send attribute
|
93
130
|
when Proc
|
94
|
-
instance_exec(
|
131
|
+
instance_exec(object, &attribute)
|
95
132
|
when Alba::One, Alba::Many
|
96
|
-
attribute.to_hash(
|
133
|
+
attribute.to_hash(object, params: params)
|
97
134
|
else
|
98
135
|
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
99
136
|
end
|
100
137
|
end
|
101
138
|
|
102
|
-
def empty_serializer
|
103
|
-
klass = Class.new
|
104
|
-
klass.include Alba::Serializer
|
105
|
-
klass
|
106
|
-
end
|
107
|
-
|
108
|
-
def inline_extended_serializer(with)
|
109
|
-
klass = empty_serializer
|
110
|
-
klass.class_eval(&with)
|
111
|
-
klass
|
112
|
-
end
|
113
|
-
|
114
139
|
def collection?
|
115
140
|
@object.is_a?(Enumerable)
|
116
141
|
end
|
@@ -129,19 +154,24 @@ module Alba
|
|
129
154
|
# Set multiple attributes at once
|
130
155
|
#
|
131
156
|
# @param attrs [Array<String, Symbol>]
|
132
|
-
|
133
|
-
|
157
|
+
# @param options [Hash] option hash including `if` that is a condition to render these attributes
|
158
|
+
def attributes(*attrs, **options)
|
159
|
+
attrs.each do |attr_name|
|
160
|
+
attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
|
161
|
+
@_attributes[attr_name.to_sym] = attr
|
162
|
+
end
|
134
163
|
end
|
135
164
|
|
136
165
|
# Set an attribute with the given block
|
137
166
|
#
|
138
167
|
# @param name [String, Symbol] key name
|
168
|
+
# @param options [Hash] option hash including `if` that is a condition to render
|
139
169
|
# @param block [Block] the block called during serialization
|
140
170
|
# @raise [ArgumentError] if block is absent
|
141
|
-
def attribute(name, &block)
|
171
|
+
def attribute(name, **options, &block)
|
142
172
|
raise ArgumentError, 'No block given in attribute method' unless block
|
143
173
|
|
144
|
-
@_attributes[name.to_sym] = block
|
174
|
+
@_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
|
145
175
|
end
|
146
176
|
|
147
177
|
# Set One association
|
@@ -150,10 +180,13 @@ module Alba
|
|
150
180
|
# @param condition [Proc]
|
151
181
|
# @param resource [Class<Alba::Resource>]
|
152
182
|
# @param key [String, Symbol] used as key when given
|
183
|
+
# @param options [Hash] option hash including `if` that is a condition to render
|
153
184
|
# @param block [Block]
|
154
185
|
# @see Alba::One#initialize
|
155
|
-
def one(name, condition = nil, resource: nil, key: nil, &block)
|
156
|
-
|
186
|
+
def one(name, condition = nil, resource: nil, key: nil, **options, &block)
|
187
|
+
nesting = self.name&.rpartition('::')&.first
|
188
|
+
one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
189
|
+
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
|
157
190
|
end
|
158
191
|
alias has_one one
|
159
192
|
|
@@ -163,25 +196,27 @@ module Alba
|
|
163
196
|
# @param condition [Proc]
|
164
197
|
# @param resource [Class<Alba::Resource>]
|
165
198
|
# @param key [String, Symbol] used as key when given
|
199
|
+
# @param options [Hash] option hash including `if` that is a condition to render
|
166
200
|
# @param block [Block]
|
167
201
|
# @see Alba::Many#initialize
|
168
|
-
def many(name, condition = nil, resource: nil, key: nil, &block)
|
169
|
-
|
202
|
+
def many(name, condition = nil, resource: nil, key: nil, **options, &block)
|
203
|
+
nesting = self.name&.rpartition('::')&.first
|
204
|
+
many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
|
205
|
+
@_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
|
170
206
|
end
|
171
207
|
alias has_many many
|
172
208
|
|
173
|
-
# Set serializer for the resource
|
174
|
-
#
|
175
|
-
# @param name [Alba::Serializer]
|
176
|
-
def serializer(name)
|
177
|
-
@_serializer = name <= Alba::Serializer ? name : nil
|
178
|
-
end
|
179
|
-
|
180
209
|
# Set key
|
181
210
|
#
|
182
211
|
# @param key [String, Symbol]
|
183
212
|
def key(key)
|
184
|
-
@_key = key.to_sym
|
213
|
+
@_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
214
|
+
end
|
215
|
+
|
216
|
+
# Set key to true
|
217
|
+
#
|
218
|
+
def key!
|
219
|
+
@_key = true
|
185
220
|
end
|
186
221
|
|
187
222
|
# Delete attributes
|
@@ -200,6 +235,17 @@ module Alba
|
|
200
235
|
def transform_keys(type)
|
201
236
|
@_transform_keys = type.to_sym
|
202
237
|
end
|
238
|
+
|
239
|
+
# Set error handler
|
240
|
+
#
|
241
|
+
# @param [Symbol] handler
|
242
|
+
# @param [Block]
|
243
|
+
def on_error(handler = nil, &block)
|
244
|
+
raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
|
245
|
+
raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
|
246
|
+
|
247
|
+
@_on_error = handler || block
|
248
|
+
end
|
203
249
|
end
|
204
250
|
end
|
205
251
|
end
|
data/lib/alba/version.rb
CHANGED
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: 0.
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2021-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Alba is designed to be a simple, easy to use and fast alternative to
|
14
14
|
existing JSON serializers. Its performance is better than almost all gems which
|
@@ -23,9 +23,9 @@ files:
|
|
23
23
|
- ".gitignore"
|
24
24
|
- ".rubocop.yml"
|
25
25
|
- ".yardopts"
|
26
|
+
- CHANGELOG.md
|
26
27
|
- CODE_OF_CONDUCT.md
|
27
28
|
- Gemfile
|
28
|
-
- Gemfile.lock
|
29
29
|
- LICENSE.txt
|
30
30
|
- README.md
|
31
31
|
- Rakefile
|
@@ -39,7 +39,6 @@ files:
|
|
39
39
|
- lib/alba/many.rb
|
40
40
|
- lib/alba/one.rb
|
41
41
|
- lib/alba/resource.rb
|
42
|
-
- lib/alba/serializer.rb
|
43
42
|
- lib/alba/version.rb
|
44
43
|
- sider.yml
|
45
44
|
homepage: https://github.com/okuramasafumi/alba
|
@@ -64,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
63
|
- !ruby/object:Gem::Version
|
65
64
|
version: '0'
|
66
65
|
requirements: []
|
67
|
-
rubygems_version: 3.2.
|
66
|
+
rubygems_version: 3.2.14
|
68
67
|
signing_key:
|
69
68
|
specification_version: 4
|
70
69
|
summary: Alba is the fastest JSON serializer for Ruby.
|
data/Gemfile.lock
DELETED
@@ -1,92 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
alba (0.13.1)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
activesupport (6.1.3)
|
10
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
-
i18n (>= 1.6, < 2)
|
12
|
-
minitest (>= 5.1)
|
13
|
-
tzinfo (~> 2.0)
|
14
|
-
zeitwerk (~> 2.3)
|
15
|
-
ast (2.4.2)
|
16
|
-
concurrent-ruby (1.1.8)
|
17
|
-
coveralls (0.8.23)
|
18
|
-
json (>= 1.8, < 3)
|
19
|
-
simplecov (~> 0.16.1)
|
20
|
-
term-ansicolor (~> 1.3)
|
21
|
-
thor (>= 0.19.4, < 2.0)
|
22
|
-
tins (~> 1.6)
|
23
|
-
docile (1.3.2)
|
24
|
-
i18n (1.8.9)
|
25
|
-
concurrent-ruby (~> 1.0)
|
26
|
-
json (2.3.1)
|
27
|
-
minitest (5.14.3)
|
28
|
-
oj (3.11.2)
|
29
|
-
parallel (1.20.1)
|
30
|
-
parser (3.0.0.0)
|
31
|
-
ast (~> 2.4.1)
|
32
|
-
rainbow (3.0.0)
|
33
|
-
rake (13.0.3)
|
34
|
-
regexp_parser (2.1.1)
|
35
|
-
rexml (3.2.4)
|
36
|
-
rubocop (1.11.0)
|
37
|
-
parallel (~> 1.10)
|
38
|
-
parser (>= 3.0.0.0)
|
39
|
-
rainbow (>= 2.2.2, < 4.0)
|
40
|
-
regexp_parser (>= 1.8, < 3.0)
|
41
|
-
rexml
|
42
|
-
rubocop-ast (>= 1.2.0, < 2.0)
|
43
|
-
ruby-progressbar (~> 1.7)
|
44
|
-
unicode-display_width (>= 1.4.0, < 3.0)
|
45
|
-
rubocop-ast (1.4.1)
|
46
|
-
parser (>= 2.7.1.5)
|
47
|
-
rubocop-minitest (0.10.3)
|
48
|
-
rubocop (>= 0.87, < 2.0)
|
49
|
-
rubocop-performance (1.10.1)
|
50
|
-
rubocop (>= 0.90.0, < 2.0)
|
51
|
-
rubocop-ast (>= 0.4.0)
|
52
|
-
rubocop-rake (0.5.1)
|
53
|
-
rubocop
|
54
|
-
rubocop-sensible (0.3.0)
|
55
|
-
rubocop (>= 0.60.0)
|
56
|
-
ruby-progressbar (1.11.0)
|
57
|
-
simplecov (0.16.1)
|
58
|
-
docile (~> 1.1)
|
59
|
-
json (>= 1.8, < 3)
|
60
|
-
simplecov-html (~> 0.10.0)
|
61
|
-
simplecov-html (0.10.2)
|
62
|
-
sync (0.5.0)
|
63
|
-
term-ansicolor (1.7.1)
|
64
|
-
tins (~> 1.0)
|
65
|
-
thor (1.0.1)
|
66
|
-
tins (1.25.0)
|
67
|
-
sync
|
68
|
-
tzinfo (2.0.4)
|
69
|
-
concurrent-ruby (~> 1.0)
|
70
|
-
unicode-display_width (2.0.0)
|
71
|
-
yard (0.9.26)
|
72
|
-
zeitwerk (2.4.2)
|
73
|
-
|
74
|
-
PLATFORMS
|
75
|
-
ruby
|
76
|
-
|
77
|
-
DEPENDENCIES
|
78
|
-
activesupport
|
79
|
-
alba!
|
80
|
-
coveralls
|
81
|
-
minitest (~> 5.14)
|
82
|
-
oj (~> 3.11)
|
83
|
-
rake (~> 13.0)
|
84
|
-
rubocop (>= 0.79.0)
|
85
|
-
rubocop-minitest (~> 0.10.3)
|
86
|
-
rubocop-performance (~> 1.10.1)
|
87
|
-
rubocop-rake (>= 0.5.1)
|
88
|
-
rubocop-sensible (~> 0.3.0)
|
89
|
-
yard
|
90
|
-
|
91
|
-
BUNDLED WITH
|
92
|
-
2.2.6
|
data/lib/alba/serializer.rb
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
module Alba
|
2
|
-
# This module represents how a resource should be serialized.
|
3
|
-
module Serializer
|
4
|
-
# @!parse include InstanceMethods
|
5
|
-
# @!parse extend ClassMethods
|
6
|
-
|
7
|
-
# @private
|
8
|
-
def self.included(base)
|
9
|
-
super
|
10
|
-
base.instance_variable_set('@_opts', {}) unless base.instance_variable_defined?('@_opts')
|
11
|
-
base.instance_variable_set('@_metadata', {}) unless base.instance_variable_defined?('@_metadata')
|
12
|
-
base.include InstanceMethods
|
13
|
-
base.extend ClassMethods
|
14
|
-
end
|
15
|
-
|
16
|
-
# Instance methods
|
17
|
-
module InstanceMethods
|
18
|
-
# @param resource [Alba::Resource]
|
19
|
-
def initialize(resource)
|
20
|
-
@resource = resource
|
21
|
-
@hash = resource.serializable_hash
|
22
|
-
@hash = {key.to_sym => @hash} if key
|
23
|
-
return if metadata.empty?
|
24
|
-
|
25
|
-
# @hash is either Hash or Array
|
26
|
-
@hash.is_a?(Hash) ? @hash.merge!(metadata.to_h) : @hash << metadata
|
27
|
-
end
|
28
|
-
|
29
|
-
# Use real encoder to actually serialize to JSON
|
30
|
-
#
|
31
|
-
# @return [String] JSON string
|
32
|
-
def serialize
|
33
|
-
Alba.encoder.call(@hash)
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def key
|
39
|
-
opts = self.class._opts
|
40
|
-
opts[:key] == true ? @resource.key : opts[:key]
|
41
|
-
end
|
42
|
-
|
43
|
-
def metadata
|
44
|
-
metadata = self.class._metadata
|
45
|
-
metadata.transform_values { |block| block.call(@resource.object) }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# Class methods
|
50
|
-
module ClassMethods
|
51
|
-
attr_reader :_opts, :_metadata
|
52
|
-
|
53
|
-
# @private
|
54
|
-
def inherited(subclass)
|
55
|
-
super
|
56
|
-
%w[_opts _metadata].each { |name| subclass.instance_variable_set("@#{name}", public_send(name).clone) }
|
57
|
-
end
|
58
|
-
|
59
|
-
# Set options, currently key only
|
60
|
-
#
|
61
|
-
# @param key [Boolean, Symbol]
|
62
|
-
def set(key: false)
|
63
|
-
@_opts[:key] = key
|
64
|
-
end
|
65
|
-
|
66
|
-
# Set metadata
|
67
|
-
#
|
68
|
-
# @param name [String, Symbol] key for the metadata
|
69
|
-
# @param block [Block] the content of the metadata
|
70
|
-
def metadata(name, &block)
|
71
|
-
@_metadata[name.to_sym] = block
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|