sober_swag 0.15.0 → 0.20.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/lint.yml +4 -9
  4. data/.github/workflows/ruby.yml +2 -6
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +50 -5
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +29 -1
  9. data/Gemfile +8 -0
  10. data/README.md +155 -4
  11. data/bin/rspec +29 -0
  12. data/docs/serializers.md +18 -13
  13. data/example/Gemfile +2 -2
  14. data/example/app/controllers/people_controller.rb +4 -0
  15. data/example/app/controllers/posts_controller.rb +5 -0
  16. data/example/config/environments/production.rb +1 -1
  17. data/lib/sober_swag.rb +6 -1
  18. data/lib/sober_swag/compiler.rb +29 -3
  19. data/lib/sober_swag/compiler/path.rb +49 -3
  20. data/lib/sober_swag/compiler/paths.rb +20 -0
  21. data/lib/sober_swag/compiler/primitive.rb +20 -1
  22. data/lib/sober_swag/compiler/type.rb +105 -22
  23. data/lib/sober_swag/controller.rb +42 -15
  24. data/lib/sober_swag/controller/route.rb +133 -28
  25. data/lib/sober_swag/input_object.rb +117 -7
  26. data/lib/sober_swag/nodes/array.rb +19 -0
  27. data/lib/sober_swag/nodes/attribute.rb +45 -4
  28. data/lib/sober_swag/nodes/base.rb +27 -7
  29. data/lib/sober_swag/nodes/binary.rb +30 -13
  30. data/lib/sober_swag/nodes/enum.rb +16 -1
  31. data/lib/sober_swag/nodes/list.rb +20 -0
  32. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  33. data/lib/sober_swag/nodes/object.rb +4 -1
  34. data/lib/sober_swag/nodes/one_of.rb +11 -3
  35. data/lib/sober_swag/nodes/primitive.rb +34 -2
  36. data/lib/sober_swag/nodes/sum.rb +8 -0
  37. data/lib/sober_swag/output_object.rb +35 -4
  38. data/lib/sober_swag/output_object/definition.rb +31 -1
  39. data/lib/sober_swag/output_object/field.rb +31 -11
  40. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  41. data/lib/sober_swag/output_object/view.rb +46 -1
  42. data/lib/sober_swag/parser.rb +7 -1
  43. data/lib/sober_swag/serializer/array.rb +27 -3
  44. data/lib/sober_swag/serializer/base.rb +75 -25
  45. data/lib/sober_swag/serializer/conditional.rb +33 -1
  46. data/lib/sober_swag/serializer/field_list.rb +18 -2
  47. data/lib/sober_swag/serializer/mapped.rb +10 -1
  48. data/lib/sober_swag/serializer/optional.rb +18 -1
  49. data/lib/sober_swag/serializer/primitive.rb +3 -0
  50. data/lib/sober_swag/server.rb +27 -11
  51. data/lib/sober_swag/type/named.rb +14 -0
  52. data/lib/sober_swag/types/comma_array.rb +4 -0
  53. data/lib/sober_swag/version.rb +1 -1
  54. data/sober_swag.gemspec +2 -2
  55. metadata +13 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff24aa407a6b8360569931c050bee2958cb6e3169f34f57f1d455f0d573327af
4
- data.tar.gz: 61bf1feb701ae67e2a2fd85f41ab2d8ccd25d5e512a4cd26e3849f65e78a14e9
3
+ metadata.gz: 97cb4bbfd84f28f6f79368c3f78182835217868a1d3142c38107ab3b03552952
4
+ data.tar.gz: 1cd8446250a8ddadc16eb114a773b9d317c2182e2bcb8c692c59681331ef972b
5
5
  SHA512:
6
- metadata.gz: 46c72426dcabb170bf8c5416e8583c254a9c627f8a6cf48424145dc688c9abcb2f7c6e61c87c55da0b8f66f9d076254cce3f815616c9f03eaa88aa14b19c72e4
7
- data.tar.gz: a13ac7c92fdaec126d93a3400e13f93287c993bd19c82caa76cc662112e82983a64de5233adfce1cd30a32bfdca6ecc794b5f06a15f7cc2ffda0bbd155b1d0a5
6
+ metadata.gz: f6b055ea451db16f12a02ebe2bc6da459aefcf13605a54f4f0b0cff8aa694b73e2c4cb2c2f759a89c859a820a74a37391728998e0939a770cda2783c2190586e
7
+ data.tar.gz: 3a49c153297750447c2776b086da135b81c80a1b628780d9a7ab6f68f204ddeaa3b21e08ffd02f8a49e92f1a7ac5942b0e42d295307299e8623ebb6a765731dd
@@ -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"
@@ -19,17 +19,14 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby: [ '2.6', '2.7' ]
22
+ ruby: [ '2.6', '2.7', '3.0' ]
23
23
 
24
24
  steps:
25
25
  - uses: actions/checkout@v2
26
26
  - 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
27
+ uses: ruby/setup-ruby@v1
31
28
  with:
32
- ruby-version: ${{ matrix.ruby }}
29
+ ruby-version: ${{ matrix.ruby }}
33
30
  - uses: actions/cache@v2
34
31
  with:
35
32
  path: vendor/bundle
@@ -40,10 +37,8 @@ jobs:
40
37
  run: |
41
38
  bundle config path vendor/bundle
42
39
  bundle install
43
- gem install rubocop
44
- gem install rubocop-rspec
45
40
  - name: Run Lints
46
- run: rubocop lib spec example
41
+ run: bundle exec rubocop -P
47
42
  - uses: actions/cache@v2
48
43
  with:
49
44
  path: example/vendor/bundle
@@ -19,15 +19,11 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby: [ '2.6', '2.7' ]
23
-
22
+ ruby: [ '2.6', '2.7', '3.0' ]
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,7 @@
13
13
  Gemfile.lock
14
14
 
15
15
  *.gem
16
+
17
+ /vendor/
18
+
19
+ .yardoc
data/.rubocop.yml CHANGED
@@ -1,85 +1,130 @@
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
5
+ NewCops: enable
7
6
  Exclude:
8
7
  - 'bin/bundle'
9
8
  - 'example/bin/bundle'
9
+ - 'vendor/bundle/**/*'
10
10
 
11
11
  Layout/LineLength:
12
12
  Max: 160
13
- require: rubocop-rspec
13
+
14
14
  RSpec/NamedSubject:
15
15
  Enabled: false
16
+
16
17
  RSpec/DescribeClass:
17
18
  Enabled: false
19
+
18
20
  Metrics/BlockLength:
19
21
  Exclude:
20
22
  - 'spec/**/*.rb'
21
23
  - 'sober_swag.gemspec'
22
24
  - 'example/spec/**/*.rb'
25
+
26
+ Lint/MissingSuper:
27
+ Enabled: false
28
+
23
29
  RSpec/ImplicitBlockExpectation:
24
30
  Enabled: false
31
+
25
32
  RSpec/ImplicitExpect:
26
33
  EnforcedStyle: should
34
+
27
35
  RSpec/LeadingSubject:
28
36
  Enabled: false
37
+
29
38
  Style/MultilineBlockChain:
30
39
  Enabled: false
40
+
31
41
  Metrics/AbcSize:
32
42
  Enabled: false
43
+
33
44
  Style/Documentation:
34
45
  Exclude:
35
46
  - 'example/db/migrate/**/*'
47
+
36
48
  Metrics/PerceivedComplexity:
37
49
  Enabled: false
50
+
38
51
  Layout/EmptyLinesAroundAttributeAccessor:
39
52
  Enabled: true
53
+
40
54
  Layout/SpaceAroundMethodCallOperator:
41
55
  Enabled: true
56
+
42
57
  Lint/DeprecatedOpenSSLConstant:
43
58
  Enabled: true
59
+
44
60
  Lint/DuplicateElsifCondition:
45
61
  Enabled: true
62
+
46
63
  Lint/MixedRegexpCaptureTypes:
47
64
  Enabled: true
65
+
48
66
  Lint/RaiseException:
49
67
  Enabled: true
68
+
50
69
  Lint/StructNewOverride:
51
70
  Enabled: true
71
+
52
72
  Style/AccessorGrouping:
53
73
  Enabled: true
74
+
54
75
  Style/ArrayCoercion:
55
76
  Enabled: true
77
+
56
78
  Style/BisectedAttrAccessor:
57
79
  Enabled: true
80
+
58
81
  Style/CaseLikeIf:
59
82
  Enabled: true
83
+
60
84
  Style/ExponentialNotation:
61
85
  Enabled: true
86
+
62
87
  Style/HashAsLastArrayItem:
63
88
  Enabled: true
89
+
64
90
  Style/HashEachMethods:
65
91
  Enabled: true
92
+
66
93
  Style/HashLikeCase:
67
94
  Enabled: true
95
+
68
96
  Style/HashTransformKeys:
69
97
  Enabled: true
98
+
70
99
  Style/HashTransformValues:
71
100
  Enabled: true
101
+
72
102
  Style/RedundantAssignment:
73
103
  Enabled: true
104
+
74
105
  Style/RedundantFetchBlock:
75
106
  Enabled: true
107
+
76
108
  Style/RedundantFileExtensionInRequire:
77
109
  Enabled: true
110
+
78
111
  Style/RedundantRegexpCharacterClass:
79
112
  Enabled: true
113
+
80
114
  Style/RedundantRegexpEscape:
81
115
  Enabled: true
116
+
82
117
  Style/SlicingWithRange:
83
118
  Enabled: true
119
+
84
120
  RSpec/NestedGroups:
85
121
  Max: 5
122
+
123
+ RSpec/ExampleLength:
124
+ Max: 10
125
+
126
+ Style/FrozenStringLiteralComment:
127
+ Enabled: false
128
+
129
+ Style/BlockDelimiters:
130
+ EnforcedStyle: braces_for_chaining
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
3
+ --plugin activesupport-concern
4
+ --plugin solargraph
5
+ --private
6
+ --protected
7
+ --files docs/serializers.md
data/CHANGELOG.md CHANGED
@@ -1,7 +1,30 @@
1
1
  # Changelog
2
2
 
3
- ## V0.15.0: 2020-09-02
3
+ ## [v0.20.0] 2021-05-17
4
4
 
5
+ - Added YARD documentation to almost every method
6
+ - Added `except` parameter to the `merge` method, which allows a specified field to be excluded from the merge.
7
+
8
+ ## [v0.19.0] 2021-03-10
9
+
10
+ - Use [redoc](https://github.com/Redocly/redoc) for generated documentation UI
11
+
12
+ ## [v0.18.0] 2021-03-02
13
+
14
+ - Add generic hash type for primitive types
15
+
16
+ ## [v0.17.0]: 2020-11-30
17
+
18
+ - Allow tagging endpoints via the new `tags` method.
19
+
20
+ ## [v0.16.0]: 2020-10-23
21
+
22
+ - Allow non-class types to be used as the inputs to controllers
23
+
24
+ ## [v0.15.0]: 2020-09-02
25
+
26
+ ### Added
27
+ - Add a new `#merge` method to output objects, which will merge fields from another output object into the given output object.
5
28
  - Add `multi` to Output Objects, as a way to define more than one field of the same type at once.
6
29
  - Add an `inherits:` key to output objects, for view inheritance.
7
30
  - Add `SoberSwag::Types::CommaArray`, which parses comma-separated strings into arrays.
@@ -9,3 +32,8 @@
9
32
  This class is mostly useful for query parameters where you want a simpler format: `tag=foo,bar` instead of `tag[]=foo,tag[]=bar`.
10
33
  - Add support for using `meta` to specify alternative `style` and `explode` keys for query and path params.
11
34
  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.
35
+
36
+ ### Fixed
37
+ - No longer swallow `Dry::Struct` errors, instead let them surface to the user.
38
+
39
+ [v0.15.0]: https://github.com/SonderMindOrg/sober_swag/releases/tag/v0.15.0
data/Gemfile CHANGED
@@ -4,3 +4,11 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in sober_swag.gemspec
6
6
  gemspec
7
+
8
+ gem 'yard'
9
+
10
+ gem 'redcarpet'
11
+
12
+ gem 'yard-activesupport-concern'
13
+
14
+ gem 'solargraph'
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
+ - {file:docs/serializers.md Serializers}
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).