sober_swag 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|

|
4
|
+

|
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.
|