sober_swag 0.14.0 → 0.19.0

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