sober_swag 0.14.0 → 0.19.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/dependabot.yml +15 -0
- data/.github/workflows/lint.yml +41 -9
- data/.github/workflows/ruby.yml +1 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +50 -5
- data/CHANGELOG.md +34 -0
- data/README.md +155 -4
- data/bin/console +36 -0
- data/bin/rspec +29 -0
- data/docs/serializers.md +74 -9
- data/example/Gemfile +2 -2
- data/example/app/controllers/application_controller.rb +5 -0
- data/example/app/controllers/people_controller.rb +4 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/lib/sober_swag.rb +1 -0
- data/lib/sober_swag/compiler.rb +1 -0
- data/lib/sober_swag/compiler/path.rb +7 -0
- data/lib/sober_swag/compiler/primitive.rb +77 -0
- data/lib/sober_swag/compiler/type.rb +57 -96
- data/lib/sober_swag/controller.rb +3 -9
- data/lib/sober_swag/controller/route.rb +30 -8
- data/lib/sober_swag/input_object.rb +36 -3
- data/lib/sober_swag/nodes/attribute.rb +8 -7
- data/lib/sober_swag/nodes/enum.rb +2 -2
- data/lib/sober_swag/nodes/primitive.rb +1 -1
- data/lib/sober_swag/output_object/definition.rb +14 -2
- data/lib/sober_swag/output_object/field_syntax.rb +16 -0
- data/lib/sober_swag/parser.rb +10 -5
- data/lib/sober_swag/serializer/base.rb +2 -0
- data/lib/sober_swag/serializer/meta.rb +3 -1
- data/lib/sober_swag/server.rb +22 -10
- data/lib/sober_swag/type.rb +7 -0
- data/lib/sober_swag/type/named.rb +35 -0
- data/lib/sober_swag/types.rb +2 -0
- data/lib/sober_swag/types/comma_array.rb +17 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +2 -2
- metadata +18 -10
data/bin/console
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'sober_swag'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
Bio = SoberSwag.input_object do
|
8
|
+
attribute :description, SoberSwag::Types::String
|
9
|
+
attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
|
10
|
+
end
|
11
|
+
|
12
|
+
Person = SoberSwag.input_object do
|
13
|
+
attribute :name, SoberSwag::Types::String
|
14
|
+
attribute? :bio, Bio.optional
|
15
|
+
end
|
16
|
+
|
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
|
21
|
+
end
|
22
|
+
|
23
|
+
SingleFloorLocation = SoberSwag.input_object do
|
24
|
+
attribute :building, SoberSwag::Types::String.enum('philosophy', 'computer science')
|
25
|
+
attribute :room, SoberSwag::Types::Integer
|
26
|
+
end
|
27
|
+
|
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')
|
32
|
+
end
|
33
|
+
|
34
|
+
SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
|
35
|
+
|
36
|
+
Pry.start
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/docs/serializers.md
CHANGED
@@ -25,21 +25,21 @@ For example, you might have a serializer that can return a date in two formats,
|
|
25
25
|
In this case, it might be used as:
|
26
26
|
|
27
27
|
```ruby
|
28
|
-
serializer.new(my_record, { format: :
|
28
|
+
serializer.new(my_record, { format: :new_style })
|
29
29
|
```
|
30
30
|
|
31
31
|
However, since it is *always* optional, you can also do:
|
32
32
|
|
33
33
|
```ruby
|
34
|
-
|
34
|
+
serializer.new(my_record)
|
35
35
|
```
|
36
36
|
|
37
37
|
And it *should* pick some default format.
|
38
38
|
|
39
39
|
### Primitives
|
40
40
|
|
41
|
-
Primitive serializers
|
42
|
-
They are implemented as [`SoberSwag::Serializer::Primitive`](../lib/sober_swag/serializer/primitive.rb), or as the `#primitive` method on
|
41
|
+
Primitive serializers — or "identity serializers" — are serializers that do nothing.
|
42
|
+
They are implemented as [`SoberSwag::Serializer::Primitive`](../lib/sober_swag/serializer/primitive.rb), or as the `#primitive` method on an `OutputObject`.
|
43
43
|
Since they don't do anything, they can be considered the most "basic" serializer.
|
44
44
|
|
45
45
|
These serializers *do not* check types.
|
@@ -51,12 +51,12 @@ serializer.serialize(10) # => 10
|
|
51
51
|
```
|
52
52
|
|
53
53
|
Thus, care should be used when working with these serializers.
|
54
|
-
In the future, we might add some "debug mode"
|
54
|
+
In the future, we might add some "debug mode" thing that will do type-checking and throw errors, however, the cost of doing so in production is probably not worth it.
|
55
55
|
|
56
56
|
### Mapped
|
57
57
|
|
58
58
|
Sometimes, you can create a serializer via a *proc*.
|
59
|
-
For example, let's say that I want a serializer that takes a `Date` and returns a
|
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
|
@@ -74,7 +74,7 @@ In the future, we might add a debug mode.
|
|
74
74
|
Oftentimes, we want to give a serializer the ability to serialize `nil` values.
|
75
75
|
This is often useful in serializing fields.
|
76
76
|
|
77
|
-
It turns out that it's pretty easy to make a serializer that can serialize `nil` values: just propogate
|
77
|
+
It turns out that it's pretty easy to make a serializer that can serialize `nil` values: just propogate `nil`s.
|
78
78
|
For example, let's say I have the following code:
|
79
79
|
|
80
80
|
```ruby
|
@@ -99,7 +99,7 @@ Continuing our example from earlier:
|
|
99
99
|
my_serializer.array.serialize([Foo.new(10, 11)]) #=> [{ bar: 10, baz: 11 }]
|
100
100
|
```
|
101
101
|
|
102
|
-
This changes the type properly
|
102
|
+
This changes the type properly too.
|
103
103
|
|
104
104
|
## OutputObjects
|
105
105
|
|
@@ -132,9 +132,25 @@ end
|
|
132
132
|
We can see a few things here:
|
133
133
|
|
134
134
|
1. You define field names with a `field` definition, which is a way to define the serializer for a single field.
|
135
|
-
2. You must provide types with field names
|
135
|
+
2. You must provide types with field names.
|
136
136
|
3. You can use blocks to do data formatting, which lets you pick different fields and such.
|
137
137
|
|
138
|
+
|
139
|
+
### Multi
|
140
|
+
|
141
|
+
If you have a few fields of the same type, you can use `#multi` to define them all at once:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
StudentOutputObject = SoberSwag::OutputObject.define do
|
145
|
+
multi [:first_name, :last_name], primitive(:String)
|
146
|
+
field :recent_grades, primitive(:Integer).array do |student|
|
147
|
+
student.graded_assignments.limit(100).pluck(:grade)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
This saves a bit of typing, and can help with refactoring later.
|
153
|
+
|
138
154
|
### Views
|
139
155
|
|
140
156
|
Sometimes, you might want to add "variant" ways to look at data.
|
@@ -201,3 +217,52 @@ end
|
|
201
217
|
```
|
202
218
|
|
203
219
|
For clarity (and to prevent infinitely-looping serializers on accident, we recommend you *always* use an explicit view for dependent output objects.
|
220
|
+
|
221
|
+
### "Inheritance"
|
222
|
+
|
223
|
+
Output objects don't support inheritance.
|
224
|
+
You can't have one output object based on another.
|
225
|
+
You *can*, however, merge one into another!
|
226
|
+
Consider this case:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
GenericBioOutput = SoberSwag::OutputObject.define do
|
230
|
+
field :name, primitive(:String)
|
231
|
+
field :brief_history, primitive(:String)
|
232
|
+
end
|
233
|
+
|
234
|
+
ExecutiveBioOutput = SoberSwag::OutputObject.define do
|
235
|
+
merge GenericBioOutput
|
236
|
+
field :company, primitive(:String)
|
237
|
+
field :position, primitive(:String)
|
238
|
+
end
|
239
|
+
```
|
240
|
+
|
241
|
+
Using `#merge` lets you add in all the fields from one output object into another.
|
242
|
+
You can even use `merge` from within a view.
|
243
|
+
|
244
|
+
Note that `merge` does *not* copy anything but fields.
|
245
|
+
Identifiers and views will not be copied over.
|
246
|
+
|
247
|
+
### View Inheritance
|
248
|
+
|
249
|
+
While defining a new Output Object, you *do not* have access to the definition of that output object.
|
250
|
+
So, how do I say that one view should be an extension of another?
|
251
|
+
Simple, use the `inherits:` kwarg:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
BioOutput = SoberSwag::OutputObject.define do
|
255
|
+
field :name, primitive(:String)
|
256
|
+
|
257
|
+
view :detail do
|
258
|
+
field :bio, primitive(:String)
|
259
|
+
end
|
260
|
+
|
261
|
+
view :super_detail, inherits: :detail do
|
262
|
+
field :age, primitive(:Integer)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
`inherits` will automatically merge in all the fields of the referenced view.
|
268
|
+
This means that the view `super_detail` will include fields `name`, `bio`, and `age`.
|
data/example/Gemfile
CHANGED
@@ -8,7 +8,7 @@ gem 'actionpack', '>= 6.0.3.2'
|
|
8
8
|
# Use sqlite3 as the database for Active Record
|
9
9
|
gem 'sqlite3', '~> 1.4'
|
10
10
|
# Use Puma as the app server
|
11
|
-
gem 'puma', '~>
|
11
|
+
gem 'puma', '~> 5.2'
|
12
12
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
13
13
|
# gem 'jbuilder', '~> 2.7'
|
14
14
|
# Use Active Model has_secure_password
|
@@ -34,7 +34,7 @@ group :development, :test do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
group :development do
|
37
|
-
gem 'listen', '>= 3.0.5', '< 3.
|
37
|
+
gem 'listen', '>= 3.0.5', '< 3.5'
|
38
38
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
39
39
|
gem 'spring'
|
40
40
|
gem 'spring-watcher-listen', '~> 2.0.0'
|
@@ -35,6 +35,7 @@ class PeopleController < ApplicationController
|
|
35
35
|
request_body(PersonParams)
|
36
36
|
response(:ok, 'the person created', PersonOutputObject)
|
37
37
|
response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
|
38
|
+
tags 'people', 'create'
|
38
39
|
end
|
39
40
|
def create
|
40
41
|
p = Person.new(parsed_body.person.to_h)
|
@@ -50,6 +51,7 @@ class PeopleController < ApplicationController
|
|
50
51
|
path_params { attribute :id, Types::Params::Integer }
|
51
52
|
response(:ok, 'the person updated', PersonOutputObject)
|
52
53
|
response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
|
54
|
+
tags 'people', 'update'
|
53
55
|
end
|
54
56
|
def update
|
55
57
|
if @person.update(parsed_body.person.to_h)
|
@@ -68,6 +70,7 @@ class PeopleController < ApplicationController
|
|
68
70
|
attribute :view, Types::String.default('base'.freeze).enum('base', 'detail')
|
69
71
|
end
|
70
72
|
response(:ok, 'all the people', PersonOutputObject.array)
|
73
|
+
tags 'people', 'list'
|
71
74
|
end
|
72
75
|
def index
|
73
76
|
@people = Person.all
|
@@ -81,6 +84,7 @@ class PeopleController < ApplicationController
|
|
81
84
|
attribute :id, Types::Params::Integer
|
82
85
|
end
|
83
86
|
response(:ok, 'the person requested', PersonOutputObject)
|
87
|
+
tags 'people', 'show'
|
84
88
|
end
|
85
89
|
def show
|
86
90
|
respond!(:ok, @person)
|
@@ -47,6 +47,7 @@ class PostsController < ApplicationController
|
|
47
47
|
MARKDOWN
|
48
48
|
end
|
49
49
|
response(:ok, 'all the posts', PostOutputObject.array)
|
50
|
+
tags 'posts', 'list'
|
50
51
|
end
|
51
52
|
def index
|
52
53
|
@posts = Post.all
|
@@ -60,6 +61,7 @@ class PostsController < ApplicationController
|
|
60
61
|
path_params(ShowPath)
|
61
62
|
query_params { attribute? :view, ViewTypes }
|
62
63
|
response(:ok, 'the requested post', PostOutputObject)
|
64
|
+
tags 'posts', 'show'
|
63
65
|
end
|
64
66
|
def show
|
65
67
|
respond!(:ok, @post, serializer_opts: { view: parsed_query.view })
|
@@ -68,6 +70,7 @@ class PostsController < ApplicationController
|
|
68
70
|
define :post, :create, '/posts/' do
|
69
71
|
request_body(PostCreate)
|
70
72
|
response(:created, 'the created post', PostOutputObject)
|
73
|
+
tags 'posts', 'create'
|
71
74
|
end
|
72
75
|
def create
|
73
76
|
@post = Post.new(parsed_body.post.to_h)
|
@@ -83,6 +86,7 @@ class PostsController < ApplicationController
|
|
83
86
|
path_params(ShowPath)
|
84
87
|
request_body(PostUpdate)
|
85
88
|
response(:ok, 'the post updated', PostOutputObject.view(:base))
|
89
|
+
tags 'posts', 'update'
|
86
90
|
end
|
87
91
|
def update
|
88
92
|
if @post.update(parsed_body.post.to_h)
|
@@ -95,6 +99,7 @@ class PostsController < ApplicationController
|
|
95
99
|
define :delete, :destroy, '/posts/{id}' do
|
96
100
|
path_params(ShowPath)
|
97
101
|
response(:ok, 'the post deleted', PostOutputObject.view(:base))
|
102
|
+
tags 'posts', 'delete'
|
98
103
|
end
|
99
104
|
def destroy
|
100
105
|
@post.destroy
|
data/lib/sober_swag.rb
CHANGED
data/lib/sober_swag/compiler.rb
CHANGED
@@ -6,6 +6,7 @@ module SoberSwag
|
|
6
6
|
class Compiler
|
7
7
|
autoload(:Type, 'sober_swag/compiler/type')
|
8
8
|
autoload(:Error, 'sober_swag/compiler/error')
|
9
|
+
autoload(:Primitive, 'sober_swag/compiler/primitive')
|
9
10
|
autoload(:Path, 'sober_swag/compiler/path')
|
10
11
|
autoload(:Paths, 'sober_swag/compiler/paths')
|
11
12
|
|
@@ -21,6 +21,7 @@ module SoberSwag
|
|
21
21
|
base[:parameters] = params if params.any?
|
22
22
|
base[:responses] = responses
|
23
23
|
base[:requestBody] = request_body if request_body
|
24
|
+
base[:tags] = tags if tags
|
24
25
|
base
|
25
26
|
end
|
26
27
|
|
@@ -72,6 +73,12 @@ module SoberSwag
|
|
72
73
|
}
|
73
74
|
}
|
74
75
|
end
|
76
|
+
|
77
|
+
def tags
|
78
|
+
return nil unless route.tags.any?
|
79
|
+
|
80
|
+
route.tags
|
81
|
+
end
|
75
82
|
end
|
76
83
|
end
|
77
84
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SoberSwag
|
2
|
+
class Compiler
|
3
|
+
##
|
4
|
+
# Compiles a primitive type.
|
5
|
+
# Almost always constructed with the values from
|
6
|
+
# {SoberSwag::Nodes::Primitive}.
|
7
|
+
class Primitive
|
8
|
+
def initialize(type)
|
9
|
+
@type = type
|
10
|
+
|
11
|
+
raise Error, "#{type.inspect} is not a class!" unless @type.is_a?(Class)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
def swagger_primitive?
|
17
|
+
SWAGGER_PRIMITIVE_DEFS.include?(type)
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Is the wrapped type a named type, causing us to make a ref?
|
22
|
+
def named?
|
23
|
+
type <= SoberSwag::Type::Named
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Turn this type into a swagger hash with a proper type key.
|
28
|
+
def type_hash
|
29
|
+
if swagger_primitive?
|
30
|
+
SWAGGER_PRIMITIVE_DEFS.fetch(type)
|
31
|
+
else
|
32
|
+
{
|
33
|
+
oneOf: [
|
34
|
+
{ '$ref'.to_sym => named_ref }
|
35
|
+
]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
DATE_PRIMITIVE = { type: :string, format: :date }.freeze
|
41
|
+
DATE_TIME_PRIMITIVE = { type: :string, format: :'date-time' }.freeze
|
42
|
+
HASH_PRIMITIVE = { type: :object, additionalProperties: true }.freeze
|
43
|
+
|
44
|
+
SWAGGER_PRIMITIVE_DEFS =
|
45
|
+
{
|
46
|
+
NilClass => :null,
|
47
|
+
TrueClass => :boolean,
|
48
|
+
FalseClass => :boolean,
|
49
|
+
Float => :number,
|
50
|
+
Integer => :integer,
|
51
|
+
String => :string
|
52
|
+
}.transform_values { |v| { type: v.freeze } }
|
53
|
+
.to_h.merge(
|
54
|
+
Date => DATE_PRIMITIVE,
|
55
|
+
DateTime => DATE_TIME_PRIMITIVE,
|
56
|
+
Time => DATE_TIME_PRIMITIVE,
|
57
|
+
Hash => HASH_PRIMITIVE
|
58
|
+
).transform_values(&:freeze).freeze
|
59
|
+
|
60
|
+
def ref_name
|
61
|
+
raise Error, 'is not a type that is named!' if swagger_primitive?
|
62
|
+
|
63
|
+
if type <= SoberSwag::Type::Named
|
64
|
+
type.root_alias.identifier
|
65
|
+
else
|
66
|
+
type.name.gsub('::', '.')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def named_ref
|
73
|
+
"#/components/schemas/#{ref_name}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -4,46 +4,6 @@ module SoberSwag
|
|
4
4
|
# A compiler for DRY-Struct data types, essentially.
|
5
5
|
# It only consumes one type at a time.
|
6
6
|
class Type # rubocop:disable Metrics/ClassLength
|
7
|
-
class << self
|
8
|
-
def get_ref(klass)
|
9
|
-
"#/components/schemas/#{safe_name(klass)}"
|
10
|
-
end
|
11
|
-
|
12
|
-
def safe_name(klass)
|
13
|
-
if klass.respond_to?(:identifier)
|
14
|
-
klass.identifier
|
15
|
-
else
|
16
|
-
klass.to_s.gsub('::', '.')
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def primitive?(value)
|
21
|
-
primitive_def(value) != nil
|
22
|
-
end
|
23
|
-
|
24
|
-
def primitive_def(value)
|
25
|
-
value = value.primitive if value.is_a?(Dry::Types::Nominal)
|
26
|
-
|
27
|
-
return nil unless value.is_a?(Class)
|
28
|
-
|
29
|
-
if (name = primitive_name(value))
|
30
|
-
{ type: name }
|
31
|
-
elsif value == Date
|
32
|
-
{ type: 'string', format: 'date' }
|
33
|
-
elsif [Time, DateTime].any?(&value.ancestors.method(:include?))
|
34
|
-
{ type: 'string', format: 'date-time' }
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def primitive_name(value)
|
39
|
-
return 'null' if value == NilClass
|
40
|
-
return 'integer' if value == Integer
|
41
|
-
return 'number' if value == Float
|
42
|
-
return 'string' if value == String
|
43
|
-
return 'boolean' if [TrueClass, FalseClass].include?(value)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
7
|
class TooComplicatedError < ::SoberSwag::Compiler::Error; end
|
48
8
|
class TooComplicatedForPathError < TooComplicatedError; end
|
49
9
|
class TooComplicatedForQueryError < TooComplicatedError; end
|
@@ -65,7 +25,15 @@ module SoberSwag
|
|
65
25
|
|
66
26
|
def object_schema
|
67
27
|
@object_schema ||=
|
68
|
-
|
28
|
+
make_object_schema
|
29
|
+
end
|
30
|
+
|
31
|
+
def object_schema_meta
|
32
|
+
return {} unless standalone? && type <= SoberSwag::Type::Named
|
33
|
+
|
34
|
+
{
|
35
|
+
description: type.description
|
36
|
+
}.reject { |_, v| v.nil? }
|
69
37
|
end
|
70
38
|
|
71
39
|
def schema_stub
|
@@ -81,14 +49,16 @@ module SoberSwag
|
|
81
49
|
raise TooComplicatedForPathError, e.message
|
82
50
|
end
|
83
51
|
|
52
|
+
DEFAULT_QUERY_SCHEMA_ATTRS = { in: :query, style: :deepObject, explode: true }.freeze
|
53
|
+
|
84
54
|
def query_schema
|
85
|
-
path_schema_stub.map { |e|
|
55
|
+
path_schema_stub.map { |e| DEFAULT_QUERY_SCHEMA_ATTRS.merge(e) }
|
86
56
|
rescue TooComplicatedError => e
|
87
57
|
raise TooComplicatedForQueryError, e.message
|
88
58
|
end
|
89
59
|
|
90
60
|
def ref_name
|
91
|
-
|
61
|
+
SoberSwag::Compiler::Primitive.new(type).ref_name
|
92
62
|
end
|
93
63
|
|
94
64
|
def found_types
|
@@ -99,6 +69,10 @@ module SoberSwag
|
|
99
69
|
end
|
100
70
|
end
|
101
71
|
|
72
|
+
def mapped_type
|
73
|
+
@mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
|
74
|
+
end
|
75
|
+
|
102
76
|
def parsed_type
|
103
77
|
@parsed_type ||=
|
104
78
|
begin
|
@@ -121,22 +95,11 @@ module SoberSwag
|
|
121
95
|
|
122
96
|
private
|
123
97
|
|
124
|
-
def generate_schema_stub
|
125
|
-
|
126
|
-
|
127
|
-
case type
|
128
|
-
when Class
|
129
|
-
# refs have to be standalone
|
130
|
-
# so to not interefere with our other stuff, do this horrible garbage
|
131
|
-
{ oneOf: [{ '$ref'.to_sym => self.class.get_ref(type) }] }
|
132
|
-
when Dry::Types::Constrained
|
133
|
-
self.class.new(type.type).schema_stub
|
134
|
-
when Dry::Types::Array::Member
|
135
|
-
{ type: :array, items: self.class.new(type.member).schema_stub }
|
136
|
-
when Dry::Types::Sum
|
137
|
-
{ oneOf: normalize(parsed_type).elements.map { |t| self.class.new(t.value).schema_stub } }
|
98
|
+
def generate_schema_stub
|
99
|
+
if type.is_a?(Class)
|
100
|
+
SoberSwag::Compiler::Primitive.new(type).type_hash
|
138
101
|
else
|
139
|
-
|
102
|
+
object_schema
|
140
103
|
end
|
141
104
|
end
|
142
105
|
|
@@ -149,6 +112,10 @@ module SoberSwag
|
|
149
112
|
end
|
150
113
|
end
|
151
114
|
|
115
|
+
def make_object_schema(metadata_keys: METADATA_KEYS)
|
116
|
+
normalize(mapped_type).cata { |e| to_object_schema(e, metadata_keys) }.merge(object_schema_meta)
|
117
|
+
end
|
118
|
+
|
152
119
|
def normalize(object)
|
153
120
|
object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
|
154
121
|
end
|
@@ -180,29 +147,14 @@ module SoberSwag
|
|
180
147
|
end
|
181
148
|
end
|
182
149
|
|
183
|
-
def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
150
|
+
def to_object_schema(object, metadata_keys = METADATA_KEYS) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
184
151
|
case object
|
185
152
|
when Nodes::List
|
186
|
-
{
|
187
|
-
type: :array,
|
188
|
-
items: object.deconstruct.first
|
189
|
-
}
|
153
|
+
{ type: :array, items: object.element }
|
190
154
|
when Nodes::Enum
|
191
|
-
{
|
192
|
-
type: :string,
|
193
|
-
enum: object.deconstruct.first
|
194
|
-
}
|
155
|
+
{ type: :string, enum: object.values }
|
195
156
|
when Nodes::OneOf
|
196
|
-
|
197
|
-
rejected = object.deconstruct.reject { |e| e[:type] == 'null' }
|
198
|
-
if rejected.length == 1
|
199
|
-
rejected.first.merge(nullable: true)
|
200
|
-
else
|
201
|
-
{ oneOf: rejected, nullable: true }
|
202
|
-
end
|
203
|
-
else
|
204
|
-
{ oneOf: object.deconstruct }
|
205
|
-
end
|
157
|
+
one_of_to_schema(object)
|
206
158
|
when Nodes::Object
|
207
159
|
# openAPI requires that you give a list of required attributes
|
208
160
|
# (which IMO is the *totally* wrong thing to do but whatever)
|
@@ -216,41 +168,50 @@ module SoberSwag
|
|
216
168
|
required: required
|
217
169
|
}
|
218
170
|
when Nodes::Attribute
|
219
|
-
name, req, value = object.deconstruct
|
171
|
+
name, req, value, meta = object.deconstruct
|
172
|
+
value = value.merge(meta&.select { |k, _| metadata_keys.include?(k) } || {})
|
220
173
|
if req
|
221
174
|
[name, value.merge(required: true)]
|
222
175
|
else
|
223
176
|
[name, value]
|
224
177
|
end
|
225
178
|
when Nodes::Primitive
|
226
|
-
value
|
227
|
-
metadata = object.metadata
|
228
|
-
type_def =
|
229
|
-
if self.class.primitive?(value)
|
230
|
-
self.class.primitive_def(value)
|
231
|
-
else
|
232
|
-
metadata.merge!(value.meta)
|
233
|
-
# refs have to be on their own, this is the stupid workaround
|
234
|
-
# so you can add descriptions and stuff
|
235
|
-
{ oneOf: [{ '$ref'.to_sym => self.class.get_ref(value) }] }
|
236
|
-
end
|
237
|
-
METADATA_KEYS.select(&metadata.method(:key?)).reduce(type_def) do |definition, key|
|
238
|
-
definition.merge(key => metadata[key])
|
239
|
-
end
|
179
|
+
object.value.merge(object.metadata.select { |k, _| metadata_keys.include?(k) })
|
240
180
|
else
|
241
181
|
raise ArgumentError, "Got confusing node #{object} (#{object.class})"
|
242
182
|
end
|
243
183
|
end
|
244
184
|
|
185
|
+
def one_of_to_schema(object)
|
186
|
+
if object.deconstruct.include?({ type: :null })
|
187
|
+
rejected = object.deconstruct.reject { |e| e[:type] == :null }
|
188
|
+
if rejected.length == 1
|
189
|
+
rejected.first.merge(nullable: true)
|
190
|
+
else
|
191
|
+
{ oneOf: flatten_oneofs_hash(rejected), nullable: true }
|
192
|
+
end
|
193
|
+
else
|
194
|
+
{ oneOf: flatten_oneofs_hash(object.deconstruct) }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def flatten_oneofs_hash(object)
|
199
|
+
object.map { |h|
|
200
|
+
h[:oneOf] || h
|
201
|
+
}.flatten
|
202
|
+
end
|
203
|
+
|
245
204
|
def path_schema_stub
|
246
205
|
@path_schema_stub ||=
|
247
|
-
|
206
|
+
make_object_schema(metadata_keys: METADATA_KEYS | %i[style explode])[:properties].map do |k, v|
|
248
207
|
# ensure_uncomplicated(k, v)
|
249
208
|
{
|
250
209
|
name: k,
|
251
|
-
schema: v.reject { |key, _| %i[required nullable].include?(key) },
|
252
|
-
required: object_schema[:required].include?(k) || false
|
253
|
-
|
210
|
+
schema: v.reject { |key, _| %i[required nullable explode style].include?(key) },
|
211
|
+
required: object_schema[:required].include?(k) || false,
|
212
|
+
style: v[:style],
|
213
|
+
explode: v[:explode]
|
214
|
+
}.reject { |_, v2| v2.nil? }
|
254
215
|
end
|
255
216
|
end
|
256
217
|
|