sober_swag 0.2.0 → 0.7.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.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +0 -1
- data/.github/workflows/ruby.yml +16 -7
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -1
- data/README.md +67 -23
- data/bin/console +10 -1
- data/docs/serializers.md +13 -13
- data/example/Gemfile +0 -2
- data/example/Gemfile.lock +1 -4
- data/example/app/controllers/people_controller.rb +7 -5
- data/example/app/controllers/posts_controller.rb +8 -0
- data/example/spec/requests/people/index_spec.rb +2 -2
- data/lib/sober_swag/compiler/type.rb +70 -53
- data/lib/sober_swag/nodes/base.rb +4 -0
- data/lib/sober_swag/output_object/field_syntax.rb +1 -1
- data/lib/sober_swag/parser.rb +4 -1
- data/lib/sober_swag/serializer.rb +1 -1
- data/lib/sober_swag/serializer/conditional.rb +5 -1
- data/lib/sober_swag/serializer/mapped.rb +3 -1
- data/lib/sober_swag/server.rb +6 -2
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +2 -0
- metadata +4 -5
- data/Gemfile.lock +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a005a30f258a97e03e6a8c69429a3bf3d8f35477ffe0dbe7ba4afb030e36986d
|
4
|
+
data.tar.gz: 0d9aaed05874fec05ebccbe4b987a9c0d46eaeb3fefe9633b34340a28f956c75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10659925df20f3eeab802f8c8648c5f589a509d247d9017076893f67501dc4ba1f97e1c3f21958dcedc1e1aff950d8375e74825f038e1e360c984e01a4656c61
|
7
|
+
data.tar.gz: be74588c605a26165363e88ab95dbfcb4a83cb4d169a224544b74389c69049a769bcde8fb14e6338f7fac92419a317fd9a2332352e7a420be95060c20523e666
|
data/.github/workflows/ruby.yml
CHANGED
@@ -17,6 +17,9 @@ jobs:
|
|
17
17
|
test:
|
18
18
|
|
19
19
|
runs-on: ubuntu-latest
|
20
|
+
strategy:
|
21
|
+
matrix:
|
22
|
+
ruby: [ '2.6', '2.7' ]
|
20
23
|
|
21
24
|
steps:
|
22
25
|
- uses: actions/checkout@v2
|
@@ -26,25 +29,31 @@ jobs:
|
|
26
29
|
# uses: ruby/setup-ruby@v1
|
27
30
|
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
|
28
31
|
with:
|
29
|
-
ruby-version:
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
30
33
|
- uses: actions/cache@v2
|
31
34
|
with:
|
32
35
|
path: vendor/bundle
|
33
|
-
key: ${{ runner.os }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
|
36
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
|
34
37
|
restore-keys: |
|
35
|
-
${{ runner.os }}-gem-deps-
|
38
|
+
${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
|
36
39
|
- name: Install dependencies
|
37
40
|
run: |
|
38
41
|
bundle config path vendor/bundle
|
39
42
|
bundle install
|
40
43
|
- name: Run tests
|
41
|
-
run: bundle exec rake
|
44
|
+
run: COVERAGE=1 bundle exec rake
|
45
|
+
- name: Upload Coverage
|
46
|
+
uses: actions/upload-artifact@master
|
47
|
+
if: always()
|
48
|
+
with:
|
49
|
+
name: coverage-report
|
50
|
+
path: coverage
|
42
51
|
- uses: actions/cache@v2
|
43
52
|
with:
|
44
53
|
path: example/vendor/bundle
|
45
|
-
key: ${{ runner.os }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
54
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
46
55
|
restore-keys: |
|
47
|
-
${{ runner.os }}-example-deps-
|
56
|
+
${{ runner.os }}-${{ matrix.ruby }}-example-deps-
|
48
57
|
- name: Install example dependencies for example
|
49
58
|
working-directory: example
|
50
59
|
run: |
|
@@ -52,4 +61,4 @@ jobs:
|
|
52
61
|
bundle install
|
53
62
|
- name: Run specs for example
|
54
63
|
working-directory: example
|
55
|
-
run: rake
|
64
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -3,7 +3,7 @@ Style/FrozenStringLiteralComment:
|
|
3
3
|
Style/BlockDelimiters:
|
4
4
|
EnforcedStyle: braces_for_chaining
|
5
5
|
AllCops:
|
6
|
-
TargetRubyVersion: 2.
|
6
|
+
TargetRubyVersion: 2.6.0
|
7
7
|
Exclude:
|
8
8
|
- 'bin/bundle'
|
9
9
|
- 'example/bin/bundle'
|
@@ -31,6 +31,8 @@ Metrics/AbcSize:
|
|
31
31
|
Style/Documentation:
|
32
32
|
Exclude:
|
33
33
|
- 'example/db/migrate/**/*'
|
34
|
+
Metrics/PerceivedComplexity:
|
35
|
+
Enabled: false
|
34
36
|
Layout/EmptyLinesAroundAttributeAccessor:
|
35
37
|
Enabled: true
|
36
38
|
Layout/SpaceAroundMethodCallOperator:
|
data/README.md
CHANGED
@@ -3,16 +3,12 @@
|
|
3
3
|

|
4
4
|

|
5
5
|
|
6
|
-
***NOTE: THIS GEM IS HIGHLY EXPERIMENTAL AND PROBABLY SHOULD NOT YET BE USED IN PRODUCTION***.
|
7
|
-
|
8
6
|
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.
|
9
|
-
Other tools generate
|
7
|
+
Other tools generate documentation from a DSL.
|
10
8
|
This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids.
|
11
9
|
|
12
10
|
An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
|
13
11
|
|
14
|
-
This gem uses pattern matching, and is thus only compatible with Ruby 2.7 or later.
|
15
|
-
|
16
12
|
## Types for a fully-automated API
|
17
13
|
|
18
14
|
SoberSwag lets you type your API using describe blocks.
|
@@ -20,22 +16,22 @@ In any controller that includes `SoberSwag::Controller`, you get access to the s
|
|
20
16
|
This lets you type your API endpoint:
|
21
17
|
|
22
18
|
```ruby
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 }
|
19
|
+
class PeopleController < ApplicationController
|
20
|
+
include SoberSwag::Controller
|
21
|
+
define :patch, :update, '/people/{id}' do
|
22
|
+
query_params do
|
23
|
+
attribute? :include_extra_info, Types::Params::Bool
|
34
24
|
end
|
25
|
+
request_body do
|
26
|
+
attribute? :name, Types::Params::String
|
27
|
+
attribute? :age, Types::Params::Integer
|
28
|
+
end
|
29
|
+
path_params { attribute :id, Types::Params::Integer }
|
35
30
|
end
|
31
|
+
end
|
36
32
|
```
|
37
33
|
|
38
|
-
We can now
|
34
|
+
We can now use this information to generate swagger documentation, available at the `swagger` action on this controller.
|
39
35
|
More than that, we can use this information *inside* our controller methods:
|
40
36
|
|
41
37
|
```ruby
|
@@ -51,7 +47,7 @@ You define the type of parameters you accept, and we reject anything that doesn'
|
|
51
47
|
### Typed Responses
|
52
48
|
|
53
49
|
Want to go further and type your responses too?
|
54
|
-
Use SoberSwag
|
50
|
+
Use SoberSwag output objects, a serializer library heavily inspired by [Blueprinter](https://github.com/procore/blueprinter)
|
55
51
|
|
56
52
|
```ruby
|
57
53
|
PersonOutputObject = SoberSwag::OutputObject.define do
|
@@ -89,27 +85,75 @@ end
|
|
89
85
|
|
90
86
|
Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development.
|
91
87
|
|
92
|
-
### SoberSwag
|
88
|
+
### SoberSwag Input Objects
|
93
89
|
|
94
90
|
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
|
91
|
+
You don't have to do them inline. You can define them in another file, like so:
|
96
92
|
|
97
93
|
```ruby
|
98
|
-
User = SoberSwag.
|
94
|
+
User = SoberSwag.input_object do
|
99
95
|
attribute :name, SoberSwag::Types::String
|
100
96
|
# use ? if attributes are not required
|
101
97
|
attribute? :favorite_movie, SoberSwag::Types::String
|
102
98
|
# use .optional if attributes may be null
|
103
|
-
attribute :age, SoberSwag::Types::Params
|
99
|
+
attribute :age, SoberSwag::Types::Params::Integer.optional
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
Then, in your controller, just do:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class PeopleController < ApplicationController
|
107
|
+
include SoberSwag::Controller
|
108
|
+
|
109
|
+
define :path, :update, '/people/{id}' do
|
110
|
+
request_body(User)
|
111
|
+
path_params { attribute :id, Types::Params::Integer }
|
112
|
+
response(:ok, 'the updated person', PersonOutputObject)
|
113
|
+
end
|
114
|
+
def update
|
115
|
+
# same as above!
|
116
|
+
end
|
104
117
|
end
|
105
118
|
```
|
106
119
|
|
107
120
|
Under the hood, this literally just generates a subclass of `Dry::Struct`.
|
108
121
|
We use the DSL-like method just to make working with Rails' reloading less annoying.
|
109
122
|
|
123
|
+
#### Adding additional documentation
|
124
|
+
|
125
|
+
You can use the `.meta` attribute on a type to add additional documentation.
|
126
|
+
Some keys are considered "well-known" and will be present on the swagger output.
|
127
|
+
For example:
|
128
|
+
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
User = SoberSwag.input_object do
|
132
|
+
attribute? :name, SoberSwag::Types::String.meta(description: <<~MARKDOWN, deprecated: true)
|
133
|
+
The given name of the students, with strings encoded as escaped-ASCII.
|
134
|
+
This is used by an internal Cobol microservice from 1968.
|
135
|
+
Please use unicode_name instead unless you are that microservice.
|
136
|
+
MARKDOWN
|
137
|
+
attribute? :unicode_name, SoberSwag::Types::String
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
This will output the swagger you expect, with a description and a deprecated flag.
|
142
|
+
|
143
|
+
#### Adding Default Values
|
144
|
+
|
145
|
+
Sometimes it makes sense to specify a default value.
|
146
|
+
Don't worry, we've got you covered:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
QueryInput = SoberSwag.input_object do
|
150
|
+
attribute :allow_first, SoberSwag::Types::Params::Bool.default(false) # smartly alters type-definition to establish that passing this is not required.
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
110
154
|
## Special Thanks
|
111
155
|
|
112
|
-
This gem is a
|
156
|
+
This gem is a mishmash of ideas from various sources.
|
113
157
|
The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
|
114
158
|
On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
|
115
159
|
The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/).
|
data/bin/console
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
1
|
+
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'bundler/setup'
|
@@ -34,6 +34,15 @@ class LinkedList < SoberSwag::InputObject
|
|
34
34
|
attribute? :next, LinkedList
|
35
35
|
end
|
36
36
|
|
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
|
44
|
+
end
|
45
|
+
|
37
46
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
38
47
|
require 'pry'
|
39
48
|
Pry.start
|
data/docs/serializers.md
CHANGED
@@ -4,11 +4,11 @@ Serializers are a way to transform from one type to another.
|
|
4
4
|
For example, you might want to change an ActiveRecord object to a JSON struct.
|
5
5
|
You might also want to change an internal date-interval into a two-element array of dates, or some custom text format.
|
6
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
|
7
|
+
Furthermore, Serializers document the *type* that they serialize, so you can use it to generate documentation.
|
8
8
|
|
9
9
|
## The Basics
|
10
10
|
|
11
|
-
All serializers are
|
11
|
+
All serializers are inherited from [`SoberSwag::Serializer::Base`](../lib/sober_swag/serializer/base.rb).
|
12
12
|
This is an abstract class that implements several methods, most of which will be documented later.
|
13
13
|
The two that are most interesting, however, are `#type` and `#serialize`.
|
14
14
|
|
@@ -55,12 +55,12 @@ In the future, we might add some "debug mode" sorta thing that will do type-chec
|
|
55
55
|
|
56
56
|
### Mapped
|
57
57
|
|
58
|
-
Sometimes, you can create a
|
58
|
+
Sometimes, you can create a serializer via a *proc*.
|
59
59
|
For example, let's say that I want a serializer that takes a `Date` and returns a string.
|
60
60
|
I can do this:
|
61
61
|
|
62
62
|
```ruby
|
63
|
-
date_string = SoberSwag::Serializer.
|
63
|
+
date_string = SoberSwag::Serializer.primitive(:String).via_map { |d| d.to_s }
|
64
64
|
```
|
65
65
|
|
66
66
|
This is implemented via [`SoberSwag::Serializer::Mapped`](../lib/sober_swag/serializer/mapped.rb).
|
@@ -87,7 +87,7 @@ my_serializer.optional.serialize(nil) # => nil
|
|
87
87
|
# ^ nils become nil
|
88
88
|
```
|
89
89
|
|
90
|
-
This properly changes the `type` to be a
|
90
|
+
This properly changes the `type` to be a nilable type, as well.
|
91
91
|
|
92
92
|
### Array
|
93
93
|
|
@@ -121,9 +121,9 @@ Let's define an output object:
|
|
121
121
|
|
122
122
|
```ruby
|
123
123
|
StudentOutputObject = SoberSwag::OutputObject.define do
|
124
|
-
field :first_name,
|
125
|
-
field :last_name,
|
126
|
-
field :recent_grades,
|
124
|
+
field :first_name, primitive(:String)
|
125
|
+
field :last_name, primitive(:String)
|
126
|
+
field :recent_grades, primitive(:Integer).array do |student|
|
127
127
|
student.graded_assignments.limit(100).pluck(:grade)
|
128
128
|
end
|
129
129
|
end
|
@@ -143,10 +143,10 @@ Let's take a look at their use:
|
|
143
143
|
|
144
144
|
```ruby
|
145
145
|
StudentOutputObject = SoberSwag::OutputObject.define do
|
146
|
-
field :first_name,
|
147
|
-
field :last_name,
|
146
|
+
field :first_name, primitive(:String)
|
147
|
+
field :last_name, primitive(:String)
|
148
148
|
view :detail do
|
149
|
-
field :recent_grades,
|
149
|
+
field :recent_grades, primitive(:Integer).array do |student|
|
150
150
|
student.graded_assignments.limit(100).pluck(:grade)
|
151
151
|
end
|
152
152
|
end
|
@@ -189,7 +189,7 @@ StudentOutputObject = SoberSwag::OutputObject.define do
|
|
189
189
|
end
|
190
190
|
```
|
191
191
|
|
192
|
-
This can cause a circular
|
192
|
+
This can cause a circular dependency.
|
193
193
|
To break this, you can use a lambda:
|
194
194
|
|
195
195
|
```ruby
|
@@ -200,4 +200,4 @@ StudentOutputObject = SoberSwag::OutputObject.define do
|
|
200
200
|
end
|
201
201
|
```
|
202
202
|
|
203
|
-
For clarity (and to prevent infinitely-looping serializers on accident, we
|
203
|
+
For clarity (and to prevent infinitely-looping serializers on accident, we recommend you *always* use an explicit view for dependent output objects.
|
data/example/Gemfile
CHANGED
data/example/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
sober_swag (0.
|
4
|
+
sober_swag (0.5.0)
|
5
5
|
activesupport
|
6
6
|
dry-struct (~> 1.0)
|
7
7
|
dry-types (~> 1.2)
|
@@ -225,8 +225,5 @@ DEPENDENCIES
|
|
225
225
|
sqlite3 (~> 1.4)
|
226
226
|
tzinfo-data
|
227
227
|
|
228
|
-
RUBY VERSION
|
229
|
-
ruby 2.7.1p83
|
230
|
-
|
231
228
|
BUNDLED WITH
|
232
229
|
2.1.4
|
@@ -61,16 +61,18 @@ class PeopleController < ApplicationController
|
|
61
61
|
|
62
62
|
define :get, :index, '/people/' do
|
63
63
|
query_params do
|
64
|
-
attribute? :
|
65
|
-
|
66
|
-
|
64
|
+
attribute? :filters do
|
65
|
+
attribute? :first_name, Types::String
|
66
|
+
attribute? :last_name, Types::String
|
67
|
+
end
|
68
|
+
attribute :view, Types::String.default('base'.freeze).enum('base', 'detail')
|
67
69
|
end
|
68
70
|
response(:ok, 'all the people', PersonOutputObject.array)
|
69
71
|
end
|
70
72
|
def index
|
71
73
|
@people = Person.all
|
72
|
-
@people = @people.where('UPPER(first_name) LIKE UPPER(?)', "%#{parsed_query.first_name}%") if parsed_query.first_name
|
73
|
-
@people = @people.where('UPPER(last_name) LIKE UPPER(?)', "%#{parsed_query.last_name}%") if parsed_query.last_name
|
74
|
+
@people = @people.where('UPPER(first_name) LIKE UPPER(?)', "%#{parsed_query.filters.first_name}%") if parsed_query.filters&.first_name
|
75
|
+
@people = @people.where('UPPER(last_name) LIKE UPPER(?)', "%#{parsed_query.filters.last_name}%") if parsed_query.filters&.last_name
|
74
76
|
respond!(:ok, @people.includes(:posts), serializer_opts: { view: parsed_query.view })
|
75
77
|
end
|
76
78
|
|
@@ -39,12 +39,20 @@ class PostsController < ApplicationController
|
|
39
39
|
define :get, :index, '/posts/' do
|
40
40
|
query_params do
|
41
41
|
attribute? :view, ViewTypes
|
42
|
+
attribute :include_first, SoberSwag::Types::Params::Bool.default(false).meta(description: <<~MARKDOWN)
|
43
|
+
For historical reasons the first-ever post is the entire text of *Finnegan's Wake.*
|
44
|
+
Unfortunately, our contractors wound up depending on this quirk to complete dark arcane ceremonies,
|
45
|
+
so we can't remove it. Thus, by default, we don't include the first post unless you explicitly ask us to
|
46
|
+
(maybe you feel like some classic literature?).
|
47
|
+
MARKDOWN
|
42
48
|
end
|
43
49
|
response(:ok, 'all the posts', PostOutputObject.array)
|
44
50
|
end
|
45
51
|
def index
|
46
52
|
@posts = Post.all
|
47
53
|
|
54
|
+
@posts = @posts.where('id > 1') unless parsed_query.include_first
|
55
|
+
|
48
56
|
respond!(:ok, @posts.includes(:person), serializer_opts: { view: parsed_query.view })
|
49
57
|
end
|
50
58
|
|
@@ -35,13 +35,13 @@ RSpec.describe 'Index action for people' do
|
|
35
35
|
end
|
36
36
|
|
37
37
|
context 'with a good first-name search' do
|
38
|
-
let(:request) { get '/people', params: { first_name: 'A' } }
|
38
|
+
let(:request) { get '/people', params: { filters: { first_name: 'A' } } }
|
39
39
|
|
40
40
|
it_behaves_like 'a request with the person'
|
41
41
|
end
|
42
42
|
|
43
43
|
context 'with a good last-name search' do
|
44
|
-
let(:request) { get '/people', params: { last_name: 'G' } }
|
44
|
+
let(:request) { get '/people', params: { filters: { last_name: 'G' } } }
|
45
45
|
|
46
46
|
it_behaves_like 'a request with the person'
|
47
47
|
end
|
@@ -22,6 +22,8 @@ module SoberSwag
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def primitive_def(value)
|
25
|
+
value = value.primitive if value.is_a?(Dry::Types::Nominal)
|
26
|
+
|
25
27
|
return nil unless value.is_a?(Class)
|
26
28
|
|
27
29
|
if (name = primitive_name(value))
|
@@ -71,13 +73,16 @@ module SoberSwag
|
|
71
73
|
end
|
72
74
|
|
73
75
|
def path_schema
|
74
|
-
path_schema_stub.map
|
76
|
+
path_schema_stub.map do |e|
|
77
|
+
ensure_uncomplicated(e[:name], e[:schema])
|
78
|
+
e.merge(in: :path)
|
79
|
+
end
|
75
80
|
rescue TooComplicatedError => e
|
76
81
|
raise TooComplicatedForPathError, e.message
|
77
82
|
end
|
78
83
|
|
79
84
|
def query_schema
|
80
|
-
path_schema_stub.map { |e| e.merge(in: :query) }
|
85
|
+
path_schema_stub.map { |e| e.merge(in: :query, style: :deepObject, explode: true) }
|
81
86
|
rescue TooComplicatedError => e
|
82
87
|
raise TooComplicatedForQueryError, e.message
|
83
88
|
end
|
@@ -129,7 +134,7 @@ module SoberSwag
|
|
129
134
|
when Dry::Types::Sum
|
130
135
|
{ oneOf: normalize(parsed_type).elements.map { |t| self.class.new(t.value).schema_stub } }
|
131
136
|
else
|
132
|
-
raise
|
137
|
+
raise SoberSwag::Compiler::Error, "Cannot generate a schema stub for #{type} (#{type.class})"
|
133
138
|
end
|
134
139
|
end
|
135
140
|
|
@@ -148,14 +153,17 @@ module SoberSwag
|
|
148
153
|
|
149
154
|
def rewrite_sums(object) # rubocop:disable Metrics/MethodLength
|
150
155
|
case object
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
156
|
+
when Nodes::Sum
|
157
|
+
lhs, rhs = object.deconstruct
|
158
|
+
if lhs.is_a?(Nodes::OneOf) && rhs.is_a?(Nodes::OneOf)
|
159
|
+
Nodes::OneOf.new(lhs.deconstruct + rhs.deconstruct)
|
160
|
+
elsif lhs.is_a?(Nodes::OneOf)
|
161
|
+
Nodes::OneOf.new([*lhs.deconstruct, rhs])
|
162
|
+
elsif rhs.is_a?(Nodes::OneOf)
|
163
|
+
Nodes::OneOf.new([lhs, *rhs.deconstruct])
|
164
|
+
else
|
165
|
+
Nodes::OneOf.new([lhs, rhs])
|
166
|
+
end
|
159
167
|
else
|
160
168
|
object
|
161
169
|
end
|
@@ -163,59 +171,69 @@ module SoberSwag
|
|
163
171
|
|
164
172
|
def flatten_one_ofs(object)
|
165
173
|
case object
|
166
|
-
|
167
|
-
Nodes::OneOf.new(
|
168
|
-
|
174
|
+
when Nodes::OneOf
|
175
|
+
Nodes::OneOf.new(object.deconstruct.uniq)
|
176
|
+
else
|
169
177
|
object
|
170
178
|
end
|
171
179
|
end
|
172
180
|
|
173
181
|
def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
174
182
|
case object
|
175
|
-
|
183
|
+
when Nodes::List
|
176
184
|
{
|
177
185
|
type: :array,
|
178
|
-
items:
|
186
|
+
items: object.deconstruct.first
|
179
187
|
}
|
180
|
-
|
188
|
+
when Nodes::Enum
|
181
189
|
{
|
182
190
|
type: :string,
|
183
|
-
enum:
|
191
|
+
enum: object.deconstruct.first
|
192
|
+
}
|
193
|
+
when Nodes::OneOf
|
194
|
+
if object.deconstruct.include?({ type: 'null' })
|
195
|
+
rejected = object.deconstruct.reject { |e| e[:type] == 'null' }
|
196
|
+
if rejected.length == 1
|
197
|
+
rejected.first.merge(nullable: true)
|
198
|
+
else
|
199
|
+
{ oneOf: rejected, nullable: true }
|
200
|
+
end
|
201
|
+
else
|
202
|
+
{ oneOf: object.deconstruct }
|
203
|
+
end
|
204
|
+
when Nodes::Object
|
205
|
+
# openAPI requires that you give a list of required attributes
|
206
|
+
# (which IMO is the *totally* wrong thing to do but whatever)
|
207
|
+
# so we must do this garbage
|
208
|
+
required = object.deconstruct.filter { |(_, b)| b[:required] }.map(&:first)
|
209
|
+
{
|
210
|
+
type: :object,
|
211
|
+
properties: object.deconstruct.map { |(a, b)|
|
212
|
+
[a, b.reject { |k, _| k == :required }]
|
213
|
+
}.to_h,
|
214
|
+
required: required
|
184
215
|
}
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
{ oneOf: cases }
|
193
|
-
in Nodes::Object[*attrs]
|
194
|
-
# openAPI requires that you give a list of required attributes
|
195
|
-
# (which IMO is the *totally* wrong thing to do but whatever)
|
196
|
-
# so we must do this garbage
|
197
|
-
required = attrs.filter { |(_, b)| b[:required] }.map(&:first)
|
198
|
-
{
|
199
|
-
type: :object,
|
200
|
-
properties: attrs.map { |(a, b)|
|
201
|
-
[a, b.reject { |k, _| k == :required }]
|
202
|
-
}.to_h,
|
203
|
-
required: required
|
204
|
-
}
|
205
|
-
in Nodes::Attribute[name, true, value]
|
206
|
-
[name, value.merge(required: true)]
|
207
|
-
in Nodes::Attribute[name, false, value]
|
208
|
-
[name, value]
|
216
|
+
when Nodes::Attribute
|
217
|
+
name, req, value = object.deconstruct
|
218
|
+
if req
|
219
|
+
[name, value.merge(required: true)]
|
220
|
+
else
|
221
|
+
[name, value]
|
222
|
+
end
|
209
223
|
# can't match on value directly as ruby uses `===` to match,
|
210
224
|
# and classes use `===` to mean `is an instance of`, as
|
211
225
|
# opposed to direct equality lmao
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
226
|
+
when Nodes::Primitive
|
227
|
+
value = object.value
|
228
|
+
metadata = object.metadata
|
229
|
+
if self.class.primitive?(value)
|
230
|
+
md = self.class.primitive_def(value)
|
231
|
+
METADATA_KEYS.select(&metadata.method(:key?)).reduce(md) do |definition, key|
|
232
|
+
definition.merge(key => metadata[key])
|
233
|
+
end
|
234
|
+
else
|
235
|
+
{ '$ref': self.class.get_ref(value) }
|
236
|
+
end
|
219
237
|
else
|
220
238
|
raise ArgumentError, "Got confusing node #{object} (#{object.class})"
|
221
239
|
end
|
@@ -224,13 +242,10 @@ module SoberSwag
|
|
224
242
|
def path_schema_stub
|
225
243
|
@path_schema_stub ||=
|
226
244
|
object_schema[:properties].map do |k, v|
|
227
|
-
ensure_uncomplicated(k, v)
|
245
|
+
# ensure_uncomplicated(k, v)
|
228
246
|
{
|
229
247
|
name: k,
|
230
248
|
schema: v.reject { |key, _| %i[required nullable].include?(key) },
|
231
|
-
# rubocop:disable Style/DoubleNegation
|
232
|
-
allowEmptyValue: !object_schema[:required].include?(k) || !!v[:nullable], # if it's required, no empties, but if *nullabe*, empties are okay
|
233
|
-
# rubocop:enable Style/DoubleNegation
|
234
249
|
required: object_schema[:required].include?(k) || false
|
235
250
|
}
|
236
251
|
end
|
@@ -239,6 +254,8 @@ module SoberSwag
|
|
239
254
|
def ensure_uncomplicated(key, value)
|
240
255
|
return if value[:type]
|
241
256
|
|
257
|
+
return value[:oneOf].each { |member| ensure_uncomplicated(key, member) } if value[:oneOf]
|
258
|
+
|
242
259
|
raise TooComplicatedError, <<~ERROR
|
243
260
|
Property #{key} has object-schema #{value}, but this type of param should be simple (IE a primitive of some kind)
|
244
261
|
ERROR
|
@@ -10,7 +10,7 @@ module SoberSwag
|
|
10
10
|
##
|
11
11
|
# Given a symbol to this, we will use a primitive name
|
12
12
|
def primitive(name)
|
13
|
-
SoberSwag::Serializer.
|
13
|
+
SoberSwag::Serializer.primitive(SoberSwag::Types.const_get(name))
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
data/lib/sober_swag/parser.rb
CHANGED
@@ -10,6 +10,9 @@ module SoberSwag
|
|
10
10
|
|
11
11
|
def to_syntax # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
12
12
|
case @node
|
13
|
+
when Dry::Types::Default
|
14
|
+
# we handle this elsewhere, so
|
15
|
+
bind(Parser.new(@node.type))
|
13
16
|
when Dry::Types::Array::Member
|
14
17
|
Nodes::List.new(bind(Parser.new(@node.member)))
|
15
18
|
when Dry::Types::Enum
|
@@ -21,7 +24,7 @@ module SoberSwag
|
|
21
24
|
when Dry::Types::Schema::Key
|
22
25
|
Nodes::Attribute.new(
|
23
26
|
@node.name,
|
24
|
-
@node.required?,
|
27
|
+
@node.required? && !@node.type.default?,
|
25
28
|
bind(Parser.new(@node.type))
|
26
29
|
)
|
27
30
|
when Dry::Types::Sum
|
data/lib/sober_swag/server.rb
CHANGED
@@ -9,8 +9,12 @@ module SoberSwag
|
|
9
9
|
Rails.application.routes.routes.map { |route|
|
10
10
|
route.defaults[:controller]
|
11
11
|
}.to_set.reject(&:nil?).map { |controller|
|
12
|
-
|
13
|
-
|
12
|
+
begin
|
13
|
+
"#{controller}_controller".classify.constantize
|
14
|
+
rescue StandardError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
}.compact.filter { |controller| controller.ancestors.include?(SoberSwag::Controller) }
|
14
18
|
end
|
15
19
|
|
16
20
|
##
|
data/lib/sober_swag/version.rb
CHANGED
data/sober_swag.gemspec
CHANGED
@@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
|
|
19
19
|
|
20
20
|
spec.metadata['homepage_uri'] = spec.homepage
|
21
21
|
|
22
|
+
spec.required_ruby_version = '>= 2.6.0'
|
23
|
+
|
22
24
|
# Specify which files should be added to the gem when it is released.
|
23
25
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
26
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sober_swag
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Super
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -166,7 +166,6 @@ files:
|
|
166
166
|
- ".ruby-version"
|
167
167
|
- ".travis.yml"
|
168
168
|
- Gemfile
|
169
|
-
- Gemfile.lock
|
170
169
|
- LICENSE.txt
|
171
170
|
- README.md
|
172
171
|
- Rakefile
|
@@ -288,14 +287,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
288
287
|
requirements:
|
289
288
|
- - ">="
|
290
289
|
- !ruby/object:Gem::Version
|
291
|
-
version:
|
290
|
+
version: 2.6.0
|
292
291
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
293
292
|
requirements:
|
294
293
|
- - ">="
|
295
294
|
- !ruby/object:Gem::Version
|
296
295
|
version: '0'
|
297
296
|
requirements: []
|
298
|
-
rubygems_version: 3.
|
297
|
+
rubygems_version: 3.0.3
|
299
298
|
signing_key:
|
300
299
|
specification_version: 4
|
301
300
|
summary: Generate swagger types from dry-types
|
data/Gemfile.lock
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
sober_swag (0.1.0)
|
5
|
-
activesupport
|
6
|
-
dry-struct (~> 1.0)
|
7
|
-
dry-types (~> 1.2)
|
8
|
-
|
9
|
-
GEM
|
10
|
-
remote: https://rubygems.org/
|
11
|
-
specs:
|
12
|
-
activesupport (6.0.3.1)
|
13
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
-
i18n (>= 0.7, < 2)
|
15
|
-
minitest (~> 5.1)
|
16
|
-
tzinfo (~> 1.1)
|
17
|
-
zeitwerk (~> 2.2, >= 2.2.2)
|
18
|
-
ast (2.4.1)
|
19
|
-
coderay (1.1.2)
|
20
|
-
concurrent-ruby (1.1.6)
|
21
|
-
diff-lcs (1.3)
|
22
|
-
docile (1.3.2)
|
23
|
-
dry-configurable (0.11.3)
|
24
|
-
concurrent-ruby (~> 1.0)
|
25
|
-
dry-core (~> 0.4, >= 0.4.7)
|
26
|
-
dry-equalizer (~> 0.2)
|
27
|
-
dry-container (0.7.2)
|
28
|
-
concurrent-ruby (~> 1.0)
|
29
|
-
dry-configurable (~> 0.1, >= 0.1.3)
|
30
|
-
dry-core (0.4.9)
|
31
|
-
concurrent-ruby (~> 1.0)
|
32
|
-
dry-equalizer (0.3.0)
|
33
|
-
dry-inflector (0.2.0)
|
34
|
-
dry-logic (1.0.6)
|
35
|
-
concurrent-ruby (~> 1.0)
|
36
|
-
dry-core (~> 0.2)
|
37
|
-
dry-equalizer (~> 0.2)
|
38
|
-
dry-struct (1.3.0)
|
39
|
-
dry-core (~> 0.4, >= 0.4.4)
|
40
|
-
dry-equalizer (~> 0.3)
|
41
|
-
dry-types (~> 1.3)
|
42
|
-
ice_nine (~> 0.11)
|
43
|
-
dry-types (1.4.0)
|
44
|
-
concurrent-ruby (~> 1.0)
|
45
|
-
dry-container (~> 0.3)
|
46
|
-
dry-core (~> 0.4, >= 0.4.4)
|
47
|
-
dry-equalizer (~> 0.3)
|
48
|
-
dry-inflector (~> 0.1, >= 0.1.2)
|
49
|
-
dry-logic (~> 1.0, >= 1.0.2)
|
50
|
-
i18n (1.8.2)
|
51
|
-
concurrent-ruby (~> 1.0)
|
52
|
-
ice_nine (0.11.2)
|
53
|
-
method_source (0.9.2)
|
54
|
-
minitest (5.14.1)
|
55
|
-
parallel (1.19.2)
|
56
|
-
parser (2.7.1.4)
|
57
|
-
ast (~> 2.4.1)
|
58
|
-
pry (0.12.2)
|
59
|
-
coderay (~> 1.1.0)
|
60
|
-
method_source (~> 0.9.0)
|
61
|
-
rainbow (3.0.0)
|
62
|
-
rake (13.0.1)
|
63
|
-
regexp_parser (1.7.1)
|
64
|
-
rexml (3.2.4)
|
65
|
-
rspec (3.9.0)
|
66
|
-
rspec-core (~> 3.9.0)
|
67
|
-
rspec-expectations (~> 3.9.0)
|
68
|
-
rspec-mocks (~> 3.9.0)
|
69
|
-
rspec-core (3.9.1)
|
70
|
-
rspec-support (~> 3.9.1)
|
71
|
-
rspec-expectations (3.9.0)
|
72
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
73
|
-
rspec-support (~> 3.9.0)
|
74
|
-
rspec-mocks (3.9.1)
|
75
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
76
|
-
rspec-support (~> 3.9.0)
|
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)
|
92
|
-
simplecov (0.18.5)
|
93
|
-
docile (~> 1.1)
|
94
|
-
simplecov-html (~> 0.11)
|
95
|
-
simplecov-html (0.12.2)
|
96
|
-
thread_safe (0.3.6)
|
97
|
-
tzinfo (1.2.7)
|
98
|
-
thread_safe (~> 0.1)
|
99
|
-
unicode-display_width (1.7.0)
|
100
|
-
zeitwerk (2.3.0)
|
101
|
-
|
102
|
-
PLATFORMS
|
103
|
-
ruby
|
104
|
-
|
105
|
-
DEPENDENCIES
|
106
|
-
bundler (~> 2.0)
|
107
|
-
pry
|
108
|
-
rake (~> 13.0)
|
109
|
-
rspec (~> 3.0)
|
110
|
-
rubocop
|
111
|
-
rubocop-rspec
|
112
|
-
simplecov
|
113
|
-
sober_swag!
|
114
|
-
|
115
|
-
BUNDLED WITH
|
116
|
-
2.1.4
|