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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +5 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +23 -1
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +73 -1
  7. data/.ruby-version +1 -1
  8. data/Gemfile.lock +29 -5
  9. data/README.md +109 -0
  10. data/bin/console +15 -14
  11. data/docs/serializers.md +203 -0
  12. data/example/.rspec +1 -0
  13. data/example/.ruby-version +1 -1
  14. data/example/Gemfile +10 -6
  15. data/example/Gemfile.lock +96 -76
  16. data/example/app/controllers/people_controller.rb +37 -21
  17. data/example/app/controllers/posts_controller.rb +102 -0
  18. data/example/app/models/application_record.rb +3 -0
  19. data/example/app/models/person.rb +6 -0
  20. data/example/app/models/post.rb +9 -0
  21. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  22. data/example/app/output_objects/person_output_object.rb +15 -0
  23. data/example/app/output_objects/post_output_object.rb +10 -0
  24. data/example/bin/bundle +24 -20
  25. data/example/bin/rails +1 -1
  26. data/example/bin/rake +1 -1
  27. data/example/config/application.rb +11 -7
  28. data/example/config/environments/development.rb +0 -1
  29. data/example/config/environments/production.rb +3 -3
  30. data/example/config/puma.rb +5 -5
  31. data/example/config/routes.rb +3 -0
  32. data/example/config/spring.rb +4 -4
  33. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  34. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  35. data/example/db/schema.rb +16 -7
  36. data/example/spec/rails_helper.rb +64 -0
  37. data/example/spec/requests/people/create_spec.rb +52 -0
  38. data/example/spec/requests/people/get_spec.rb +35 -0
  39. data/example/spec/requests/people/index_spec.rb +69 -0
  40. data/example/spec/spec_helper.rb +94 -0
  41. data/lib/sober_swag.rb +6 -3
  42. data/lib/sober_swag/compiler/error.rb +2 -0
  43. data/lib/sober_swag/compiler/path.rb +2 -5
  44. data/lib/sober_swag/compiler/paths.rb +0 -1
  45. data/lib/sober_swag/compiler/type.rb +28 -15
  46. data/lib/sober_swag/controller.rb +16 -11
  47. data/lib/sober_swag/controller/route.rb +18 -21
  48. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  50. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  51. data/lib/sober_swag/input_object.rb +28 -0
  52. data/lib/sober_swag/nodes/array.rb +1 -1
  53. data/lib/sober_swag/nodes/base.rb +2 -4
  54. data/lib/sober_swag/nodes/binary.rb +2 -1
  55. data/lib/sober_swag/nodes/enum.rb +4 -2
  56. data/lib/sober_swag/nodes/list.rb +0 -1
  57. data/lib/sober_swag/nodes/primitive.rb +6 -5
  58. data/lib/sober_swag/output_object.rb +102 -0
  59. data/lib/sober_swag/output_object/definition.rb +30 -0
  60. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  61. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
  62. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  63. data/lib/sober_swag/parser.rb +5 -3
  64. data/lib/sober_swag/serializer.rb +5 -2
  65. data/lib/sober_swag/serializer/array.rb +12 -0
  66. data/lib/sober_swag/serializer/base.rb +50 -1
  67. data/lib/sober_swag/serializer/conditional.rb +15 -2
  68. data/lib/sober_swag/serializer/field_list.rb +29 -6
  69. data/lib/sober_swag/serializer/mapped.rb +12 -2
  70. data/lib/sober_swag/serializer/meta.rb +35 -0
  71. data/lib/sober_swag/serializer/optional.rb +17 -2
  72. data/lib/sober_swag/serializer/primitive.rb +4 -1
  73. data/lib/sober_swag/server.rb +83 -0
  74. data/lib/sober_swag/types.rb +3 -0
  75. data/lib/sober_swag/version.rb +1 -1
  76. data/sober_swag.gemspec +6 -4
  77. metadata +77 -44
  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: bb2b4cc3cb2635010c4c3c0950e6ce87272046bf2e4cdbf08cf2e299518444b0
4
+ data.tar.gz: c407225c2c1c5abfadc3e074e5aba98ffb929dc47dda16f5f32293a73669c444
5
5
  SHA512:
6
- metadata.gz: 747f4d9f9c0750de1a8109fb443bbe21df72929f74f447ce1473d676b2dcbc952c52dcc7731c6cd097d6f06452220ee808b883d4529da458b89baa2dde84ee76
7
- data.tar.gz: 839284d0a57c363348e486320256c3f4d097b4c193eab3914416037a178a4a32e7b07e236e72b77376a68fce9715ffe62898a2b8ef7659fb9f2ccbf3a7b01378
6
+ metadata.gz: 3dbeeaec1ac6280e3029d779b31490a1661e436e87f833b4e5dce4c3eef96031e18988148aa48ebd1d849fe85353e93c1308f64a013c25d03424ad5b81041c8e
7
+ data.tar.gz: 51d0d80744d371cc51a2ba5497a9e8942d26a3e96a97625069476b12b612562fff00a0e371441c7851662e2f181a5ff9e33f0d4dcaecb92c233d6a8c4c07f66f
@@ -0,0 +1,5 @@
1
+ check_name: 'Rubocop Lint'
2
+ versions:
3
+ rubocop: 'latest'
4
+ rubocop-rspec: 'latest'
5
+
@@ -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 }}
@@ -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: bundle install
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
@@ -9,3 +9,6 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ .ruby-version
13
+
14
+ *.gem
@@ -3,5 +3,77 @@ Style/FrozenStringLiteralComment:
3
3
  Style/BlockDelimiters:
4
4
  EnforcedStyle: braces_for_chaining
5
5
  AllCops:
6
- TargetRubyVersion: 2.7.0
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
@@ -1 +1 @@
1
- 2.7.0
1
+ 2.7.1
@@ -9,12 +9,13 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (6.0.2.1)
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.0)
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.6)
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.2
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).
@@ -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
- 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 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.