sober_swag 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|