sober_swag 0.3.0 → 0.8.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 +9 -6
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -1
- data/README.md +66 -20
- 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 +65 -52
- 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/mapped.rb +0 -7
- data/lib/sober_swag/server.rb +6 -2
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +3 -0
- metadata +18 -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: ad48275d462cfbd18f8bec906c7dbbf4999a360dca0417e3697370bdf43482d6
|
|
4
|
+
data.tar.gz: a5e6a560fa783c21604917402b97638315d874e96be67679bce49a003f2b0654
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a52d5e36c080525ae5dd3ec1d44b054916b9e926b7342b41c4448a3d6e4aa500e2b544ec71d83ce7dde1927fdfef27b77612e1206fed7db18b5300fd712358c
|
|
7
|
+
data.tar.gz: 67d15424d01ef63ed06125b2270232c5d9c95d5615eaa155ce40902c09e14224ad5745dbbd92f3e8fee9053d6b4ea1357156e2e868a5ede4c4d2e546c9bb1e68
|
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,13 +29,13 @@ 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
|
|
@@ -48,9 +51,9 @@ jobs:
|
|
|
48
51
|
- uses: actions/cache@v2
|
|
49
52
|
with:
|
|
50
53
|
path: example/vendor/bundle
|
|
51
|
-
key: ${{ runner.os }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
|
54
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-example-deps-${{ hashFiles('example/**/Gemfile.lock') }}
|
|
52
55
|
restore-keys: |
|
|
53
|
-
${{ runner.os }}-example-deps-
|
|
56
|
+
${{ runner.os }}-${{ matrix.ruby }}-example-deps-
|
|
54
57
|
- name: Install example dependencies for example
|
|
55
58
|
working-directory: example
|
|
56
59
|
run: |
|
|
@@ -58,4 +61,4 @@ jobs:
|
|
|
58
61
|
bundle install
|
|
59
62
|
- name: Run specs for example
|
|
60
63
|
working-directory: example
|
|
61
|
-
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
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|

|
|
5
5
|
|
|
6
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.
|
|
7
|
-
Other tools generate
|
|
7
|
+
Other tools generate documentation from a DSL.
|
|
8
8
|
This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids.
|
|
9
9
|
|
|
10
10
|
An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation).
|
|
11
11
|
|
|
12
|
-
This gem uses pattern matching, and is thus only compatible with Ruby 2.7 or later.
|
|
13
|
-
|
|
14
12
|
## Types for a fully-automated API
|
|
15
13
|
|
|
16
14
|
SoberSwag lets you type your API using describe blocks.
|
|
@@ -18,22 +16,22 @@ In any controller that includes `SoberSwag::Controller`, you get access to the s
|
|
|
18
16
|
This lets you type your API endpoint:
|
|
19
17
|
|
|
20
18
|
```ruby
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
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
|
|
24
|
+
end
|
|
25
|
+
request_body do
|
|
26
|
+
attribute? :name, Types::Params::String
|
|
27
|
+
attribute? :age, Types::Params::Integer
|
|
32
28
|
end
|
|
29
|
+
path_params { attribute :id, Types::Params::Integer }
|
|
33
30
|
end
|
|
31
|
+
end
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
We can now
|
|
34
|
+
We can now use this information to generate swagger documentation, available at the `swagger` action on this controller.
|
|
37
35
|
More than that, we can use this information *inside* our controller methods:
|
|
38
36
|
|
|
39
37
|
```ruby
|
|
@@ -87,27 +85,75 @@ end
|
|
|
87
85
|
|
|
88
86
|
Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development.
|
|
89
87
|
|
|
90
|
-
### SoberSwag
|
|
88
|
+
### SoberSwag Input Objects
|
|
91
89
|
|
|
92
90
|
Input parameters (including path, query, and request body) are typed using [dry-struct](https://dry-rb.org/gems/dry-struct/1.0/).
|
|
93
|
-
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:
|
|
94
92
|
|
|
95
93
|
```ruby
|
|
96
|
-
User = SoberSwag.
|
|
94
|
+
User = SoberSwag.input_object do
|
|
97
95
|
attribute :name, SoberSwag::Types::String
|
|
98
96
|
# use ? if attributes are not required
|
|
99
97
|
attribute? :favorite_movie, SoberSwag::Types::String
|
|
100
98
|
# use .optional if attributes may be null
|
|
101
|
-
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
|
|
102
117
|
end
|
|
103
118
|
```
|
|
104
119
|
|
|
105
120
|
Under the hood, this literally just generates a subclass of `Dry::Struct`.
|
|
106
121
|
We use the DSL-like method just to make working with Rails' reloading less annoying.
|
|
107
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
|
+
|
|
108
154
|
## Special Thanks
|
|
109
155
|
|
|
110
|
-
This gem is a
|
|
156
|
+
This gem is a mishmash of ideas from various sources.
|
|
111
157
|
The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based.
|
|
112
158
|
On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers.
|
|
113
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
|
|
@@ -73,13 +73,16 @@ module SoberSwag
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def path_schema
|
|
76
|
-
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
|
|
77
80
|
rescue TooComplicatedError => e
|
|
78
81
|
raise TooComplicatedForPathError, e.message
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
def query_schema
|
|
82
|
-
path_schema_stub.map { |e| e.merge(in: :query) }
|
|
85
|
+
path_schema_stub.map { |e| e.merge(in: :query, style: :deepObject, explode: true) }
|
|
83
86
|
rescue TooComplicatedError => e
|
|
84
87
|
raise TooComplicatedForQueryError, e.message
|
|
85
88
|
end
|
|
@@ -150,14 +153,17 @@ module SoberSwag
|
|
|
150
153
|
|
|
151
154
|
def rewrite_sums(object) # rubocop:disable Metrics/MethodLength
|
|
152
155
|
case object
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
167
|
else
|
|
162
168
|
object
|
|
163
169
|
end
|
|
@@ -165,59 +171,69 @@ module SoberSwag
|
|
|
165
171
|
|
|
166
172
|
def flatten_one_ofs(object)
|
|
167
173
|
case object
|
|
168
|
-
|
|
169
|
-
Nodes::OneOf.new(
|
|
170
|
-
|
|
174
|
+
when Nodes::OneOf
|
|
175
|
+
Nodes::OneOf.new(object.deconstruct.uniq)
|
|
176
|
+
else
|
|
171
177
|
object
|
|
172
178
|
end
|
|
173
179
|
end
|
|
174
180
|
|
|
175
181
|
def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
176
182
|
case object
|
|
177
|
-
|
|
183
|
+
when Nodes::List
|
|
178
184
|
{
|
|
179
185
|
type: :array,
|
|
180
|
-
items:
|
|
186
|
+
items: object.deconstruct.first
|
|
181
187
|
}
|
|
182
|
-
|
|
188
|
+
when Nodes::Enum
|
|
183
189
|
{
|
|
184
190
|
type: :string,
|
|
185
|
-
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
|
|
186
215
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{ oneOf: cases }
|
|
195
|
-
in Nodes::Object[*attrs]
|
|
196
|
-
# openAPI requires that you give a list of required attributes
|
|
197
|
-
# (which IMO is the *totally* wrong thing to do but whatever)
|
|
198
|
-
# so we must do this garbage
|
|
199
|
-
required = attrs.filter { |(_, b)| b[:required] }.map(&:first)
|
|
200
|
-
{
|
|
201
|
-
type: :object,
|
|
202
|
-
properties: attrs.map { |(a, b)|
|
|
203
|
-
[a, b.reject { |k, _| k == :required }]
|
|
204
|
-
}.to_h,
|
|
205
|
-
required: required
|
|
206
|
-
}
|
|
207
|
-
in Nodes::Attribute[name, true, value]
|
|
208
|
-
[name, value.merge(required: true)]
|
|
209
|
-
in Nodes::Attribute[name, false, value]
|
|
210
|
-
[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
|
|
211
223
|
# can't match on value directly as ruby uses `===` to match,
|
|
212
224
|
# and classes use `===` to mean `is an instance of`, as
|
|
213
225
|
# opposed to direct equality lmao
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
221
237
|
else
|
|
222
238
|
raise ArgumentError, "Got confusing node #{object} (#{object.class})"
|
|
223
239
|
end
|
|
@@ -226,13 +242,10 @@ module SoberSwag
|
|
|
226
242
|
def path_schema_stub
|
|
227
243
|
@path_schema_stub ||=
|
|
228
244
|
object_schema[:properties].map do |k, v|
|
|
229
|
-
ensure_uncomplicated(k, v)
|
|
245
|
+
# ensure_uncomplicated(k, v)
|
|
230
246
|
{
|
|
231
247
|
name: k,
|
|
232
248
|
schema: v.reject { |key, _| %i[required nullable].include?(key) },
|
|
233
|
-
# rubocop:disable Style/DoubleNegation
|
|
234
|
-
allowEmptyValue: !object_schema[:required].include?(k) || !!v[:nullable], # if it's required, no empties, but if *nullabe*, empties are okay
|
|
235
|
-
# rubocop:enable Style/DoubleNegation
|
|
236
249
|
required: object_schema[:required].include?(k) || false
|
|
237
250
|
}
|
|
238
251
|
end
|
|
@@ -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
|
|
@@ -29,13 +29,6 @@ module SoberSwag
|
|
|
29
29
|
def type
|
|
30
30
|
@base.type
|
|
31
31
|
end
|
|
32
|
-
|
|
33
|
-
##
|
|
34
|
-
# I have no freaking clue if ruby optimizes proc composition,
|
|
35
|
-
# but we at least save some node traversals here
|
|
36
|
-
def via_map(&block)
|
|
37
|
-
SoberSwag::Serializer::Mapped.new(@base, @map_f >> block)
|
|
38
|
-
end
|
|
39
32
|
end
|
|
40
33
|
end
|
|
41
34
|
end
|
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
|
|
@@ -34,6 +36,7 @@ Gem::Specification.new do |spec|
|
|
|
34
36
|
|
|
35
37
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
36
38
|
spec.add_development_dependency 'pry'
|
|
39
|
+
spec.add_development_dependency 'pry-byebug'
|
|
37
40
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
38
41
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
39
42
|
spec.add_development_dependency 'rubocop'
|
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.8.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-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: pry-byebug
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
83
97
|
- !ruby/object:Gem::Dependency
|
|
84
98
|
name: rake
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,7 +180,6 @@ files:
|
|
|
166
180
|
- ".ruby-version"
|
|
167
181
|
- ".travis.yml"
|
|
168
182
|
- Gemfile
|
|
169
|
-
- Gemfile.lock
|
|
170
183
|
- LICENSE.txt
|
|
171
184
|
- README.md
|
|
172
185
|
- Rakefile
|
|
@@ -288,14 +301,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
288
301
|
requirements:
|
|
289
302
|
- - ">="
|
|
290
303
|
- !ruby/object:Gem::Version
|
|
291
|
-
version:
|
|
304
|
+
version: 2.6.0
|
|
292
305
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
293
306
|
requirements:
|
|
294
307
|
- - ">="
|
|
295
308
|
- !ruby/object:Gem::Version
|
|
296
309
|
version: '0'
|
|
297
310
|
requirements: []
|
|
298
|
-
rubygems_version: 3.
|
|
311
|
+
rubygems_version: 3.0.3
|
|
299
312
|
signing_key:
|
|
300
313
|
specification_version: 4
|
|
301
314
|
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.2.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.2)
|
|
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.4.4)
|
|
22
|
-
docile (1.3.2)
|
|
23
|
-
dry-configurable (0.11.6)
|
|
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.3)
|
|
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.2)
|
|
70
|
-
rspec-support (~> 3.9.3)
|
|
71
|
-
rspec-expectations (3.9.2)
|
|
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.3)
|
|
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.4.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
|