alba 0.11.0 → 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/.github/workflows/main.yml +25 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +12 -5
- data/.yardopts +2 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +7 -4
- data/README.md +264 -29
- data/benchmark/local.rb +198 -0
- data/lib/alba.rb +52 -11
- data/lib/alba/association.rb +28 -3
- data/lib/alba/key_transformer.rb +31 -0
- data/lib/alba/many.rb +12 -4
- data/lib/alba/one.rb +11 -3
- data/lib/alba/resource.rb +176 -58
- data/lib/alba/version.rb +1 -1
- data/sider.yml +1 -0
- metadata +8 -6
- data/.travis.yml +0 -9
- data/Gemfile.lock +0 -89
- data/lib/alba/serializer.rb +0 -59
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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on: [push,pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
11
|
+
ruby: [2.5, 2.6, 2.7, 3.0, head, truffleruby]
|
12
|
+
exclude:
|
13
|
+
- os: windows-latest
|
14
|
+
ruby: truffleruby
|
15
|
+
runs-on: ${{ matrix.os }}
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v2
|
18
|
+
- name: Set up Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: Run the default task
|
24
|
+
run: |
|
25
|
+
bundle exec rake
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -6,21 +6,21 @@ inherit_gem:
|
|
6
6
|
require:
|
7
7
|
- rubocop-minitest
|
8
8
|
- rubocop-performance
|
9
|
+
- rubocop-rake
|
9
10
|
|
10
11
|
AllCops:
|
11
12
|
Exclude:
|
12
13
|
- 'Rakefile'
|
13
14
|
- 'alba.gemspec'
|
15
|
+
- 'benchmark/**/*.rb'
|
14
16
|
NewCops: enable
|
15
17
|
EnabledByDefault: true
|
18
|
+
TargetRubyVersion: 2.5
|
16
19
|
|
17
20
|
# Oneline comment is not valid so until it gets valid, we disable it
|
18
21
|
Bundler/GemComment:
|
19
22
|
Enabled: false
|
20
23
|
|
21
|
-
Layout/ClassStructure:
|
22
|
-
Enabled: true
|
23
|
-
|
24
24
|
Layout/SpaceInsideHashLiteralBraces:
|
25
25
|
EnforcedStyle: no_space
|
26
26
|
|
@@ -37,13 +37,20 @@ Metrics/ClassLength:
|
|
37
37
|
Metrics/MethodLength:
|
38
38
|
Max: 15
|
39
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
|
+
|
40
45
|
Style/ConstantVisibility:
|
41
|
-
|
46
|
+
Exclude:
|
47
|
+
- 'lib/alba/version.rb'
|
42
48
|
|
43
49
|
Style/Copyright:
|
44
50
|
Enabled: false
|
45
51
|
|
46
|
-
|
52
|
+
# I know what I do :)
|
53
|
+
Style/DisableCopsWithinSourceCodeDirective:
|
47
54
|
Enabled: false
|
48
55
|
|
49
56
|
Style/FrozenStringLiteralComment:
|
data/.yardopts
ADDED
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
@@ -5,10 +5,13 @@ gemspec
|
|
5
5
|
|
6
6
|
gem 'activesupport', require: false # For backend
|
7
7
|
gem 'coveralls', require: false # For test coverage
|
8
|
-
gem 'minitest', '~> 5.
|
9
|
-
gem 'oj', '~> 3.
|
8
|
+
gem 'minitest', '~> 5.14' # For test
|
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.
|
13
|
-
gem 'rubocop-performance', '~> 1.
|
12
|
+
gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
|
13
|
+
gem 'rubocop-performance', '~> 1.10.1', require: false # For lint
|
14
|
+
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
14
15
|
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
16
|
+
gem 'ruby-prof', require: false # For performance profiling
|
17
|
+
gem 'yard', require: false
|
data/README.md
CHANGED
@@ -2,26 +2,26 @@
|
|
2
2
|
[![Build Status](https://travis-ci.com/okuramasafumi/alba.svg?branch=master)](https://travis-ci.com/okuramasafumi/alba)
|
3
3
|
[![Coverage Status](https://coveralls.io/repos/github/okuramasafumi/alba/badge.svg?branch=master)](https://coveralls.io/github/okuramasafumi/alba?branch=master)
|
4
4
|
[![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
|
5
|
+
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)
|
6
|
+
![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)
|
5
7
|
|
6
8
|
# Alba
|
7
9
|
|
8
10
|
`Alba` is the fastest JSON serializer for Ruby.
|
9
11
|
|
10
|
-
|
12
|
+
## Why yet another JSON serializer?
|
11
13
|
|
12
14
|
We know that there are several other JSON serializers for Ruby around, but none of them made us satisfied.
|
13
15
|
|
14
16
|
Alba has some advantages over other JSON serializers which we've wanted to have.
|
15
17
|
|
16
|
-
|
18
|
+
### Easy to understand
|
17
19
|
|
18
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.
|
19
21
|
|
20
|
-
|
22
|
+
### Performance
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
Alba is faster than most of the alternatives. We have a [benchmark](https://gist.github.com/okuramasafumi/4e375525bd3a28e4ca812d2a3b3e5829).
|
24
|
+
Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark).
|
25
25
|
|
26
26
|
## Installation
|
27
27
|
|
@@ -43,13 +43,41 @@ Or install it yourself as:
|
|
43
43
|
|
44
44
|
Alba supports CRuby 2.5.7 and higher and latest TruffleRuby.
|
45
45
|
|
46
|
+
## Documentation
|
47
|
+
|
48
|
+
You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramasafumi/alba).
|
49
|
+
|
50
|
+
## Features
|
51
|
+
|
52
|
+
* Resource-based serialization
|
53
|
+
* Arbitrary attribute definition
|
54
|
+
* One and many association with the ability to define them inline
|
55
|
+
* Adding condition and filter to association
|
56
|
+
* Parameters can be injected and used in attributes and associations
|
57
|
+
* Conditional attributes and associations
|
58
|
+
* Selectable backend
|
59
|
+
* Key transformation
|
60
|
+
* Root key inference
|
61
|
+
* Error handling
|
62
|
+
* Resource name inflection based on association name
|
63
|
+
* No runtime dependencies
|
64
|
+
|
65
|
+
## Anti features
|
66
|
+
|
67
|
+
* Sorting keys
|
68
|
+
* Class level support of parameters
|
69
|
+
* Supporting all existing JSON encoder/decoder
|
70
|
+
* Cache
|
71
|
+
* [JSON:API](https://jsonapi.org) support
|
72
|
+
* And many others
|
73
|
+
|
46
74
|
## Usage
|
47
75
|
|
48
76
|
### Configuration
|
49
77
|
|
50
78
|
Alba's configuration is fairly simple.
|
51
79
|
|
52
|
-
#### Backend
|
80
|
+
#### Backend configuration
|
53
81
|
|
54
82
|
Backend is the actual part serializing an object into JSON. Alba supports these backends.
|
55
83
|
|
@@ -63,6 +91,26 @@ You can set a backend like this:
|
|
63
91
|
Alba.backend = :oj
|
64
92
|
```
|
65
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
|
+
|
66
114
|
### Simple serialization with key
|
67
115
|
|
68
116
|
```ruby
|
@@ -80,6 +128,8 @@ end
|
|
80
128
|
class UserResource
|
81
129
|
include Alba::Resource
|
82
130
|
|
131
|
+
key :user
|
132
|
+
|
83
133
|
attributes :id, :name
|
84
134
|
|
85
135
|
attribute :name_with_email do |resource|
|
@@ -87,12 +137,6 @@ class UserResource
|
|
87
137
|
end
|
88
138
|
end
|
89
139
|
|
90
|
-
class SerializerWithKey
|
91
|
-
include Alba::Serializer
|
92
|
-
|
93
|
-
set key: :user
|
94
|
-
end
|
95
|
-
|
96
140
|
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
97
141
|
UserResource.new(user).serialize
|
98
142
|
# => "{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}"
|
@@ -129,7 +173,7 @@ class ArticleResource
|
|
129
173
|
attributes :title
|
130
174
|
end
|
131
175
|
|
132
|
-
class
|
176
|
+
class UserResource
|
133
177
|
include Alba::Resource
|
134
178
|
|
135
179
|
attributes :id
|
@@ -143,7 +187,7 @@ user.articles << article1
|
|
143
187
|
article2 = Article.new(2, 'Super nice', 'Really nice!')
|
144
188
|
user.articles << article2
|
145
189
|
|
146
|
-
|
190
|
+
UserResource.new(user).serialize
|
147
191
|
# => '{"id":1,"articles":[{"title":"Hello World!"},{"title":"Super nice"}]}'
|
148
192
|
```
|
149
193
|
|
@@ -152,7 +196,7 @@ UserResource1.new(user).serialize
|
|
152
196
|
`Alba.serialize` method is a shortcut to define everything inline.
|
153
197
|
|
154
198
|
```ruby
|
155
|
-
Alba.serialize(user,
|
199
|
+
Alba.serialize(user, key: :foo) do
|
156
200
|
attributes :id
|
157
201
|
many :articles do
|
158
202
|
attributes :title, :body
|
@@ -161,7 +205,7 @@ end
|
|
161
205
|
# => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
|
162
206
|
```
|
163
207
|
|
164
|
-
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.
|
165
209
|
|
166
210
|
### Inheritance and Ignorance
|
167
211
|
|
@@ -193,6 +237,209 @@ RestrictedFooResouce.new(foo).serialize
|
|
193
237
|
end
|
194
238
|
```
|
195
239
|
|
240
|
+
### Attribute key transformation
|
241
|
+
|
242
|
+
** Note: You need to install `active_support` gem to use `transform_keys` DSL.
|
243
|
+
|
244
|
+
With `active_support` installed, you can transform attribute keys.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
class User
|
248
|
+
attr_reader :id, :first_name, :last_name
|
249
|
+
|
250
|
+
def initialize(id, first_name, last_name)
|
251
|
+
@id = id
|
252
|
+
@first_name = first_name
|
253
|
+
@last_name = last_name
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class UserResource
|
258
|
+
include Alba::Resource
|
259
|
+
|
260
|
+
attributes :id, :first_name, :last_name
|
261
|
+
|
262
|
+
transform_keys :lower_camel
|
263
|
+
end
|
264
|
+
|
265
|
+
user = User.new(1, 'Masafumi', 'Okura')
|
266
|
+
UserResourceCamel.new(user).serialize
|
267
|
+
# => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}'
|
268
|
+
```
|
269
|
+
|
270
|
+
Supported transformation types are :camel, :lower_camel and :dash.
|
271
|
+
|
272
|
+
### Filtering attributes
|
273
|
+
|
274
|
+
You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky.
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
class User
|
278
|
+
attr_accessor :id, :name, :email, :created_at, :updated_at
|
279
|
+
|
280
|
+
def initialize(id, name, email)
|
281
|
+
@id = id
|
282
|
+
@name = name
|
283
|
+
@email = email
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class UserResource
|
288
|
+
include Alba::Resource
|
289
|
+
|
290
|
+
attributes :id, :name, :email
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
# Here using `Proc#>>` method to compose a proc from `super`
|
295
|
+
def converter
|
296
|
+
super >> proc { |hash| hash.compact }
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
user = User.new(1, nil, nil)
|
301
|
+
UserResource.new(user).serialize # => '{"id":1}'
|
302
|
+
```
|
303
|
+
|
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.
|
305
|
+
|
306
|
+
It's not recommended to swap the whole conversion logic. It's recommended to always call `super` when you override `converter`.
|
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
|
+
|
196
443
|
## Comparison
|
197
444
|
|
198
445
|
Alba is faster than alternatives.
|
@@ -210,18 +457,6 @@ Alba.backend = :active_support
|
|
210
457
|
|
211
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.
|
212
459
|
|
213
|
-
## Alba internals
|
214
|
-
|
215
|
-
Alba has three component, `Serializer`, `Resource` and `Value` (`Value` is conceptual and not implemented directly).
|
216
|
-
|
217
|
-
`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`.
|
218
|
-
|
219
|
-
`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`.
|
220
|
-
|
221
|
-
`One` and `Many` are the special object fetching other resources and converting them into Hash.
|
222
|
-
|
223
|
-
The main `Alba` module holds config values and one convenience method, `.serialize`.
|
224
|
-
|
225
460
|
## Development
|
226
461
|
|
227
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.
|