sober_swag 0.14.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/lint.yml +41 -9
- data/.github/workflows/ruby.yml +1 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +50 -5
- data/CHANGELOG.md +34 -0
- data/README.md +155 -4
- data/bin/console +36 -0
- data/bin/rspec +29 -0
- data/docs/serializers.md +74 -9
- data/example/Gemfile +2 -2
- data/example/app/controllers/application_controller.rb +5 -0
- data/example/app/controllers/people_controller.rb +4 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/lib/sober_swag.rb +1 -0
- data/lib/sober_swag/compiler.rb +1 -0
- data/lib/sober_swag/compiler/path.rb +7 -0
- data/lib/sober_swag/compiler/primitive.rb +77 -0
- data/lib/sober_swag/compiler/type.rb +57 -96
- data/lib/sober_swag/controller.rb +3 -9
- data/lib/sober_swag/controller/route.rb +30 -8
- data/lib/sober_swag/input_object.rb +36 -3
- data/lib/sober_swag/nodes/attribute.rb +8 -7
- data/lib/sober_swag/nodes/enum.rb +2 -2
- data/lib/sober_swag/nodes/primitive.rb +1 -1
- data/lib/sober_swag/output_object/definition.rb +14 -2
- data/lib/sober_swag/output_object/field_syntax.rb +16 -0
- data/lib/sober_swag/parser.rb +10 -5
- data/lib/sober_swag/serializer/base.rb +2 -0
- data/lib/sober_swag/serializer/meta.rb +3 -1
- data/lib/sober_swag/server.rb +22 -10
- data/lib/sober_swag/type.rb +7 -0
- data/lib/sober_swag/type/named.rb +35 -0
- data/lib/sober_swag/types.rb +2 -0
- data/lib/sober_swag/types/comma_array.rb +17 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +2 -2
- metadata +18 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63ab4c5db5ca2e6fe63713b7416d127b3bdc730e82562d4ca13b477eaf362460
|
4
|
+
data.tar.gz: ce6351db33dc3506be6baa66e228532bd8e3830c9e7b67b27c2c6b899a77677c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bab9a59d19cef6552f33555a5cac081c750df97f0364d06fb39fbd5ad4973262f43ca233d52be1f0544815830345a59574dd4913ca2ec6cb2ef7f9e465502f35
|
7
|
+
data.tar.gz: f0e3db070ac4b30457d309d68a88c81b2cb090478553a622f9463e8cff9f3f67bb128f120ebcd9ce73164f21fb2e83b4a8befa02e87779e241fae96188ccc077
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
3
|
+
# Please see the documentation for all configuration options:
|
4
|
+
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
5
|
+
|
6
|
+
version: 2
|
7
|
+
updates:
|
8
|
+
- package-ecosystem: "bundler"
|
9
|
+
directory: "/"
|
10
|
+
schedule:
|
11
|
+
interval: "daily"
|
12
|
+
- package-ecosystem: "bundler"
|
13
|
+
directory: "/example"
|
14
|
+
schedule:
|
15
|
+
interval: "daily"
|
data/.github/workflows/lint.yml
CHANGED
@@ -1,15 +1,47 @@
|
|
1
|
-
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
2
7
|
|
3
|
-
|
8
|
+
name: Ruby Lint
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
4
15
|
|
5
16
|
jobs:
|
6
|
-
|
17
|
+
test:
|
18
|
+
|
7
19
|
runs-on: ubuntu-latest
|
20
|
+
strategy:
|
21
|
+
matrix:
|
22
|
+
ruby: [ '2.6', '2.7' ]
|
23
|
+
|
8
24
|
steps:
|
9
|
-
- uses: actions/checkout@
|
10
|
-
- name:
|
11
|
-
uses:
|
25
|
+
- uses: actions/checkout@v2
|
26
|
+
- name: Set up Ruby
|
27
|
+
uses: ruby/setup-ruby@v1
|
28
|
+
with:
|
29
|
+
ruby-version: ${{ matrix.ruby }}
|
30
|
+
- uses: actions/cache@v2
|
31
|
+
with:
|
32
|
+
path: vendor/bundle
|
33
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
|
34
|
+
restore-keys: |
|
35
|
+
${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
|
36
|
+
- name: Install dependencies
|
37
|
+
run: |
|
38
|
+
bundle config path vendor/bundle
|
39
|
+
bundle install
|
40
|
+
- name: Run Lints
|
41
|
+
run: bundle exec rubocop -P
|
42
|
+
- uses: actions/cache@v2
|
12
43
|
with:
|
13
|
-
|
14
|
-
|
15
|
-
|
44
|
+
path: example/vendor/bundle
|
45
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
46
|
+
restore-keys: |
|
47
|
+
${{ runner.os }}-${{ matrix.ruby }}-example-deps-
|
data/.github/workflows/ruby.yml
CHANGED
@@ -20,14 +20,10 @@ jobs:
|
|
20
20
|
strategy:
|
21
21
|
matrix:
|
22
22
|
ruby: [ '2.6', '2.7' ]
|
23
|
-
|
24
23
|
steps:
|
25
24
|
- uses: actions/checkout@v2
|
26
25
|
- name: Set up Ruby
|
27
|
-
|
28
|
-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
29
|
-
# uses: ruby/setup-ruby@v1
|
30
|
-
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
|
26
|
+
uses: ruby/setup-ruby@v1
|
31
27
|
with:
|
32
28
|
ruby-version: ${{ matrix.ruby }}
|
33
29
|
- uses: actions/cache@v2
|
data/.rubocop.yml
CHANGED
@@ -1,81 +1,126 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
Style/BlockDelimiters:
|
4
|
-
EnforcedStyle: braces_for_chaining
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
5
3
|
AllCops:
|
6
4
|
TargetRubyVersion: 2.6.0
|
7
5
|
Exclude:
|
8
6
|
- 'bin/bundle'
|
9
7
|
- 'example/bin/bundle'
|
8
|
+
- 'vendor/bundle/**/*'
|
10
9
|
|
11
10
|
Layout/LineLength:
|
12
11
|
Max: 160
|
13
|
-
|
12
|
+
|
14
13
|
RSpec/NamedSubject:
|
15
14
|
Enabled: false
|
15
|
+
|
16
16
|
RSpec/DescribeClass:
|
17
17
|
Enabled: false
|
18
|
+
|
18
19
|
Metrics/BlockLength:
|
19
20
|
Exclude:
|
20
21
|
- 'spec/**/*.rb'
|
21
22
|
- 'sober_swag.gemspec'
|
22
23
|
- 'example/spec/**/*.rb'
|
24
|
+
|
23
25
|
RSpec/ImplicitBlockExpectation:
|
24
26
|
Enabled: false
|
27
|
+
|
25
28
|
RSpec/ImplicitExpect:
|
26
29
|
EnforcedStyle: should
|
30
|
+
|
31
|
+
RSpec/LeadingSubject:
|
32
|
+
Enabled: false
|
33
|
+
|
27
34
|
Style/MultilineBlockChain:
|
28
35
|
Enabled: false
|
36
|
+
|
29
37
|
Metrics/AbcSize:
|
30
38
|
Enabled: false
|
39
|
+
|
31
40
|
Style/Documentation:
|
32
41
|
Exclude:
|
33
42
|
- 'example/db/migrate/**/*'
|
43
|
+
|
34
44
|
Metrics/PerceivedComplexity:
|
35
45
|
Enabled: false
|
46
|
+
|
36
47
|
Layout/EmptyLinesAroundAttributeAccessor:
|
37
48
|
Enabled: true
|
49
|
+
|
38
50
|
Layout/SpaceAroundMethodCallOperator:
|
39
51
|
Enabled: true
|
52
|
+
|
40
53
|
Lint/DeprecatedOpenSSLConstant:
|
41
54
|
Enabled: true
|
55
|
+
|
42
56
|
Lint/DuplicateElsifCondition:
|
43
57
|
Enabled: true
|
58
|
+
|
44
59
|
Lint/MixedRegexpCaptureTypes:
|
45
60
|
Enabled: true
|
61
|
+
|
46
62
|
Lint/RaiseException:
|
47
63
|
Enabled: true
|
64
|
+
|
48
65
|
Lint/StructNewOverride:
|
49
66
|
Enabled: true
|
67
|
+
|
50
68
|
Style/AccessorGrouping:
|
51
69
|
Enabled: true
|
70
|
+
|
52
71
|
Style/ArrayCoercion:
|
53
72
|
Enabled: true
|
73
|
+
|
54
74
|
Style/BisectedAttrAccessor:
|
55
75
|
Enabled: true
|
76
|
+
|
56
77
|
Style/CaseLikeIf:
|
57
78
|
Enabled: true
|
79
|
+
|
58
80
|
Style/ExponentialNotation:
|
59
81
|
Enabled: true
|
82
|
+
|
60
83
|
Style/HashAsLastArrayItem:
|
61
84
|
Enabled: true
|
85
|
+
|
62
86
|
Style/HashEachMethods:
|
63
87
|
Enabled: true
|
88
|
+
|
64
89
|
Style/HashLikeCase:
|
65
90
|
Enabled: true
|
91
|
+
|
66
92
|
Style/HashTransformKeys:
|
67
93
|
Enabled: true
|
94
|
+
|
68
95
|
Style/HashTransformValues:
|
69
96
|
Enabled: true
|
97
|
+
|
70
98
|
Style/RedundantAssignment:
|
71
99
|
Enabled: true
|
100
|
+
|
72
101
|
Style/RedundantFetchBlock:
|
73
102
|
Enabled: true
|
103
|
+
|
74
104
|
Style/RedundantFileExtensionInRequire:
|
75
105
|
Enabled: true
|
106
|
+
|
76
107
|
Style/RedundantRegexpCharacterClass:
|
77
108
|
Enabled: true
|
109
|
+
|
78
110
|
Style/RedundantRegexpEscape:
|
79
111
|
Enabled: true
|
112
|
+
|
80
113
|
Style/SlicingWithRange:
|
81
114
|
Enabled: true
|
115
|
+
|
116
|
+
RSpec/NestedGroups:
|
117
|
+
Max: 5
|
118
|
+
|
119
|
+
RSpec/ExampleLength:
|
120
|
+
Max: 10
|
121
|
+
|
122
|
+
Style/FrozenStringLiteralComment:
|
123
|
+
Enabled: false
|
124
|
+
|
125
|
+
Style/BlockDelimiters:
|
126
|
+
EnforcedStyle: braces_for_chaining
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [v0.19.0] 2021-03-10
|
4
|
+
|
5
|
+
- Use [redoc](https://github.com/Redocly/redoc) for generated documentation UI
|
6
|
+
|
7
|
+
## [v0.18.0] 2021-03-02
|
8
|
+
|
9
|
+
- Add generic hash type for primitive types
|
10
|
+
|
11
|
+
## [v0.17.0]: 2020-11-30
|
12
|
+
|
13
|
+
- Allow tagging endpoints via the new `tags` method.
|
14
|
+
|
15
|
+
## [v0.16.0]: 2020-10-23
|
16
|
+
|
17
|
+
- Allow non-class types to be used as the inputs to controllers
|
18
|
+
|
19
|
+
## [v0.15.0]: 2020-09-02
|
20
|
+
|
21
|
+
### Added
|
22
|
+
- Add a new `#merge` method to output objects, which will merge fields from another output object into the given output object.
|
23
|
+
- Add `multi` to Output Objects, as a way to define more than one field of the same type at once.
|
24
|
+
- Add an `inherits:` key to output objects, for view inheritance.
|
25
|
+
- Add `SoberSwag::Types::CommaArray`, which parses comma-separated strings into arrays.
|
26
|
+
This also sets `style` to `form` and `explode` to `false` when generating Swagger docs.
|
27
|
+
This class is mostly useful for query parameters where you want a simpler format: `tag=foo,bar` instead of `tag[]=foo,tag[]=bar`.
|
28
|
+
- Add support for using `meta` to specify alternative `style` and `explode` keys for query and path params.
|
29
|
+
Note that this support *does not* extend to parsing: If you modify the `style` or `explode` keywords, you will need to make those input formats work with the actual type yourself.
|
30
|
+
|
31
|
+
### Fixed
|
32
|
+
- No longer swallow `Dry::Struct` errors, instead let them surface to the user.
|
33
|
+
|
34
|
+
[v0.15.0]: https://github.com/SonderMindOrg/sober_swag/releases/tag/v0.15.0
|
data/README.md
CHANGED
@@ -9,6 +9,10 @@ This generates documentation from *types*, which (conveniently) also lets you ge
|
|
9
9
|
|
10
10
|
An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
|
11
11
|
|
12
|
+
Further documentation on using the gem is available in the `docs/` directory:
|
13
|
+
|
14
|
+
- [Serializers](docs/serializers.md)
|
15
|
+
|
12
16
|
## Types for a fully-automated API
|
13
17
|
|
14
18
|
SoberSwag lets you type your API using describe blocks.
|
@@ -18,7 +22,14 @@ This lets you type your API endpoint:
|
|
18
22
|
```ruby
|
19
23
|
class PeopleController < ApplicationController
|
20
24
|
include SoberSwag::Controller
|
25
|
+
|
21
26
|
define :patch, :update, '/people/{id}' do
|
27
|
+
summary 'Update a Person record.'
|
28
|
+
description <<~MARKDOWN
|
29
|
+
You can use this endpoint to update a Person record. Note that age cannot
|
30
|
+
be a negative integer.
|
31
|
+
MARKDOWN
|
32
|
+
|
22
33
|
query_params do
|
23
34
|
attribute? :include_extra_info, Types::Params::Bool
|
24
35
|
end
|
@@ -28,11 +39,13 @@ class PeopleController < ApplicationController
|
|
28
39
|
end
|
29
40
|
path_params { attribute :id, Types::Params::Integer }
|
30
41
|
end
|
42
|
+
def update
|
43
|
+
# update action here
|
44
|
+
end
|
31
45
|
end
|
32
46
|
```
|
33
47
|
|
34
|
-
|
35
|
-
More than that, we can use this information *inside* our controller methods:
|
48
|
+
Then we can use the information from our SoberSwag definition *inside* the controller method:
|
36
49
|
|
37
50
|
```ruby
|
38
51
|
def update
|
@@ -44,6 +57,51 @@ end
|
|
44
57
|
No need for `params.require` or anything like that.
|
45
58
|
You define the type of parameters you accept, and we reject anything that doesn't fit.
|
46
59
|
|
60
|
+
### Rendering Swagger documentation from SoberSwag
|
61
|
+
|
62
|
+
We can also use the information from SoberSwag objects to generate Swagger
|
63
|
+
documentation, available at the `swagger` action on this controller.
|
64
|
+
|
65
|
+
You can create the `swagger` action for a controller as follows:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# config/routes.rb
|
69
|
+
Rails.application.routes.draw do
|
70
|
+
# Add a `swagger` GET endpoint to render the Swagger documentation created
|
71
|
+
# by SoberSwag.
|
72
|
+
resources :people do
|
73
|
+
get :swagger, on: :collection
|
74
|
+
end
|
75
|
+
|
76
|
+
# Or use a concern to make it easier to enable swagger endpoints for a number
|
77
|
+
# of controllers at once.
|
78
|
+
concern :swaggerable do
|
79
|
+
get :swagger, on: :collection
|
80
|
+
end
|
81
|
+
|
82
|
+
resources :people, concerns: :swaggerable do
|
83
|
+
get :search, on: :collection
|
84
|
+
end
|
85
|
+
|
86
|
+
resources :places, only: [:index], concerns: :swaggerable
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
If you don't want the API documentation to show up in certain cases, you can
|
91
|
+
use an environment variable or a check on the current Rails environment.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# config/routes.rb
|
95
|
+
Rails.application.routes.draw do
|
96
|
+
resources :people do
|
97
|
+
# Enable based on environment variable.
|
98
|
+
get :swagger, on: :collection if ENV['ENABLE_SWAGGER']
|
99
|
+
# Or just disable in production.
|
100
|
+
get :swagger, on: :collection unless Rails.env.production?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
47
105
|
### Typed Responses
|
48
106
|
|
49
107
|
Want to go further and type your responses too?
|
@@ -53,6 +111,8 @@ Use SoberSwag output objects, a serializer library heavily inspired by [Blueprin
|
|
53
111
|
PersonOutputObject = SoberSwag::OutputObject.define do
|
54
112
|
field :id, primitive(:Integer)
|
55
113
|
field :name, primitive(:String).optional
|
114
|
+
# For fields that don't map to a simple attribute on your model, you can
|
115
|
+
# use a block.
|
56
116
|
field :is_registered, primitive(:Bool) do |person|
|
57
117
|
person.registered?
|
58
118
|
end
|
@@ -95,7 +155,7 @@ User = SoberSwag.input_object do
|
|
95
155
|
attribute :name, SoberSwag::Types::String
|
96
156
|
# use ? if attributes are not required
|
97
157
|
attribute? :favorite_movie, SoberSwag::Types::String
|
98
|
-
# use .optional if attributes may be
|
158
|
+
# use .optional if attributes may be nil
|
99
159
|
attribute :age, SoberSwag::Types::Params::Integer.optional
|
100
160
|
end
|
101
161
|
```
|
@@ -120,6 +180,63 @@ end
|
|
120
180
|
Under the hood, this literally just generates a subclass of `Dry::Struct`.
|
121
181
|
We use the DSL-like method just to make working with Rails' reloading less annoying.
|
122
182
|
|
183
|
+
#### Nested object attributes
|
184
|
+
|
185
|
+
You can nest attributes using a block. They'll return as nested JSON objects.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
User = SoberSwag.input_object do
|
189
|
+
attribute :user_notes do
|
190
|
+
attribute :note, SoberSwag::Types::String
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
If you want to use a specific type of object within an input object, you can
|
196
|
+
nest them by setting the other input object as the type of an attribute. For
|
197
|
+
example, if you had a UserGroup object with various Users, you could write
|
198
|
+
them like this:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
User = SoberSwag.input_object do
|
202
|
+
attribute :name, SoberSwag::Types::String
|
203
|
+
attribute :age, SoberSwag::Types::Params::Integer.optional
|
204
|
+
end
|
205
|
+
|
206
|
+
UserGroup = SoberSwag.input_object do
|
207
|
+
attribute :name, SoberSwag::Types::String
|
208
|
+
attribute :users, SoberSwag::Types::Array.of(User)
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
#### Input and Output Object Identifiers
|
213
|
+
|
214
|
+
Both input objects and output objects accept an identifier, which is used in
|
215
|
+
the Swagger Documentation to disambiguate between SoberSwag types.
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
User = SoberSwag.input_object do
|
219
|
+
identifier 'User'
|
220
|
+
|
221
|
+
attribute? :name, SoberSwag::Types::String
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
PersonOutputObject = SoberSwag::OutputObject.define do
|
227
|
+
identifier 'PersonOutput'
|
228
|
+
|
229
|
+
field :id, primitive(:Integer)
|
230
|
+
field :name, primitive(:String).optional
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
You can use these to make your Swagger documentation a bit easier to follow,
|
235
|
+
and it can also be useful for 'namespacing' objects if you're developing in
|
236
|
+
a large application, e.g. if you had a pet store and for some reason users
|
237
|
+
with cats and users with dogs were different, you could namespace it with
|
238
|
+
`identifier 'Dogs.User'`.
|
239
|
+
|
123
240
|
#### Adding additional documentation
|
124
241
|
|
125
242
|
You can use the `.meta` attribute on a type to add additional documentation.
|
@@ -151,10 +268,44 @@ QueryInput = SoberSwag.input_object do
|
|
151
268
|
end
|
152
269
|
```
|
153
270
|
|
271
|
+
## Tags
|
272
|
+
|
273
|
+
If you want to organize your API into sections, you can use `tags`.
|
274
|
+
It's quite simple:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
define :patch, :update, '/people/{id}' do
|
278
|
+
# other cool config
|
279
|
+
tags 'people', 'mutations', 'incurs_cost'
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
This will map to OpenAPI's `tags` field (naturally), and the UI codegen will automatically organize your endpoints by their tags.
|
284
|
+
|
285
|
+
## Testing the validity of output objects
|
286
|
+
|
287
|
+
If you're using RSpec and want to test the validity of output objects, you can do so relatively easily.
|
288
|
+
|
289
|
+
For example, assuming that you have a `UserOutputObject` class for representing a User record, and you have a `:user` factory via FactoryBot, you can validate that the serialization works without error like so:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
RSpec.describe UserOutputObject do
|
293
|
+
describe 'serialized result' do
|
294
|
+
subject do
|
295
|
+
described_class.type.new(described_class.serialize(create(:user)))
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'works with an object' do
|
299
|
+
expect { subject }.not_to raise_error
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
154
305
|
## Special Thanks
|
155
306
|
|
156
307
|
This gem is a mishmash of ideas from various sources.
|
157
308
|
The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
|
158
309
|
On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
|
159
310
|
The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/).
|
160
|
-
Generating the swagger
|
311
|
+
Generating the swagger documentation happens via the use of a catamorphism, which I believe I first really understood thanks to [this medium article by Jared Tobin](https://medium.com/@jaredtobin/practical-recursion-schemes-c10648ec1c29).
|