sober_swag 0.15.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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).