sober_swag 0.1.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 +7 -0
- data/.github/workflows/ruby.yml +33 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/Rakefile +8 -0
- data/bin/console +38 -0
- data/bin/setup +8 -0
- data/example/.gitignore +24 -0
- data/example/.ruby-version +1 -0
- data/example/Gemfile +42 -0
- data/example/Gemfile.lock +212 -0
- data/example/README.md +24 -0
- data/example/Rakefile +6 -0
- data/example/app/controllers/application_controller.rb +2 -0
- data/example/app/controllers/concerns/.keep +0 -0
- data/example/app/controllers/people_controller.rb +74 -0
- data/example/app/jobs/application_job.rb +7 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/concerns/.keep +0 -0
- data/example/app/models/person.rb +2 -0
- data/example/bin/bundle +114 -0
- data/example/bin/rails +9 -0
- data/example/bin/rake +9 -0
- data/example/bin/setup +33 -0
- data/example/bin/spring +17 -0
- data/example/config/application.rb +37 -0
- data/example/config/boot.rb +4 -0
- data/example/config/credentials.yml.enc +1 -0
- data/example/config/database.yml +25 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +44 -0
- data/example/config/environments/production.rb +91 -0
- data/example/config/environments/test.rb +38 -0
- data/example/config/initializers/application_controller_renderer.rb +8 -0
- data/example/config/initializers/backtrace_silencers.rb +7 -0
- data/example/config/initializers/cors.rb +16 -0
- data/example/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/config/initializers/inflections.rb +16 -0
- data/example/config/initializers/mime_types.rb +4 -0
- data/example/config/initializers/wrap_parameters.rb +14 -0
- data/example/config/locales/en.yml +33 -0
- data/example/config/puma.rb +38 -0
- data/example/config/routes.rb +6 -0
- data/example/config/spring.rb +6 -0
- data/example/config.ru +5 -0
- data/example/db/migrate/20200311152021_create_people.rb +12 -0
- data/example/db/schema.rb +23 -0
- data/example/db/seeds.rb +7 -0
- data/example/lib/tasks/.keep +0 -0
- data/example/log/.keep +0 -0
- data/example/person.json +4 -0
- data/example/public/robots.txt +1 -0
- data/example/test/controllers/.keep +0 -0
- data/example/test/fixtures/.keep +0 -0
- data/example/test/fixtures/files/.keep +0 -0
- data/example/test/fixtures/people.yml +11 -0
- data/example/test/integration/.keep +0 -0
- data/example/test/models/.keep +0 -0
- data/example/test/models/person_test.rb +7 -0
- data/example/test/test_helper.rb +13 -0
- data/example/tmp/.keep +0 -0
- data/example/vendor/.keep +0 -0
- data/lib/sober_swag/blueprint/field.rb +36 -0
- data/lib/sober_swag/blueprint/field_syntax.rb +17 -0
- data/lib/sober_swag/blueprint/view.rb +44 -0
- data/lib/sober_swag/blueprint.rb +113 -0
- data/lib/sober_swag/compiler/error.rb +5 -0
- data/lib/sober_swag/compiler/path.rb +80 -0
- data/lib/sober_swag/compiler/paths.rb +54 -0
- data/lib/sober_swag/compiler/type.rb +235 -0
- data/lib/sober_swag/compiler.rb +107 -0
- data/lib/sober_swag/controller/route.rb +136 -0
- data/lib/sober_swag/controller/undefined_body_error.rb +6 -0
- data/lib/sober_swag/controller/undefined_path_error.rb +6 -0
- data/lib/sober_swag/controller/undefined_query_error.rb +6 -0
- data/lib/sober_swag/controller.rb +157 -0
- data/lib/sober_swag/nodes/array.rb +30 -0
- data/lib/sober_swag/nodes/attribute.rb +31 -0
- data/lib/sober_swag/nodes/base.rb +51 -0
- data/lib/sober_swag/nodes/binary.rb +44 -0
- data/lib/sober_swag/nodes/enum.rb +28 -0
- data/lib/sober_swag/nodes/list.rb +40 -0
- data/lib/sober_swag/nodes/nullable_primitive.rb +6 -0
- data/lib/sober_swag/nodes/object.rb +12 -0
- data/lib/sober_swag/nodes/one_of.rb +12 -0
- data/lib/sober_swag/nodes/primitive.rb +29 -0
- data/lib/sober_swag/nodes/sum.rb +6 -0
- data/lib/sober_swag/nodes.rb +20 -0
- data/lib/sober_swag/parser.rb +73 -0
- data/lib/sober_swag/path/integer.rb +21 -0
- data/lib/sober_swag/path/lit.rb +41 -0
- data/lib/sober_swag/path/literal.rb +29 -0
- data/lib/sober_swag/path/param.rb +33 -0
- data/lib/sober_swag/path.rb +8 -0
- data/lib/sober_swag/serializer/array.rb +21 -0
- data/lib/sober_swag/serializer/base.rb +38 -0
- data/lib/sober_swag/serializer/conditional.rb +49 -0
- data/lib/sober_swag/serializer/field_list.rb +44 -0
- data/lib/sober_swag/serializer/mapped.rb +29 -0
- data/lib/sober_swag/serializer/optional.rb +29 -0
- data/lib/sober_swag/serializer/primitive.rb +15 -0
- data/lib/sober_swag/serializer.rb +23 -0
- data/lib/sober_swag/types.rb +5 -0
- data/lib/sober_swag/version.rb +5 -0
- data/lib/sober_swag.rb +29 -0
- data/sober_swag.gemspec +40 -0
- metadata +269 -0
data/example/log/.keep
ADDED
|
File without changes
|
data/example/person.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
|
2
|
+
|
|
3
|
+
one:
|
|
4
|
+
first_name: MyText
|
|
5
|
+
last_name: MyText
|
|
6
|
+
date_of_birth: 2020-03-11 09:20:21
|
|
7
|
+
|
|
8
|
+
two:
|
|
9
|
+
first_name: MyText
|
|
10
|
+
last_name: MyText
|
|
11
|
+
date_of_birth: 2020-03-11 09:20:21
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
2
|
+
require_relative '../config/environment'
|
|
3
|
+
require 'rails/test_help'
|
|
4
|
+
|
|
5
|
+
class ActiveSupport::TestCase
|
|
6
|
+
# Run tests in parallel with specified workers
|
|
7
|
+
parallelize(workers: :number_of_processors)
|
|
8
|
+
|
|
9
|
+
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
|
10
|
+
fixtures :all
|
|
11
|
+
|
|
12
|
+
# Add more helper methods to be used by all tests here...
|
|
13
|
+
end
|
data/example/tmp/.keep
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Blueprint
|
|
3
|
+
class Field
|
|
4
|
+
def initialize(name, serializer, from: nil, &block)
|
|
5
|
+
@name = name
|
|
6
|
+
@root_serializer = serializer
|
|
7
|
+
@from = from
|
|
8
|
+
@block = block
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
def serializer
|
|
14
|
+
@serializer ||= @root_serializer.serializer.via_map(&transform_proc)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def transform_proc
|
|
20
|
+
if @block
|
|
21
|
+
@block
|
|
22
|
+
else
|
|
23
|
+
key = @from || @name
|
|
24
|
+
proc do |object, _|
|
|
25
|
+
if object.respond_to?(key)
|
|
26
|
+
object.public_send(key)
|
|
27
|
+
else
|
|
28
|
+
object[key]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Blueprint
|
|
3
|
+
##
|
|
4
|
+
# Syntax for definitions that can add fields.
|
|
5
|
+
module FieldSyntax
|
|
6
|
+
def field(name, serializer, from: nil, &block)
|
|
7
|
+
add_field!(Field.new(name, serializer, from: from, &block))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Given a symbol to this, we will use a primitive name
|
|
12
|
+
def primitive(name)
|
|
13
|
+
SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(name))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Blueprint
|
|
3
|
+
class View
|
|
4
|
+
|
|
5
|
+
def self.define(name, base_fields, &block)
|
|
6
|
+
self.new(name, base_fields).tap do |view|
|
|
7
|
+
view.instance_eval(&block)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NestingError < Error; end;
|
|
12
|
+
|
|
13
|
+
include FieldSyntax
|
|
14
|
+
|
|
15
|
+
def initialize(name, base_fields = [])
|
|
16
|
+
@name = name
|
|
17
|
+
@fields = base_fields.dup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :name, :fields
|
|
21
|
+
|
|
22
|
+
def except!(name)
|
|
23
|
+
@fields.select! { |f| f.name != name }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def view(*)
|
|
27
|
+
raise NestingError, 'no views in views'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_field!(field)
|
|
31
|
+
@fields << field
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Get the serializer defined by this view.
|
|
36
|
+
# WARNING: Don't add more fields after you call this.
|
|
37
|
+
def serializer
|
|
38
|
+
@serializer ||=
|
|
39
|
+
SoberSwag::Serializer::FieldList.new(fields)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'sober_swag/serializer'
|
|
2
|
+
|
|
3
|
+
module SoberSwag
|
|
4
|
+
##
|
|
5
|
+
# Create a serializer that is heavily inspired by the "Blueprinter" library.
|
|
6
|
+
# This allows you to make "views" and such inside.
|
|
7
|
+
#
|
|
8
|
+
# Under the hood, this is actually all based on {SoberSwag::Serialzier::Base}.
|
|
9
|
+
class Blueprint
|
|
10
|
+
autoload(:Field, 'sober_swag/blueprint/field')
|
|
11
|
+
autoload(:FieldSyntax, 'sober_swag/blueprint/field_syntax')
|
|
12
|
+
autoload(:View, 'sober_swag/blueprint/view')
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Use a Blueprint to define a new serializer.
|
|
16
|
+
# It will be based on {SoberSwag::Serializer::Base}.
|
|
17
|
+
#
|
|
18
|
+
# An example is illustrative:
|
|
19
|
+
#
|
|
20
|
+
# PersonSerializer = SoberSwag::Blueprint.define do
|
|
21
|
+
# field :id, primitive(:Integer)
|
|
22
|
+
# field :name, primtive(:String).optional
|
|
23
|
+
#
|
|
24
|
+
# view :complex do
|
|
25
|
+
# field :age, primitive(:Integer)
|
|
26
|
+
# field :title, primitive(:String)
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# Note: This currently will generate a new *class* that does serialization.
|
|
31
|
+
# However, this is only a hack to get rid of the weird naming issue when
|
|
32
|
+
# generating swagger from dry structs: their section of the schema area
|
|
33
|
+
# is defined by their *Ruby Class Name*. In the future, if we get rid of this,
|
|
34
|
+
# we might be able to keep this on the value-level, in which case {#define}
|
|
35
|
+
# can simply return an *instance* of SoberSwag::Serializer that does
|
|
36
|
+
# the correct thing, with the name you give it. This works for now, though.
|
|
37
|
+
def self.define(&block)
|
|
38
|
+
self.new.tap { |o|
|
|
39
|
+
o.instance_eval(&block)
|
|
40
|
+
}.serializer_class
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(base_fields = [])
|
|
44
|
+
@fields = base_fields.dup
|
|
45
|
+
@views = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_reader :fields, :views
|
|
49
|
+
|
|
50
|
+
include FieldSyntax
|
|
51
|
+
|
|
52
|
+
def add_field!(field)
|
|
53
|
+
@fields << field
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def view(name, &block)
|
|
57
|
+
@views << View.define(name, fields, &block)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Instead of generating a new value-level construct,
|
|
62
|
+
# this will generate a new *class*. This is so that you can
|
|
63
|
+
# name this blueprint, and have the views named as sub-classes.
|
|
64
|
+
# This is important as the type compiler uses class names
|
|
65
|
+
# for generating refs.
|
|
66
|
+
def serializer_class
|
|
67
|
+
@serializer_class ||= make_serializer_class!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def make_serializer_class!
|
|
73
|
+
# Klass we'll use
|
|
74
|
+
klass = Class.new(SoberSwag::Serializer::Base)
|
|
75
|
+
# The actual serialization logic is defined in a field list
|
|
76
|
+
base_serializer = SoberSwag::Serializer::FieldList.new(fields)
|
|
77
|
+
# WhateverBlueprint::Base is now used as the name for a ref
|
|
78
|
+
klass.const_set(:Base, base_serializer.type)
|
|
79
|
+
final_serializer = views.reduce(base_serializer) do |base, view|
|
|
80
|
+
view_serializer = view.serializer
|
|
81
|
+
# If we have a view :foo, its type is named
|
|
82
|
+
# WhateverBlueprint::Foo
|
|
83
|
+
klass.const_set(view.name.to_s.classify, view_serializer.type)
|
|
84
|
+
SoberSwag::Serializer::Conditional.new(
|
|
85
|
+
proc do |object, options|
|
|
86
|
+
if options[:view].to_s == view.name.to_s
|
|
87
|
+
[:left, object]
|
|
88
|
+
else
|
|
89
|
+
[:right, object]
|
|
90
|
+
end
|
|
91
|
+
end,
|
|
92
|
+
view_serializer,
|
|
93
|
+
base
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
klass.send(:define_method, :serialize) do |object, options = {}|
|
|
97
|
+
final_serializer.serialize(object, options)
|
|
98
|
+
end
|
|
99
|
+
klass.send(:define_method, :type) do
|
|
100
|
+
final_serializer.type
|
|
101
|
+
end
|
|
102
|
+
klass.send(:define_singleton_method, :type) do
|
|
103
|
+
final_serializer.type
|
|
104
|
+
end
|
|
105
|
+
klass.send(:define_singleton_method, :serializer) do
|
|
106
|
+
klass.new
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
klass
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Compiler
|
|
3
|
+
##
|
|
4
|
+
# Compile a singular path, and that's it.
|
|
5
|
+
# Only handles the actual body.
|
|
6
|
+
class Path
|
|
7
|
+
##
|
|
8
|
+
# @param route [SoberSwag::Controller::Route] a route to use
|
|
9
|
+
# @param compiler [SoberSwag::Compiler] the compiler to use for type compilation
|
|
10
|
+
def initialize(route, compiler)
|
|
11
|
+
@route = route
|
|
12
|
+
@compiler = compiler
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :route, :compiler
|
|
16
|
+
|
|
17
|
+
def schema
|
|
18
|
+
base = {}
|
|
19
|
+
base[:summary] = route.summary if route.summary
|
|
20
|
+
base[:description] = route.description if route.description
|
|
21
|
+
base[:parameters] = params if params.any?
|
|
22
|
+
base[:responses] = responses
|
|
23
|
+
base[:requestBody] = request_body if request_body
|
|
24
|
+
base
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def responses
|
|
28
|
+
route.response_serializers.map { |status, serializer|
|
|
29
|
+
[
|
|
30
|
+
status.to_s,
|
|
31
|
+
{
|
|
32
|
+
description: route.response_descriptions[status],
|
|
33
|
+
content: {
|
|
34
|
+
'application/json': {
|
|
35
|
+
schema: compiler.response_for(
|
|
36
|
+
serializer.respond_to?(:new) ? serializer.new.type : serializer.type
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}.to_h
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def params
|
|
46
|
+
query_params + path_params
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def query_params
|
|
50
|
+
if route.query_params_class
|
|
51
|
+
compiler.query_params_for(route.query_params_class)
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def path_params
|
|
58
|
+
if route.path_params_class
|
|
59
|
+
compiler.path_params_for(route.path_params_class)
|
|
60
|
+
else
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def request_body
|
|
66
|
+
return nil unless route.request_body_class
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
required: true,
|
|
70
|
+
content: {
|
|
71
|
+
'application/json': {
|
|
72
|
+
schema: compiler.body_for(route.request_body_class)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Compiler
|
|
3
|
+
##
|
|
4
|
+
# Compile multiple routes into a paths set.
|
|
5
|
+
class Paths
|
|
6
|
+
def initialize
|
|
7
|
+
@routes = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add_route(route)
|
|
11
|
+
@routes << route
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def grouped_paths
|
|
15
|
+
routes.group_by(&:path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Slightly weird method that gives you a compiled
|
|
20
|
+
# paths list. Since this is only a compiler for paths,
|
|
21
|
+
# it has *no idea* how to handle types. So, it takes a compiler
|
|
22
|
+
# which it will use to do that for it.
|
|
23
|
+
def paths_list(compiler)
|
|
24
|
+
grouped_paths.transform_values do |values|
|
|
25
|
+
values.map { |route|
|
|
26
|
+
[route.method, compile_route(route, compiler)]
|
|
27
|
+
}.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Get a list of all types we discovered when compiling
|
|
33
|
+
# the paths.
|
|
34
|
+
def found_types
|
|
35
|
+
return enum_for(:found_types) unless block_given?
|
|
36
|
+
|
|
37
|
+
routes.each do |route|
|
|
38
|
+
%i[body_class query_class path_params_class].each do |k|
|
|
39
|
+
yield route.public_send(k) if route.public_send(k)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_reader :routes
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def compile_route(route, compiler)
|
|
49
|
+
SoberSwag::Compiler::Path.new(route, compiler).schema
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
module SoberSwag
|
|
2
|
+
class Compiler
|
|
3
|
+
##
|
|
4
|
+
# A compiler for DRY-Struct data types, essentially.
|
|
5
|
+
# It only consumes one type at a time.
|
|
6
|
+
class Type
|
|
7
|
+
class << self
|
|
8
|
+
def get_ref(klass)
|
|
9
|
+
"#/components/schemas/#{safe_name(klass)}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def safe_name(klass)
|
|
13
|
+
klass.to_s.gsub('::', '.')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def primitive?(value)
|
|
17
|
+
primitive_def(value) != nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def primitive_def(value)
|
|
21
|
+
return nil unless value.is_a?(Class)
|
|
22
|
+
|
|
23
|
+
if (name = primitive_name(value))
|
|
24
|
+
{ type: name }
|
|
25
|
+
elsif value == Date
|
|
26
|
+
{ type: 'string', format: 'date' }
|
|
27
|
+
elsif [Time, DateTime].any?(&value.ancestors.method(:include?))
|
|
28
|
+
{ type: 'string', format: 'date-time' }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def primitive_name(value)
|
|
33
|
+
return 'null' if value == NilClass
|
|
34
|
+
return 'integer' if value == Integer
|
|
35
|
+
return 'number' if value == Float
|
|
36
|
+
return 'string' if value == String
|
|
37
|
+
return 'boolean' if [TrueClass, FalseClass].include?(value)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class TooComplicatedError < ::SoberSwag::Compiler::Error; end
|
|
42
|
+
class TooComplicatedForPathError < TooComplicatedError; end
|
|
43
|
+
class TooComplicatedForQueryError < TooComplicatedError; end
|
|
44
|
+
|
|
45
|
+
def initialize(type)
|
|
46
|
+
@type = type
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :type
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Is this type standalone, IE, worth serializing on its own
|
|
53
|
+
# in the schemas section of our schema?
|
|
54
|
+
def standalone?
|
|
55
|
+
type.is_a?(Class)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def object_schema
|
|
59
|
+
@object_schema ||=
|
|
60
|
+
normalize(parsed_type).cata(&method(:to_object_schema))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def schema_stub
|
|
64
|
+
@schema_stub ||= generate_schema_stub
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def path_schema
|
|
68
|
+
path_schema_stub.map { |e| e.merge(in: :path) }
|
|
69
|
+
rescue TooComplicatedError => e
|
|
70
|
+
raise TooComplicatedForPathError, e.message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def query_schema
|
|
74
|
+
path_schema_stub.map { |e| e.merge(in: :query) }
|
|
75
|
+
rescue TooComplicatedError => e
|
|
76
|
+
raise TooComplicatedForQueryError, e.message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ref_name
|
|
80
|
+
self.class.safe_name(type)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def found_types
|
|
84
|
+
@found_types ||=
|
|
85
|
+
begin
|
|
86
|
+
(_, found_types) = parsed_result
|
|
87
|
+
found_types
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parsed_type
|
|
92
|
+
@parsed_type ||=
|
|
93
|
+
begin
|
|
94
|
+
(parsed, _) = parsed_result
|
|
95
|
+
parsed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parsed_result
|
|
100
|
+
@parsed_result ||= Parser.new(type_for_parser).run_parser
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def eql?(other)
|
|
104
|
+
other.class == self.class && other.type == type
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def hash
|
|
108
|
+
[self.class, type].hash
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def generate_schema_stub
|
|
114
|
+
return self.class.primitive_def(type) if self.class.primitive?(type)
|
|
115
|
+
|
|
116
|
+
case type
|
|
117
|
+
when Class
|
|
118
|
+
{ :$ref => self.class.get_ref(type) }
|
|
119
|
+
when Dry::Types::Constrained
|
|
120
|
+
self.class.new(type.type).schema_stub
|
|
121
|
+
when Dry::Types::Array::Member
|
|
122
|
+
{ type: :array, items: self.class.new(type.member).schema_stub }
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "Cannot generate a schema stub for #{type} (#{type.class})"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def type_for_parser
|
|
129
|
+
if type.is_a?(Class)
|
|
130
|
+
type.schema.type
|
|
131
|
+
else
|
|
132
|
+
# Probably a constrained array
|
|
133
|
+
type
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize(object)
|
|
138
|
+
object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def rewrite_sums(object)
|
|
142
|
+
case object
|
|
143
|
+
in Nodes::Sum[Nodes::OneOf[*lhs], Nodes::OneOf[*rhs]]
|
|
144
|
+
Nodes::OneOf.new(lhs + rhs)
|
|
145
|
+
in Nodes::Sum[Nodes::OneOf[*args], rhs]
|
|
146
|
+
Nodes::OneOf.new(args + [rhs])
|
|
147
|
+
in Nodes::Sum[lhs, Nodes::OneOf[*args]]
|
|
148
|
+
Nodes::OneOf.new([lhs] + args)
|
|
149
|
+
in Nodes::Sum[lhs, rhs]
|
|
150
|
+
Nodes::OneOf.new([lhs, rhs])
|
|
151
|
+
else
|
|
152
|
+
object
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def flatten_one_ofs(object)
|
|
157
|
+
case object
|
|
158
|
+
in Nodes::OneOf[*args]
|
|
159
|
+
Nodes::OneOf.new(args.uniq)
|
|
160
|
+
else
|
|
161
|
+
object
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def to_object_schema(object)
|
|
166
|
+
case object
|
|
167
|
+
in Nodes::List[element]
|
|
168
|
+
{
|
|
169
|
+
type: :array,
|
|
170
|
+
items: element
|
|
171
|
+
}
|
|
172
|
+
in Nodes::Enum[values]
|
|
173
|
+
{
|
|
174
|
+
type: :string,
|
|
175
|
+
enum: values
|
|
176
|
+
}
|
|
177
|
+
in Nodes::OneOf[{ type: 'null' }, b]
|
|
178
|
+
b.merge(nullable: true)
|
|
179
|
+
in Nodes::OneOf[a, { type: 'null' }]
|
|
180
|
+
a.merge(nullable: true)
|
|
181
|
+
in Nodes::OneOf[*attrs] if attrs.include?(type: 'null')
|
|
182
|
+
{ oneOf: attrs.reject { |e| e[:type] == 'null' }, nullable: true }
|
|
183
|
+
in Nodes::OneOf[*cases]
|
|
184
|
+
{ oneOf: cases }
|
|
185
|
+
in Nodes::Object[*attrs]
|
|
186
|
+
# openAPI requires that you give a list of required attributes
|
|
187
|
+
# (which IMO is the *totally* wrong thing to do but whatever)
|
|
188
|
+
# so we must do this garbage
|
|
189
|
+
required = attrs.filter {|(_, b)| b[:required] }.map(&:first)
|
|
190
|
+
{
|
|
191
|
+
type: :object,
|
|
192
|
+
properties: attrs.map { |(a,b)|
|
|
193
|
+
[a, b.select { |k, _| k != :required }]
|
|
194
|
+
}.to_h,
|
|
195
|
+
required: required
|
|
196
|
+
}
|
|
197
|
+
in Nodes::Attribute[name, true, value]
|
|
198
|
+
[name, value.merge(required: true)]
|
|
199
|
+
in Nodes::Attribute[name, false, value]
|
|
200
|
+
[name, value]
|
|
201
|
+
# can't match on value directly as ruby uses `===` to match,
|
|
202
|
+
# and classes use `===` to mean `is an instance of`, as
|
|
203
|
+
# opposed to direct equality lmao
|
|
204
|
+
in Nodes::Primitive[value:] if self.class.primitive?(value)
|
|
205
|
+
self.class.primitive_def(value)
|
|
206
|
+
in Nodes::Primitive[value:]
|
|
207
|
+
{ '$ref': self.class.get_ref(value) }
|
|
208
|
+
else
|
|
209
|
+
raise ArgumentError, "Got confusing node #{object} (#{object.class})"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def path_schema_stub
|
|
214
|
+
@path_schema_stub ||=
|
|
215
|
+
object_schema[:properties].map do |k, v|
|
|
216
|
+
ensure_uncomplicated(k, v)
|
|
217
|
+
{
|
|
218
|
+
name: k,
|
|
219
|
+
schema: v.reject { |k, _| %i[required nullable].include?(k) },
|
|
220
|
+
allowEmptyValue: !object_schema[:required].include?(k) || !!v[:nullable], # if it's required, no empties, but if *nullabe*, empties are okay
|
|
221
|
+
required: object_schema[:required].include?(k) || false,
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def ensure_uncomplicated(key, value)
|
|
227
|
+
return if value[:type]
|
|
228
|
+
raise TooComplicatedError, <<~ERROR
|
|
229
|
+
Property #{key} has object-schema #{value}, but this type of param should be simple (IE a primitive of some kind)
|
|
230
|
+
ERROR
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|