sober_swag 0.11.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c07baa6165df700b23c0f8a0c9bd1a1dcb841d79b07995479b0c1e19a69928d7
4
- data.tar.gz: 523a0eabeb954497f531485962b0f058dc9a32ec398df899162c66e5d985d01e
3
+ metadata.gz: ad86b8f4701d16ec53f0fc998bb2a881cdc448acfbb509bbfeb7821cc7142d1b
4
+ data.tar.gz: ce70ffa29ca02b8fc70e7f78c0b41bf56e8e3cdffbcf7282d8dad1cf16717b23
5
5
  SHA512:
6
- metadata.gz: 0ccf5c1f96d1647e42e082bfdd05c0a0ba3ff9d6d0bbe9139f9349bd17c917e854057af26a073c84aeeb54ac065c8c9f297d99d8063a0bccf032f77217c900bd
7
- data.tar.gz: f01787f76223c896154312b43862ac1276154715c9b8234a761c84dc046a1f5ab7f82e387c821805bf2f3b22ed99452bd834336dfdfc8f15350345efcc575de8
6
+ metadata.gz: '081ef6717877b1c40682fbcbf1436a7e44fd51307a03650edc9a6d4a594a99bc4e9f07adbb73a9073f784aa75dc6a5463f0aa1d77b77fefc976c0531273ced22'
7
+ data.tar.gz: c1fb5da8a19b9d6ee1324badbbff4caac4db49349efd301b9daaa74097372f2241e174617af5b59970b467610145419b6e392b579fc4a3457073439e5ec6824e
@@ -1,15 +1,50 @@
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
+ # 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
31
+ with:
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-
39
+ - name: Install dependencies
40
+ run: |
41
+ bundle config path vendor/bundle
42
+ bundle install
43
+ - name: Run Lints
44
+ run: bundle exec rubocop -P
45
+ - uses: actions/cache@v2
12
46
  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 }}
47
+ path: example/vendor/bundle
48
+ key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
49
+ restore-keys: |
50
+ ${{ runner.os }}-${{ matrix.ruby }}-example-deps-
data/.gitignore CHANGED
@@ -13,3 +13,5 @@
13
13
  Gemfile.lock
14
14
 
15
15
  *.gem
16
+
17
+ /vendor/
@@ -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
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [v0.16.0]: 2020-10-23
4
+
5
+ - Allow non-class types to be used as the inputs to controllers
6
+
7
+ ## [v0.15.0]: 2020-09-02
8
+
9
+ ### Added
10
+ - Add a new `#merge` method to output objects, which will merge fields from another output object into the given output object.
11
+ - Add `multi` to Output Objects, as a way to define more than one field of the same type at once.
12
+ - Add an `inherits:` key to output objects, for view inheritance.
13
+ - Add `SoberSwag::Types::CommaArray`, which parses comma-separated strings into arrays.
14
+ This also sets `style` to `form` and `explode` to `false` when generating Swagger docs.
15
+ This class is mostly useful for query parameters where you want a simpler format: `tag=foo,bar` instead of `tag[]=foo,tag[]=bar`.
16
+ - Add support for using `meta` to specify alternative `style` and `explode` keys for query and path params.
17
+ 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.
18
+
19
+ ### Fixed
20
+ - No longer swallow `Dry::Struct` errors, instead let them surface to the user.
21
+
22
+ [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,30 @@ QueryInput = SoberSwag.input_object do
151
268
  end
152
269
  ```
153
270
 
271
+ ## Testing the validity of output objects
272
+
273
+ If you're using RSpec and want to test the validity of output objects, you can do so relatively easily.
274
+
275
+ 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:
276
+
277
+ ```ruby
278
+ RSpec.describe UserOutputObject do
279
+ describe 'serialized result' do
280
+ subject do
281
+ described_class.type.new(described_class.serialize(create(:user)))
282
+ end
283
+
284
+ it 'works with an object' do
285
+ expect { subject }.not_to raise_error
286
+ end
287
+ end
288
+ end
289
+ ```
290
+
154
291
  ## Special Thanks
155
292
 
156
293
  This gem is a mishmash of ideas from various sources.
157
294
  The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
158
295
  On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
159
296
  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).
297
+ 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).
@@ -1,48 +1,36 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
2
 
4
3
  require 'bundler/setup'
5
4
  require 'sober_swag'
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
- module Types
10
- include Dry.Types()
11
- end
5
+ require 'pry'
12
6
 
13
7
  Bio = SoberSwag.input_object do
14
- attribute :name, primitive(:String).meta(description: 'A very basic bio name')
8
+ attribute :description, SoberSwag::Types::String
9
+ attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
15
10
  end
16
11
 
17
12
  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')
20
- attribute? :mood, Types::String
13
+ attribute :name, SoberSwag::Types::String
14
+ attribute? :bio, Bio.optional
21
15
  end
22
16
 
23
- ##
24
- # Demonstration of subclass-style.
25
- class PersonSearch < SoberSwag::InputObject
26
- attribute? :name, Types::String
27
- attribute? :age, Types::Integer
17
+ MultiFloorLocation = SoberSwag.input_object do
18
+ attribute :building, SoberSwag::Types::String.enum('science', 'mathematics', 'literature')
19
+ attribute :floor, SoberSwag::Types::String
20
+ attribute :room, SoberSwag::Types::Integer
28
21
  end
29
22
 
30
- ##
31
- # Demonstration of subclass-style *and* recursion in structs.
32
- class LinkedList < SoberSwag::InputObject
33
- attribute :value, Types::String
34
- attribute? :next, LinkedList
23
+ SingleFloorLocation = SoberSwag.input_object do
24
+ attribute :building, SoberSwag::Types::String.enum('philosophy', 'computer science')
25
+ attribute :room, SoberSwag::Types::Integer
35
26
  end
36
27
 
37
- Foo = SoberSwag::OutputObject.define do
38
- field :name, primitive(:String)
39
- field :age, primitive(:String)
40
-
41
- view :foo do
42
- field :bar, primitive(:String).optional
43
- end
28
+ SchoolClass = SoberSwag.input_object do
29
+ attribute :prof, Person.meta(description: 'The person who teaches this class.')
30
+ attribute :students, SoberSwag::Types::Array.of(Person)
31
+ attribute :location, (SingleFloorLocation | MultiFloorLocation).meta(description: 'What building and room this is in')
44
32
  end
45
33
 
46
- # (If you use this, don't forget to add pry to your Gemfile!)
47
- require 'pry'
34
+ SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
35
+
48
36
  Pry.start