sober_swag 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +5 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/ruby.yml +23 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +73 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +29 -5
- data/README.md +109 -0
- data/bin/console +15 -14
- data/docs/serializers.md +203 -0
- data/example/.rspec +1 -0
- data/example/.ruby-version +1 -1
- data/example/Gemfile +10 -6
- data/example/Gemfile.lock +96 -76
- data/example/app/controllers/people_controller.rb +37 -21
- data/example/app/controllers/posts_controller.rb +102 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/person.rb +6 -0
- data/example/app/models/post.rb +9 -0
- data/example/app/output_objects/person_errors_output_object.rb +5 -0
- data/example/app/output_objects/person_output_object.rb +15 -0
- data/example/app/output_objects/post_output_object.rb +10 -0
- data/example/bin/bundle +24 -20
- data/example/bin/rails +1 -1
- data/example/bin/rake +1 -1
- data/example/config/application.rb +11 -7
- data/example/config/environments/development.rb +0 -1
- data/example/config/environments/production.rb +3 -3
- data/example/config/puma.rb +5 -5
- data/example/config/routes.rb +3 -0
- data/example/config/spring.rb +4 -4
- data/example/db/migrate/20200311152021_create_people.rb +0 -1
- data/example/db/migrate/20200603172347_create_posts.rb +11 -0
- data/example/db/schema.rb +16 -7
- data/example/spec/rails_helper.rb +64 -0
- data/example/spec/requests/people/create_spec.rb +52 -0
- data/example/spec/requests/people/get_spec.rb +35 -0
- data/example/spec/requests/people/index_spec.rb +69 -0
- data/example/spec/spec_helper.rb +94 -0
- data/lib/sober_swag.rb +6 -3
- data/lib/sober_swag/compiler/error.rb +2 -0
- data/lib/sober_swag/compiler/path.rb +2 -5
- data/lib/sober_swag/compiler/paths.rb +0 -1
- data/lib/sober_swag/compiler/type.rb +28 -15
- data/lib/sober_swag/controller.rb +16 -11
- data/lib/sober_swag/controller/route.rb +18 -21
- data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
- data/lib/sober_swag/input_object.rb +28 -0
- data/lib/sober_swag/nodes/array.rb +1 -1
- data/lib/sober_swag/nodes/base.rb +2 -4
- data/lib/sober_swag/nodes/binary.rb +2 -1
- data/lib/sober_swag/nodes/enum.rb +4 -2
- data/lib/sober_swag/nodes/list.rb +0 -1
- data/lib/sober_swag/nodes/primitive.rb +6 -5
- data/lib/sober_swag/output_object.rb +102 -0
- data/lib/sober_swag/output_object/definition.rb +30 -0
- data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
- data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
- data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
- data/lib/sober_swag/parser.rb +5 -3
- data/lib/sober_swag/serializer.rb +5 -2
- data/lib/sober_swag/serializer/array.rb +12 -0
- data/lib/sober_swag/serializer/base.rb +50 -1
- data/lib/sober_swag/serializer/conditional.rb +15 -2
- data/lib/sober_swag/serializer/field_list.rb +29 -6
- data/lib/sober_swag/serializer/mapped.rb +12 -2
- data/lib/sober_swag/serializer/meta.rb +35 -0
- data/lib/sober_swag/serializer/optional.rb +17 -2
- data/lib/sober_swag/serializer/primitive.rb +4 -1
- data/lib/sober_swag/server.rb +83 -0
- data/lib/sober_swag/types.rb +3 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +6 -4
- metadata +77 -44
- data/example/person.json +0 -4
- data/example/test/controllers/.keep +0 -0
- data/example/test/fixtures/.keep +0 -0
- data/example/test/fixtures/files/.keep +0 -0
- data/example/test/fixtures/people.yml +0 -11
- data/example/test/integration/.keep +0 -0
- data/example/test/models/.keep +0 -0
- data/example/test/models/person_test.rb +0 -7
- data/example/test/test_helper.rb +0 -13
- data/lib/sober_swag/blueprint.rb +0 -113
- data/lib/sober_swag/path.rb +0 -8
- data/lib/sober_swag/path/integer.rb +0 -21
- data/lib/sober_swag/path/lit.rb +0 -41
- data/lib/sober_swag/path/literal.rb +0 -29
- data/lib/sober_swag/path/param.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb2b4cc3cb2635010c4c3c0950e6ce87272046bf2e4cdbf08cf2e299518444b0
|
4
|
+
data.tar.gz: c407225c2c1c5abfadc3e074e5aba98ffb929dc47dda16f5f32293a73669c444
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3dbeeaec1ac6280e3029d779b31490a1661e436e87f833b4e5dce4c3eef96031e18988148aa48ebd1d849fe85353e93c1308f64a013c25d03424ad5b81041c8e
|
7
|
+
data.tar.gz: 51d0d80744d371cc51a2ba5497a9e8942d26a3e96a97625069476b12b612562fff00a0e371441c7851662e2f181a5ff9e33f0d4dcaecb92c233d6a8c4c07f66f
|
@@ -0,0 +1,15 @@
|
|
1
|
+
name: Linters
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v1
|
10
|
+
- name: Rubocop Linter
|
11
|
+
uses: andrewmcodes/rubocop-linter-action@v3.0.0
|
12
|
+
with:
|
13
|
+
action_config_path: '.github/config/rubocop_linter_action.yml' # Note: this is the default location
|
14
|
+
env:
|
15
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/.github/workflows/ruby.yml
CHANGED
@@ -27,7 +27,29 @@ jobs:
|
|
27
27
|
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
|
28
28
|
with:
|
29
29
|
ruby-version: 2.7
|
30
|
+
- uses: actions/cache@v2
|
31
|
+
with:
|
32
|
+
path: vendor/bundle
|
33
|
+
key: ${{ runner.os }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
|
34
|
+
restore-keys: |
|
35
|
+
${{ runner.os }}-gem-deps-
|
30
36
|
- name: Install dependencies
|
31
|
-
run:
|
37
|
+
run: |
|
38
|
+
bundle config path vendor/bundle
|
39
|
+
bundle install
|
32
40
|
- name: Run tests
|
33
41
|
run: bundle exec rake
|
42
|
+
- uses: actions/cache@v2
|
43
|
+
with:
|
44
|
+
path: example/vendor/bundle
|
45
|
+
key: ${{ runner.os }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
46
|
+
restore-keys: |
|
47
|
+
${{ runner.os }}-example-deps-
|
48
|
+
- name: Install example dependencies for example
|
49
|
+
working-directory: example
|
50
|
+
run: |
|
51
|
+
bundle config path vendor/bundle
|
52
|
+
bundle install
|
53
|
+
- name: Run specs for example
|
54
|
+
working-directory: example
|
55
|
+
run: rake
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -3,5 +3,77 @@ Style/FrozenStringLiteralComment:
|
|
3
3
|
Style/BlockDelimiters:
|
4
4
|
EnforcedStyle: braces_for_chaining
|
5
5
|
AllCops:
|
6
|
-
TargetRubyVersion: 2.7.
|
6
|
+
TargetRubyVersion: 2.7.1
|
7
|
+
Exclude:
|
8
|
+
- 'bin/bundle'
|
9
|
+
- 'example/bin/bundle'
|
10
|
+
|
11
|
+
Layout/LineLength:
|
12
|
+
Max: 160
|
7
13
|
require: rubocop-rspec
|
14
|
+
RSpec/NamedSubject:
|
15
|
+
Enabled: false
|
16
|
+
RSpec/DescribeClass:
|
17
|
+
Enabled: false
|
18
|
+
Metrics/BlockLength:
|
19
|
+
Exclude:
|
20
|
+
- 'spec/**/*.rb'
|
21
|
+
- 'sober_swag.gemspec'
|
22
|
+
- 'example/spec/**/*.rb'
|
23
|
+
RSpec/ImplicitBlockExpectation:
|
24
|
+
Enabled: false
|
25
|
+
RSpec/ImplicitExpect:
|
26
|
+
EnforcedStyle: should
|
27
|
+
Style/MultilineBlockChain:
|
28
|
+
Enabled: false
|
29
|
+
Metrics/AbcSize:
|
30
|
+
Enabled: false
|
31
|
+
Style/Documentation:
|
32
|
+
Exclude:
|
33
|
+
- 'example/db/migrate/**/*'
|
34
|
+
Layout/EmptyLinesAroundAttributeAccessor:
|
35
|
+
Enabled: true
|
36
|
+
Layout/SpaceAroundMethodCallOperator:
|
37
|
+
Enabled: true
|
38
|
+
Lint/DeprecatedOpenSSLConstant:
|
39
|
+
Enabled: true
|
40
|
+
Lint/DuplicateElsifCondition:
|
41
|
+
Enabled: true
|
42
|
+
Lint/MixedRegexpCaptureTypes:
|
43
|
+
Enabled: true
|
44
|
+
Lint/RaiseException:
|
45
|
+
Enabled: true
|
46
|
+
Lint/StructNewOverride:
|
47
|
+
Enabled: true
|
48
|
+
Style/AccessorGrouping:
|
49
|
+
Enabled: true
|
50
|
+
Style/ArrayCoercion:
|
51
|
+
Enabled: true
|
52
|
+
Style/BisectedAttrAccessor:
|
53
|
+
Enabled: true
|
54
|
+
Style/CaseLikeIf:
|
55
|
+
Enabled: true
|
56
|
+
Style/ExponentialNotation:
|
57
|
+
Enabled: true
|
58
|
+
Style/HashAsLastArrayItem:
|
59
|
+
Enabled: true
|
60
|
+
Style/HashEachMethods:
|
61
|
+
Enabled: true
|
62
|
+
Style/HashLikeCase:
|
63
|
+
Enabled: true
|
64
|
+
Style/HashTransformKeys:
|
65
|
+
Enabled: true
|
66
|
+
Style/HashTransformValues:
|
67
|
+
Enabled: true
|
68
|
+
Style/RedundantAssignment:
|
69
|
+
Enabled: true
|
70
|
+
Style/RedundantFetchBlock:
|
71
|
+
Enabled: true
|
72
|
+
Style/RedundantFileExtensionInRequire:
|
73
|
+
Enabled: true
|
74
|
+
Style/RedundantRegexpCharacterClass:
|
75
|
+
Enabled: true
|
76
|
+
Style/RedundantRegexpEscape:
|
77
|
+
Enabled: true
|
78
|
+
Style/SlicingWithRange:
|
79
|
+
Enabled: true
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.7.
|
1
|
+
2.7.1
|
data/Gemfile.lock
CHANGED
@@ -9,12 +9,13 @@ PATH
|
|
9
9
|
GEM
|
10
10
|
remote: https://rubygems.org/
|
11
11
|
specs:
|
12
|
-
activesupport (6.0.
|
12
|
+
activesupport (6.0.3.1)
|
13
13
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
14
|
i18n (>= 0.7, < 2)
|
15
15
|
minitest (~> 5.1)
|
16
16
|
tzinfo (~> 1.1)
|
17
|
-
zeitwerk (~> 2.2)
|
17
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
18
|
+
ast (2.4.1)
|
18
19
|
coderay (1.1.2)
|
19
20
|
concurrent-ruby (1.1.6)
|
20
21
|
diff-lcs (1.3)
|
@@ -50,11 +51,17 @@ GEM
|
|
50
51
|
concurrent-ruby (~> 1.0)
|
51
52
|
ice_nine (0.11.2)
|
52
53
|
method_source (0.9.2)
|
53
|
-
minitest (5.14.
|
54
|
+
minitest (5.14.1)
|
55
|
+
parallel (1.19.2)
|
56
|
+
parser (2.7.1.4)
|
57
|
+
ast (~> 2.4.1)
|
54
58
|
pry (0.12.2)
|
55
59
|
coderay (~> 1.1.0)
|
56
60
|
method_source (~> 0.9.0)
|
61
|
+
rainbow (3.0.0)
|
57
62
|
rake (13.0.1)
|
63
|
+
regexp_parser (1.7.1)
|
64
|
+
rexml (3.2.4)
|
58
65
|
rspec (3.9.0)
|
59
66
|
rspec-core (~> 3.9.0)
|
60
67
|
rspec-expectations (~> 3.9.0)
|
@@ -68,13 +75,28 @@ GEM
|
|
68
75
|
diff-lcs (>= 1.2.0, < 2.0)
|
69
76
|
rspec-support (~> 3.9.0)
|
70
77
|
rspec-support (3.9.2)
|
78
|
+
rubocop (0.88.0)
|
79
|
+
parallel (~> 1.10)
|
80
|
+
parser (>= 2.7.1.1)
|
81
|
+
rainbow (>= 2.2.2, < 4.0)
|
82
|
+
regexp_parser (>= 1.7)
|
83
|
+
rexml
|
84
|
+
rubocop-ast (>= 0.1.0, < 1.0)
|
85
|
+
ruby-progressbar (~> 1.7)
|
86
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
87
|
+
rubocop-ast (0.1.0)
|
88
|
+
parser (>= 2.7.0.1)
|
89
|
+
rubocop-rspec (1.42.0)
|
90
|
+
rubocop (>= 0.87.0)
|
91
|
+
ruby-progressbar (1.10.1)
|
71
92
|
simplecov (0.18.5)
|
72
93
|
docile (~> 1.1)
|
73
94
|
simplecov-html (~> 0.11)
|
74
95
|
simplecov-html (0.12.2)
|
75
96
|
thread_safe (0.3.6)
|
76
|
-
tzinfo (1.2.
|
97
|
+
tzinfo (1.2.7)
|
77
98
|
thread_safe (~> 0.1)
|
99
|
+
unicode-display_width (1.7.0)
|
78
100
|
zeitwerk (2.3.0)
|
79
101
|
|
80
102
|
PLATFORMS
|
@@ -85,8 +107,10 @@ DEPENDENCIES
|
|
85
107
|
pry
|
86
108
|
rake (~> 13.0)
|
87
109
|
rspec (~> 3.0)
|
110
|
+
rubocop
|
111
|
+
rubocop-rspec
|
88
112
|
simplecov
|
89
113
|
sober_swag!
|
90
114
|
|
91
115
|
BUNDLED WITH
|
92
|
-
2.1.
|
116
|
+
2.1.4
|
data/README.md
CHANGED
@@ -1,7 +1,116 @@
|
|
1
1
|
# SoberSwag
|
2
2
|
|
3
3
|
![Ruby Test Status](https://github.com/SonderMindOrg/sober_swag/workflows/Ruby/badge.svg?branch=master)
|
4
|
+
![Linters Status](https://github.com/SonderMindOrg/sober_swag/workflows/Linters/badge.svg?branch=master)
|
5
|
+
|
6
|
+
***NOTE: THIS GEM IS HIGHLY EXPERIMENTAL AND PROBABLY SHOULD NOT YET BE USED IN PRODUCTION***.
|
4
7
|
|
5
8
|
SoberSwag is a combination of [Dry-Types](https://dry-rb.org/gems/dry-types/1.2/) and [Swagger](https://swagger.io/) that makes your Rails APIs more awesome.
|
6
9
|
Other tools generate documenation from a DSL.
|
7
10
|
This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids.
|
11
|
+
|
12
|
+
An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
|
13
|
+
|
14
|
+
This gem uses pattern matching, and is thus only compatible with Ruby 2.7 or later.
|
15
|
+
|
16
|
+
## Types for a fully-automated API
|
17
|
+
|
18
|
+
SoberSwag lets you type your API using describe blocks.
|
19
|
+
In any controller that includes `SoberSwag::Controller`, you get access to the super-cool DSL method `define`.
|
20
|
+
This lets you type your API endpoint:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class PeopleController < ApplicationController
|
24
|
+
include SoberSwag::Controller
|
25
|
+
define :patch, :update, '/people/{id}' do
|
26
|
+
query_params do
|
27
|
+
attribute? :include_extra_info, Types::Params::Bool
|
28
|
+
end
|
29
|
+
request_body do
|
30
|
+
attribute? :name, Types::Params::String
|
31
|
+
attribute? :age, Types::Params::Integer
|
32
|
+
end
|
33
|
+
path_params { attribute :id, Types::Params::Integer }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
We can now us this information to generate swagger documentation, available at the `swagger` action on this controller.
|
39
|
+
More than that, we can use this information *inside* our controller methods:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
def update
|
43
|
+
@person = Person.find(parsed_path.id)
|
44
|
+
@person.update!(parsed_body.to_h)
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
No need for `params.require` or anything like that.
|
49
|
+
You define the type of parameters you accept, and we reject anything that doesn't fit.
|
50
|
+
|
51
|
+
### Typed Responses
|
52
|
+
|
53
|
+
Want to go further and type your responses too?
|
54
|
+
Use SoberSwag blueprints, a serializer library heavily inspired by [OutputObjecter](https://github.com/procore/blueprinter)
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
PersonOutputObject = SoberSwag::OutputObject.define do
|
58
|
+
field :id, primitive(:Integer)
|
59
|
+
field :name, primitive(:String).optional
|
60
|
+
field :is_registered, primitive(:Bool) do |person|
|
61
|
+
person.registered?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Now, in your `define` block, you can tell us that this is the *type* of your response:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class PeopleController < ApplicationController
|
70
|
+
include SoberSwag::Controller
|
71
|
+
define :patch, :update, '/people/{id}' do
|
72
|
+
request_body do
|
73
|
+
attribute? :name, Types::Params::String
|
74
|
+
attribute? :age, Types::Params::Integer
|
75
|
+
end
|
76
|
+
path_params { attribute :id, Types::Params::Integer }
|
77
|
+
response(:ok, 'the updated person', PersonOutputObject)
|
78
|
+
end
|
79
|
+
def update
|
80
|
+
person = Person.find(parsed_path.id)
|
81
|
+
if person.update(parsed_body.to_h)
|
82
|
+
respond!(:ok, person)
|
83
|
+
else
|
84
|
+
render json: person.errors
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development.
|
91
|
+
|
92
|
+
### SoberSwag Structs
|
93
|
+
|
94
|
+
Input parameters (including path, query, and request body) are typed using [dry-struct](https://dry-rb.org/gems/dry-struct/1.0/).
|
95
|
+
You don't have to do them inline: you can define them in another file, like so:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
User = SoberSwag.struct do
|
99
|
+
attribute :name, SoberSwag::Types::String
|
100
|
+
# use ? if attributes are not required
|
101
|
+
attribute? :favorite_movie, SoberSwag::Types::String
|
102
|
+
# use .optional if attributes may be null
|
103
|
+
attribute :age, SoberSwag::Types::Params::::Integer.optional
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Under the hood, this literally just generates a subclass of `Dry::Struct`.
|
108
|
+
We use the DSL-like method just to make working with Rails' reloading less annoying.
|
109
|
+
|
110
|
+
## Special Thanks
|
111
|
+
|
112
|
+
This gem is a mismatch of ideas from various sources.
|
113
|
+
The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
|
114
|
+
On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
|
115
|
+
The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/).
|
116
|
+
Generating the swagger documenation 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).
|
data/bin/console
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'sober_swag'
|
6
|
-
require 'dry-struct'
|
7
6
|
|
8
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
9
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,28 +10,30 @@ module Types
|
|
11
10
|
include Dry.Types()
|
12
11
|
end
|
13
12
|
|
14
|
-
|
15
|
-
attribute :name,
|
13
|
+
Bio = SoberSwag.input_object do
|
14
|
+
attribute :name, primitive(:String).meta(description: 'A very basic bio name')
|
16
15
|
end
|
17
16
|
|
18
|
-
|
19
|
-
attribute :name,
|
20
|
-
attribute :age,
|
17
|
+
Person = SoberSwag.input_object do
|
18
|
+
attribute :name, primitive(:String).meta(description: 'The full name description')
|
19
|
+
attribute :age, param(:Integer).constrained(gt: 0).optional.meta(description: 'My cool age')
|
21
20
|
attribute? :mood, Types::String
|
22
|
-
attribute :bio, Bio
|
23
|
-
|
24
|
-
attribute :foo do
|
25
|
-
attribute :bar, Types::String
|
26
|
-
attribute :baz, Types::String.optional
|
27
|
-
end
|
28
21
|
end
|
29
22
|
|
30
|
-
|
23
|
+
##
|
24
|
+
# Demonstration of subclass-style.
|
25
|
+
class PersonSearch < SoberSwag::InputObject
|
31
26
|
attribute? :name, Types::String
|
32
27
|
attribute? :age, Types::Integer
|
33
28
|
end
|
34
29
|
|
30
|
+
##
|
31
|
+
# Demonstration of subclass-style *and* recursion in structs.
|
32
|
+
class LinkedList < SoberSwag::InputObject
|
33
|
+
attribute :value, Types::String
|
34
|
+
attribute? :next, LinkedList
|
35
|
+
end
|
35
36
|
|
36
37
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
37
|
-
require
|
38
|
+
require 'pry'
|
38
39
|
Pry.start
|
data/docs/serializers.md
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# Serializers
|
2
|
+
|
3
|
+
Serializers are a way to transform from one type to another.
|
4
|
+
For example, you might want to change an ActiveRecord object to a JSON struct.
|
5
|
+
You might also want to change an internal date-interval into a two-element array of dates, or some custom text format.
|
6
|
+
You can do all of these things with SoberSwag serializers.
|
7
|
+
Furthermore, Serializers document the *type* that they serialize, so you can use it to degenerate documentation.
|
8
|
+
|
9
|
+
## The Basics
|
10
|
+
|
11
|
+
All serializers are inherted from [`SoberSwag::Serializer::Base`](../lib/sober_swag/serializer/base.rb).
|
12
|
+
This is an abstract class that implements several methods, most of which will be documented later.
|
13
|
+
The two that are most interesting, however, are `#type` and `#serialize`.
|
14
|
+
|
15
|
+
The first, `#type`, returns a SoberSwag-compatible type definition.
|
16
|
+
This might be an instance of `SoberSwag::Struct`, or something else.
|
17
|
+
You'll never need to implement this yourself, but you should note that we generally do not *enforce* these types *at serialization time*.
|
18
|
+
This might change in the future, likely under a debug flag.
|
19
|
+
|
20
|
+
The second, `#serialize`, does the actual work of serialization.
|
21
|
+
It takes *two arguments*.
|
22
|
+
The first is the argument that we will transform into the output type.
|
23
|
+
The second is *always* optional, and is a *hash of options* to use to customize serialization.
|
24
|
+
For example, you might have a serializer that can return a date in two formats, depending on a boolean flag.
|
25
|
+
In this case, it might be used as:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
serializer.new(my_record, { format: :newstyle })
|
29
|
+
```
|
30
|
+
|
31
|
+
However, since it is *always* optional, you can also do:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
serilaizer.new(my_record)
|
35
|
+
```
|
36
|
+
|
37
|
+
And it *should* pick some default format.
|
38
|
+
|
39
|
+
### Primitives
|
40
|
+
|
41
|
+
Primitive serializers, or "identity serializers," are serializers that do nothing.
|
42
|
+
They are implemented as [`SoberSwag::Serializer::Primitive`](../lib/sober_swag/serializer/primitive.rb), or as the `#primitive` method on a `OutputObject`.
|
43
|
+
Since they don't do anything, they can be considered the most "basic" serializer.
|
44
|
+
|
45
|
+
These serializers *do not* check types.
|
46
|
+
That is, the following code will not throw an error:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
serializer = SoberSwag::Serializer::Primitive.new(SoberSwag::Types::String)
|
50
|
+
serializer.serialize(10) # => 10
|
51
|
+
```
|
52
|
+
|
53
|
+
Thus, care should be used when working with these serializers.
|
54
|
+
In the future, we might add some "debug mode" sorta thing that will do type-checking and throw errors, however, the cost of doing so in production is probably not worth it.
|
55
|
+
|
56
|
+
### Mapped
|
57
|
+
|
58
|
+
Sometimes, you can create a serilaizer via a *proc*.
|
59
|
+
For example, let's say that I want a serializer that takes a `Date` and returns a string.
|
60
|
+
I can do this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
date_string = SoberSwag::Serializer.Primitive(:String).via_map { |d| d.to_s }
|
64
|
+
```
|
65
|
+
|
66
|
+
This is implemented via [`SoberSwag::Serializer::Mapped`](../lib/sober_swag/serializer/mapped.rb).
|
67
|
+
Basically, it uses your given proc to do serialization.
|
68
|
+
|
69
|
+
Once again, this does not do type-checking.
|
70
|
+
In the future, we might add a debug mode.
|
71
|
+
|
72
|
+
### Optional
|
73
|
+
|
74
|
+
Oftentimes, we want to give a serializer the ability to serialize `nil` values.
|
75
|
+
This is often useful in serializing fields.
|
76
|
+
|
77
|
+
It turns out that it's pretty easy to make a serializer that can serialize `nil` values: just propogate nils.
|
78
|
+
For example, let's say I have the following code:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Foo = Struct.new(:bar, :baz)
|
82
|
+
my_serializer.serialize(Foo.new(10, 11)) # => { bar: 10, baz: 11 }
|
83
|
+
# ^ my_serializer is defined elsewhere
|
84
|
+
my_serializer.optional.serialize(Foo.new(10, 11)) # => { bar: 10, baz: 11 }
|
85
|
+
# ^ can serialize the type from before
|
86
|
+
my_serializer.optional.serialize(nil) # => nil
|
87
|
+
# ^ nils become nil
|
88
|
+
```
|
89
|
+
|
90
|
+
This properly changes the `type` to be a nillable type, as well.
|
91
|
+
|
92
|
+
### Array
|
93
|
+
|
94
|
+
Oftentimes, if we have a serializer for a single value, we want to serialize an array of values.
|
95
|
+
You can use the `#array` method on a serializer to get that.
|
96
|
+
Continuing our example from earlier:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
my_serializer.array.serialize([Foo.new(10, 11)]) #=> [{ bar: 10, baz: 11 }]
|
100
|
+
```
|
101
|
+
|
102
|
+
This changes the type properly, too.
|
103
|
+
|
104
|
+
## OutputObjects
|
105
|
+
|
106
|
+
98% of the time, when we're writing web APIs, we want to transform our domain objects into JSON objects.
|
107
|
+
We often want different ways to do this, too.
|
108
|
+
Consider, for exmaple, and API for a college.
|
109
|
+
We might want to provide one detailed way to serialize a student, which includes their full name, grade, student ID, GPA, and so on.
|
110
|
+
On another page, we might want to display a classroom with a list of students.
|
111
|
+
However, on the classroom page, we don't want to serialize a full student: that's sending too much data.
|
112
|
+
Instead, we probably want to serialize a "stub" view.
|
113
|
+
|
114
|
+
OutputObjects are the answer to these problems.
|
115
|
+
They're a way to define a serializer for a JSON object, along with a type, and to define "variant" ways to serialize things.
|
116
|
+
|
117
|
+
|
118
|
+
### The Basics
|
119
|
+
|
120
|
+
Let's define an output object:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
StudentOutputObject = SoberSwag::OutputObject.define do
|
124
|
+
field :first_name, Primitive(:String)
|
125
|
+
field :last_name, Primitive(:String)
|
126
|
+
field :recent_grades, Primitive(:Integer).array do |student|
|
127
|
+
student.graded_assignments.limit(100).pluck(:grade)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
We can see a few things here:
|
133
|
+
|
134
|
+
1. You define field names with a `field` definition, which is a way to define the serializer for a single field.
|
135
|
+
2. You must provide types with field names
|
136
|
+
3. You can use blocks to do data formatting, which lets you pick different fields and such.
|
137
|
+
|
138
|
+
### Views
|
139
|
+
|
140
|
+
Sometimes, you might want to add "variant" ways to look at data.
|
141
|
+
We call these "views," based on the output objecter concept.
|
142
|
+
Let's take a look at their use:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
StudentOutputObject = SoberSwag::OutputObject.define do
|
146
|
+
field :first_name, Primitive(:String)
|
147
|
+
field :last_name, Primitive(:String)
|
148
|
+
view :detail do
|
149
|
+
field :recent_grades, Primitive(:Integer).array do |student|
|
150
|
+
student.graded_assignments.limit(100).pluck(:grade)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
StudentOutputObject.serialize(my_student) # => { first_name: 'Rich', last_name: 'Evans' }
|
156
|
+
StudentOutputObject.serialize(
|
157
|
+
my_student,
|
158
|
+
{ view: :detail }
|
159
|
+
) # => { first_name: 'Rich', last_name: 'Evans', recent_grades: [0, 0, 0, 1] }
|
160
|
+
```
|
161
|
+
|
162
|
+
The options hash of the serializer will be used to determine which view to serialize with.
|
163
|
+
Handily, each view is actually *its own* serializer.
|
164
|
+
You can obtain a serializer for a single view very easily:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
StudentOutputObject.view(:detail)
|
168
|
+
```
|
169
|
+
|
170
|
+
If you want an output object without the view-checking behavior, you can use `.base` on an output object.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
StudentOutputObject.base
|
174
|
+
```
|
175
|
+
|
176
|
+
Both of these are great for defining *relationships* between data.
|
177
|
+
|
178
|
+
### Circular OutputObjects
|
179
|
+
|
180
|
+
Sometimes, you might want to include an output object inside another output object, that itself has that output object inside it.
|
181
|
+
Or, less confusingly, you wanna do this:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
StudentOutputObject = SoberSwag::OutputObject.define do
|
185
|
+
# some other fields
|
186
|
+
view :detail do
|
187
|
+
field :classes, ClassOutputObject.array
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
This can cause a circular dependecy.
|
193
|
+
To break this, you can use a lambda:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
StudentOutputObject = SoberSwag::OutputObject.define do
|
197
|
+
view :detail do
|
198
|
+
field :classes, -> { ClassOutputObject.view(:base).array }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
For clarity (and to prevent infinitely-looping serializers on accident, we reccomend you *always* use an explicit view for dependent output objects.
|