sober_swag 0.1.0 → 0.6.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +33 -2
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +75 -1
  7. data/.ruby-version +1 -1
  8. data/README.md +154 -1
  9. data/bin/console +16 -15
  10. data/docs/serializers.md +203 -0
  11. data/example/.rspec +1 -0
  12. data/example/.ruby-version +1 -1
  13. data/example/Gemfile +9 -7
  14. data/example/Gemfile.lock +96 -79
  15. data/example/app/controllers/people_controller.rb +41 -23
  16. data/example/app/controllers/posts_controller.rb +110 -0
  17. data/example/app/models/application_record.rb +3 -0
  18. data/example/app/models/person.rb +6 -0
  19. data/example/app/models/post.rb +9 -0
  20. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  21. data/example/app/output_objects/person_output_object.rb +15 -0
  22. data/example/app/output_objects/post_output_object.rb +10 -0
  23. data/example/bin/bundle +24 -20
  24. data/example/bin/rails +1 -1
  25. data/example/bin/rake +1 -1
  26. data/example/config/application.rb +11 -7
  27. data/example/config/environments/development.rb +0 -1
  28. data/example/config/environments/production.rb +3 -3
  29. data/example/config/puma.rb +5 -5
  30. data/example/config/routes.rb +3 -0
  31. data/example/config/spring.rb +4 -4
  32. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  33. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  34. data/example/db/schema.rb +16 -7
  35. data/example/spec/rails_helper.rb +64 -0
  36. data/example/spec/requests/people/create_spec.rb +52 -0
  37. data/example/spec/requests/people/get_spec.rb +35 -0
  38. data/example/spec/requests/people/index_spec.rb +69 -0
  39. data/example/spec/spec_helper.rb +94 -0
  40. data/lib/sober_swag.rb +6 -3
  41. data/lib/sober_swag/compiler/error.rb +2 -0
  42. data/lib/sober_swag/compiler/path.rb +2 -5
  43. data/lib/sober_swag/compiler/paths.rb +0 -1
  44. data/lib/sober_swag/compiler/type.rb +86 -56
  45. data/lib/sober_swag/controller.rb +16 -11
  46. data/lib/sober_swag/controller/route.rb +18 -21
  47. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  48. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  50. data/lib/sober_swag/input_object.rb +28 -0
  51. data/lib/sober_swag/nodes/array.rb +1 -1
  52. data/lib/sober_swag/nodes/base.rb +5 -3
  53. data/lib/sober_swag/nodes/binary.rb +2 -1
  54. data/lib/sober_swag/nodes/enum.rb +4 -2
  55. data/lib/sober_swag/nodes/list.rb +0 -1
  56. data/lib/sober_swag/nodes/primitive.rb +6 -5
  57. data/lib/sober_swag/output_object.rb +102 -0
  58. data/lib/sober_swag/output_object/definition.rb +30 -0
  59. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  60. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +2 -2
  61. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  62. data/lib/sober_swag/parser.rb +9 -4
  63. data/lib/sober_swag/serializer.rb +5 -2
  64. data/lib/sober_swag/serializer/array.rb +12 -0
  65. data/lib/sober_swag/serializer/base.rb +50 -1
  66. data/lib/sober_swag/serializer/conditional.rb +19 -2
  67. data/lib/sober_swag/serializer/field_list.rb +29 -6
  68. data/lib/sober_swag/serializer/mapped.rb +15 -3
  69. data/lib/sober_swag/serializer/meta.rb +35 -0
  70. data/lib/sober_swag/serializer/optional.rb +17 -2
  71. data/lib/sober_swag/serializer/primitive.rb +4 -1
  72. data/lib/sober_swag/server.rb +83 -0
  73. data/lib/sober_swag/types.rb +3 -0
  74. data/lib/sober_swag/version.rb +1 -1
  75. data/sober_swag.gemspec +8 -4
  76. metadata +79 -47
  77. data/Gemfile.lock +0 -92
  78. data/example/person.json +0 -4
  79. data/example/test/controllers/.keep +0 -0
  80. data/example/test/fixtures/.keep +0 -0
  81. data/example/test/fixtures/files/.keep +0 -0
  82. data/example/test/fixtures/people.yml +0 -11
  83. data/example/test/integration/.keep +0 -0
  84. data/example/test/models/.keep +0 -0
  85. data/example/test/models/person_test.rb +0 -7
  86. data/example/test/test_helper.rb +0 -13
  87. data/lib/sober_swag/blueprint.rb +0 -113
  88. data/lib/sober_swag/path.rb +0 -8
  89. data/lib/sober_swag/path/integer.rb +0 -21
  90. data/lib/sober_swag/path/lit.rb +0 -41
  91. data/lib/sober_swag/path/literal.rb +0 -29
  92. data/lib/sober_swag/path/param.rb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e58f142c7c7e5716d0d023844846ffcac306ff45f6ab84650cdf33003898ff83
4
- data.tar.gz: 33e35fb1910ebe36f1618b2091e0cfb7762dbadbebaba046c8a67d3f58038e24
3
+ metadata.gz: cdf3896754f6acf3bef2df51e947439be3822d3635eb924d6029ab3157b13bd9
4
+ data.tar.gz: 75846c0b78e4e8dc9fc550af50aeb2bb73f3c1200e8c50af6c07092d2dd8c277
5
5
  SHA512:
6
- metadata.gz: 747f4d9f9c0750de1a8109fb443bbe21df72929f74f447ce1473d676b2dcbc952c52dcc7731c6cd097d6f06452220ee808b883d4529da458b89baa2dde84ee76
7
- data.tar.gz: 839284d0a57c363348e486320256c3f4d097b4c193eab3914416037a178a4a32e7b07e236e72b77376a68fce9715ffe62898a2b8ef7659fb9f2ccbf3a7b01378
6
+ metadata.gz: ae7076a9dcabcf2a99b642f967782195d1680ff13b6fd251123abdec15ec2eab18435ec1897fe212a060779b1bdffb8061bcb4b2eb89884713230ca0c70ecc27
7
+ data.tar.gz: 5f3fb9ee3c31feb8ede072171e5f0f2f5108061d7f09c44e4f4d88d929c001519734fce64b4c261ea4d8e63d9aa1862fdf374cdddd00da2b58d9cf75fc367320
@@ -0,0 +1,4 @@
1
+ check_name: 'Rubocop Lint'
2
+ versions:
3
+ rubocop: 'latest'
4
+ rubocop-rspec: 'latest'
@@ -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 }}
@@ -17,6 +17,9 @@ jobs:
17
17
  test:
18
18
 
19
19
  runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby: [ '2.6', '2.7' ]
20
23
 
21
24
  steps:
22
25
  - uses: actions/checkout@v2
@@ -26,8 +29,36 @@ jobs:
26
29
  # uses: ruby/setup-ruby@v1
27
30
  uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
28
31
  with:
29
- ruby-version: 2.7
32
+ ruby-version: ${{ matrix.ruby }}
33
+ - uses: actions/cache@v2
34
+ with:
35
+ path: vendor/bundle
36
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
37
+ restore-keys: |
38
+ ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
30
39
  - name: Install dependencies
31
- run: bundle install
40
+ run: |
41
+ bundle config path vendor/bundle
42
+ bundle install
32
43
  - name: Run tests
44
+ run: COVERAGE=1 bundle exec rake
45
+ - name: Upload Coverage
46
+ uses: actions/upload-artifact@master
47
+ if: always()
48
+ with:
49
+ name: coverage-report
50
+ path: coverage
51
+ - uses: actions/cache@v2
52
+ with:
53
+ path: example/vendor/bundle
54
+ key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
55
+ restore-keys: |
56
+ ${{ runner.os }}-${{ matrix.ruby }}-example-deps-
57
+ - name: Install example dependencies for example
58
+ working-directory: example
59
+ run: |
60
+ bundle config path vendor/bundle
61
+ bundle install
62
+ - name: Run specs for example
63
+ working-directory: example
33
64
  run: bundle exec rake
data/.gitignore CHANGED
@@ -9,3 +9,7 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ .ruby-version
13
+ Gemfile.lock
14
+
15
+ *.gem
@@ -3,5 +3,79 @@ Style/FrozenStringLiteralComment:
3
3
  Style/BlockDelimiters:
4
4
  EnforcedStyle: braces_for_chaining
5
5
  AllCops:
6
- TargetRubyVersion: 2.7.0
6
+ TargetRubyVersion: 2.6.0
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
+ Metrics/PerceivedComplexity:
35
+ Enabled: false
36
+ Layout/EmptyLinesAroundAttributeAccessor:
37
+ Enabled: true
38
+ Layout/SpaceAroundMethodCallOperator:
39
+ Enabled: true
40
+ Lint/DeprecatedOpenSSLConstant:
41
+ Enabled: true
42
+ Lint/DuplicateElsifCondition:
43
+ Enabled: true
44
+ Lint/MixedRegexpCaptureTypes:
45
+ Enabled: true
46
+ Lint/RaiseException:
47
+ Enabled: true
48
+ Lint/StructNewOverride:
49
+ Enabled: true
50
+ Style/AccessorGrouping:
51
+ Enabled: true
52
+ Style/ArrayCoercion:
53
+ Enabled: true
54
+ Style/BisectedAttrAccessor:
55
+ Enabled: true
56
+ Style/CaseLikeIf:
57
+ Enabled: true
58
+ Style/ExponentialNotation:
59
+ Enabled: true
60
+ Style/HashAsLastArrayItem:
61
+ Enabled: true
62
+ Style/HashEachMethods:
63
+ Enabled: true
64
+ Style/HashLikeCase:
65
+ Enabled: true
66
+ Style/HashTransformKeys:
67
+ Enabled: true
68
+ Style/HashTransformValues:
69
+ Enabled: true
70
+ Style/RedundantAssignment:
71
+ Enabled: true
72
+ Style/RedundantFetchBlock:
73
+ Enabled: true
74
+ Style/RedundantFileExtensionInRequire:
75
+ Enabled: true
76
+ Style/RedundantRegexpCharacterClass:
77
+ Enabled: true
78
+ Style/RedundantRegexpEscape:
79
+ Enabled: true
80
+ Style/SlicingWithRange:
81
+ Enabled: true
@@ -1 +1 @@
1
- 2.7.0
1
+ 2.6.5
data/README.md CHANGED
@@ -1,7 +1,160 @@
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)
4
5
 
5
6
  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
- Other tools generate documenation from a DSL.
7
+ Other tools generate documentation from a DSL.
7
8
  This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids.
9
+
10
+ An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
11
+
12
+ ## Types for a fully-automated API
13
+
14
+ SoberSwag lets you type your API using describe blocks.
15
+ In any controller that includes `SoberSwag::Controller`, you get access to the super-cool DSL method `define`.
16
+ This lets you type your API endpoint:
17
+
18
+ ```ruby
19
+ class PeopleController < ApplicationController
20
+ include SoberSwag::Controller
21
+ define :patch, :update, '/people/{id}' do
22
+ query_params do
23
+ attribute? :include_extra_info, Types::Params::Bool
24
+ end
25
+ request_body do
26
+ attribute? :name, Types::Params::String
27
+ attribute? :age, Types::Params::Integer
28
+ end
29
+ path_params { attribute :id, Types::Params::Integer }
30
+ end
31
+ end
32
+ ```
33
+
34
+ We can now use this information to generate swagger documentation, available at the `swagger` action on this controller.
35
+ More than that, we can use this information *inside* our controller methods:
36
+
37
+ ```ruby
38
+ def update
39
+ @person = Person.find(parsed_path.id)
40
+ @person.update!(parsed_body.to_h)
41
+ end
42
+ ```
43
+
44
+ No need for `params.require` or anything like that.
45
+ You define the type of parameters you accept, and we reject anything that doesn't fit.
46
+
47
+ ### Typed Responses
48
+
49
+ Want to go further and type your responses too?
50
+ Use SoberSwag output objects, a serializer library heavily inspired by [Blueprinter](https://github.com/procore/blueprinter)
51
+
52
+ ```ruby
53
+ PersonOutputObject = SoberSwag::OutputObject.define do
54
+ field :id, primitive(:Integer)
55
+ field :name, primitive(:String).optional
56
+ field :is_registered, primitive(:Bool) do |person|
57
+ person.registered?
58
+ end
59
+ end
60
+ ```
61
+
62
+ Now, in your `define` block, you can tell us that this is the *type* of your response:
63
+
64
+ ```ruby
65
+ class PeopleController < ApplicationController
66
+ include SoberSwag::Controller
67
+ define :patch, :update, '/people/{id}' do
68
+ request_body do
69
+ attribute? :name, Types::Params::String
70
+ attribute? :age, Types::Params::Integer
71
+ end
72
+ path_params { attribute :id, Types::Params::Integer }
73
+ response(:ok, 'the updated person', PersonOutputObject)
74
+ end
75
+ def update
76
+ person = Person.find(parsed_path.id)
77
+ if person.update(parsed_body.to_h)
78
+ respond!(:ok, person)
79
+ else
80
+ render json: person.errors
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development.
87
+
88
+ ### SoberSwag Input Objects
89
+
90
+ Input parameters (including path, query, and request body) are typed using [dry-struct](https://dry-rb.org/gems/dry-struct/1.0/).
91
+ You don't have to do them inline. You can define them in another file, like so:
92
+
93
+ ```ruby
94
+ User = SoberSwag.input_object do
95
+ attribute :name, SoberSwag::Types::String
96
+ # use ? if attributes are not required
97
+ attribute? :favorite_movie, SoberSwag::Types::String
98
+ # use .optional if attributes may be null
99
+ attribute :age, SoberSwag::Types::Params::Integer.optional
100
+ end
101
+ ```
102
+
103
+ Then, in your controller, just do:
104
+
105
+ ```ruby
106
+ class PeopleController < ApplicationController
107
+ include SoberSwag::Controller
108
+
109
+ define :path, :update, '/people/{id}' do
110
+ request_body(User)
111
+ path_params { attribute :id, Types::Params::Integer }
112
+ response(:ok, 'the updated person', PersonOutputObject)
113
+ end
114
+ def update
115
+ # same as above!
116
+ end
117
+ end
118
+ ```
119
+
120
+ Under the hood, this literally just generates a subclass of `Dry::Struct`.
121
+ We use the DSL-like method just to make working with Rails' reloading less annoying.
122
+
123
+ #### Adding additional documentation
124
+
125
+ You can use the `.meta` attribute on a type to add additional documentation.
126
+ Some keys are considered "well-known" and will be present on the swagger output.
127
+ For example:
128
+
129
+
130
+ ```ruby
131
+ User = SoberSwag.input_object do
132
+ attribute? :name, SoberSwag::Types::String.meta(description: <<~MARKDOWN, deprecated: true)
133
+ The given name of the students, with strings encoded as escaped-ASCII.
134
+ This is used by an internal Cobol microservice from 1968.
135
+ Please use unicode_name instead unless you are that microservice.
136
+ MARKDOWN
137
+ attribute? :unicode_name, SoberSwag::Types::String
138
+ end
139
+ ```
140
+
141
+ This will output the swagger you expect, with a description and a deprecated flag.
142
+
143
+ #### Adding Default Values
144
+
145
+ Sometimes it makes sense to specify a default value.
146
+ Don't worry, we've got you covered:
147
+
148
+ ```ruby
149
+ QueryInput = SoberSwag.input_object do
150
+ attribute :allow_first, SoberSwag::Types::Params::Bool.default(false) # smartly alters type-definition to establish that passing this is not required.
151
+ end
152
+ ```
153
+
154
+ ## Special Thanks
155
+
156
+ This gem is a mishmash of ideas from various sources.
157
+ The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
158
+ On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
159
+ The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/).
160
+ 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).
@@ -1,9 +1,8 @@
1
- #!/usr/bin/env ruby -W:no-experimental
1
+ #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
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
- class Bio < Dry::Struct
15
- attribute :name, Types::String
13
+ Bio = SoberSwag.input_object do
14
+ attribute :name, primitive(:String).meta(description: 'A very basic bio name')
16
15
  end
17
16
 
18
- class Person < Dry::Struct
19
- attribute :name, Types::String
20
- attribute :age, Types::Integer.constrained(gt: 0).optional | Types::String.optional
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
- class PersonSearch < Dry::Struct
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 "pry"
38
+ require 'pry'
38
39
  Pry.start
@@ -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 generate documentation.
8
+
9
+ ## The Basics
10
+
11
+ All serializers are inherited 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 serializer 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 nilable 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 dependency.
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 recommend you *always* use an explicit view for dependent output objects.