modern 0.4.2
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/.editorconfig +8 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +173 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +36 -0
- data/Rakefile +10 -0
- data/TODOS.md +4 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/example/Gemfile +9 -0
- data/example/Gemfile.lock +102 -0
- data/example/config.ru +11 -0
- data/example/example.rb +19 -0
- data/lib/modern/app/error_handling.rb +45 -0
- data/lib/modern/app/request_handling/input_handling.rb +65 -0
- data/lib/modern/app/request_handling/output_handling.rb +54 -0
- data/lib/modern/app/request_handling/request_container.rb +55 -0
- data/lib/modern/app/request_handling.rb +70 -0
- data/lib/modern/app/router.rb +27 -0
- data/lib/modern/app/trie_router.rb +37 -0
- data/lib/modern/app.rb +82 -0
- data/lib/modern/capsule.rb +17 -0
- data/lib/modern/configuration.rb +16 -0
- data/lib/modern/core_ext/array.rb +23 -0
- data/lib/modern/core_ext/hash.rb +17 -0
- data/lib/modern/descriptor/content.rb +14 -0
- data/lib/modern/descriptor/converters/input/base.rb +29 -0
- data/lib/modern/descriptor/converters/input/json.rb +21 -0
- data/lib/modern/descriptor/converters/output/base.rb +25 -0
- data/lib/modern/descriptor/converters/output/json.rb +48 -0
- data/lib/modern/descriptor/converters/output/yaml.rb +21 -0
- data/lib/modern/descriptor/converters.rb +4 -0
- data/lib/modern/descriptor/core.rb +63 -0
- data/lib/modern/descriptor/info.rb +27 -0
- data/lib/modern/descriptor/parameters.rb +149 -0
- data/lib/modern/descriptor/request_body.rb +13 -0
- data/lib/modern/descriptor/response.rb +26 -0
- data/lib/modern/descriptor/route.rb +93 -0
- data/lib/modern/descriptor/security.rb +104 -0
- data/lib/modern/descriptor/server.rb +12 -0
- data/lib/modern/descriptor.rb +15 -0
- data/lib/modern/doc_generator/open_api3/operations.rb +114 -0
- data/lib/modern/doc_generator/open_api3/paths.rb +24 -0
- data/lib/modern/doc_generator/open_api3/schema_default_types.rb +50 -0
- data/lib/modern/doc_generator/open_api3/schemas.rb +171 -0
- data/lib/modern/doc_generator/open_api3/security_schemes.rb +15 -0
- data/lib/modern/doc_generator/open_api3.rb +141 -0
- data/lib/modern/dsl/info.rb +39 -0
- data/lib/modern/dsl/response_builder.rb +41 -0
- data/lib/modern/dsl/root.rb +38 -0
- data/lib/modern/dsl/route_builder.rb +130 -0
- data/lib/modern/dsl/scope.rb +144 -0
- data/lib/modern/dsl/scope_settings.rb +39 -0
- data/lib/modern/dsl.rb +14 -0
- data/lib/modern/errors/error.rb +7 -0
- data/lib/modern/errors/setup_errors.rb +11 -0
- data/lib/modern/errors/web_errors.rb +83 -0
- data/lib/modern/errors.rb +3 -0
- data/lib/modern/redirect.rb +30 -0
- data/lib/modern/request.rb +34 -0
- data/lib/modern/response.rb +39 -0
- data/lib/modern/services.rb +17 -0
- data/lib/modern/struct.rb +25 -0
- data/lib/modern/types.rb +41 -0
- data/lib/modern/util/header_parsing.rb +27 -0
- data/lib/modern/util/trie_node.rb +53 -0
- data/lib/modern/version.rb +6 -0
- data/lib/modern.rb +8 -0
- data/manual/01-why_modern.md +115 -0
- data/modern.gemspec +54 -0
- metadata +439 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/struct"
|
4
|
+
require "modern/errors/web_errors"
|
5
|
+
|
6
|
+
module Modern
|
7
|
+
module Descriptor
|
8
|
+
module Parameters
|
9
|
+
class Base < Modern::Struct
|
10
|
+
# TODO: maybe do something behind-the-scenes where `required` is
|
11
|
+
# expressed as part of dry-types (| Types::Nil); we need
|
12
|
+
# the field for easily generating a doc later, but we could parse
|
13
|
+
# it out of the type if we had to.
|
14
|
+
attribute :name, Types::Strict::String
|
15
|
+
attribute :type, Types::Type
|
16
|
+
attribute :description, Types::Strict::String.optional.default(nil)
|
17
|
+
attribute :deprecated, Types::Strict::Bool.default(false)
|
18
|
+
|
19
|
+
def friendly_name
|
20
|
+
name
|
21
|
+
end
|
22
|
+
|
23
|
+
def retrieve(request, route_captures)
|
24
|
+
ret = do_retrieve(request, route_captures)
|
25
|
+
|
26
|
+
raise Errors::BadRequestError, "Invalid/missing parameter '#{friendly_name}'." \
|
27
|
+
if required && ret.nil?
|
28
|
+
|
29
|
+
begin
|
30
|
+
ret.nil? ? nil : type[ret]
|
31
|
+
rescue StandardError => _err
|
32
|
+
raise Errors::InternalServiceError,
|
33
|
+
"Couldn't interpret the value provided for parameter '#{friendly_name}': #{ret}."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def openapi3_in
|
38
|
+
self.class.name.split("::").last.downcase
|
39
|
+
end
|
40
|
+
|
41
|
+
def do_retrieve(_request, _route_captures)
|
42
|
+
raise "#{self.class.name}#do_retrieve(request, route_captures) must be implemented."
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_openapi3(is_api_key = false)
|
46
|
+
{
|
47
|
+
name: friendly_name,
|
48
|
+
in: openapi3_in,
|
49
|
+
required: !is_api_key ? required : nil,
|
50
|
+
description: !is_api_key ? description : nil,
|
51
|
+
deprecated: !is_api_key ? deprecated : nil
|
52
|
+
}.compact
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Path < Base
|
57
|
+
# TODO: add 'matrix' and 'label'
|
58
|
+
attribute :style, Types::Coercible::String.default("simple").enum("simple")
|
59
|
+
|
60
|
+
def required
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def do_retrieve(_request, route_captures)
|
65
|
+
route_captures[name]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Cookie < Base
|
70
|
+
attribute :cookie_name, Types::Coercible::String
|
71
|
+
attribute :style, Types::Coercible::String.default("form").enum("form")
|
72
|
+
attribute :required, Types::Strict::Bool.default(false)
|
73
|
+
|
74
|
+
def friendly_name
|
75
|
+
cookie_name
|
76
|
+
end
|
77
|
+
|
78
|
+
def do_retrieve(request, _route_captures = nil)
|
79
|
+
request.cookies[cookie_name]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Header < Base
|
84
|
+
attribute :header_name, Types::Coercible::String
|
85
|
+
attribute :style, Types::Coercible::String.default("simple").enum("simple")
|
86
|
+
attribute :required, Types::Strict::Bool.default(false)
|
87
|
+
|
88
|
+
attr_reader :rack_env_key
|
89
|
+
|
90
|
+
def initialize(fields)
|
91
|
+
super(fields)
|
92
|
+
|
93
|
+
@rack_env_key = "HTTP_" + header_name.upcase.tr("-", "_")
|
94
|
+
end
|
95
|
+
|
96
|
+
def friendly_name
|
97
|
+
header_name
|
98
|
+
end
|
99
|
+
|
100
|
+
def do_retrieve(request, _route_captures = nil)
|
101
|
+
request.env[@rack_env_key]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Query < Base
|
106
|
+
# TODO: add 'space_delimited', 'pipe_delimited', 'deep_object'
|
107
|
+
attribute :style, Types::Coercible::String.default("form").enum("form")
|
108
|
+
attribute :required, Types::Strict::Bool.default(false)
|
109
|
+
|
110
|
+
attribute :allow_empty_value, Types::Strict::Bool.default(false)
|
111
|
+
|
112
|
+
attr_reader :parser
|
113
|
+
|
114
|
+
def initialize(fields)
|
115
|
+
super(fields)
|
116
|
+
|
117
|
+
@query_parser = Rack::QueryParser.make_default(99, 99)
|
118
|
+
end
|
119
|
+
|
120
|
+
def do_retrieve(request, _route_captures = nil)
|
121
|
+
@query_parser.parse_query(request.query_string)[name]
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_openapi3(is_api_key = false)
|
125
|
+
super.merge(
|
126
|
+
allowEmptyValue: !is_api_key ? description : nil,
|
127
|
+
).compact
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.from_inputs(name, parameter_type, opts)
|
132
|
+
opts = opts.merge(name: name.to_s)
|
133
|
+
|
134
|
+
case parameter_type.to_sym
|
135
|
+
when :path
|
136
|
+
Modern::Descriptor::Parameters::Path.new(opts)
|
137
|
+
when :cookie
|
138
|
+
Modern::Descriptor::Parameters::Cookie.new(opts)
|
139
|
+
when :header
|
140
|
+
Modern::Descriptor::Parameters::Header.new(opts)
|
141
|
+
when :query
|
142
|
+
Modern::Descriptor::Parameters::Query.new(opts)
|
143
|
+
else
|
144
|
+
raise "Unrecognized parameter type '#{parameter_type}'.'"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/struct"
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module Descriptor
|
7
|
+
class RequestBody < Modern::Struct
|
8
|
+
attribute :type, (Types::Type | Types::Struct).optional.default(nil)
|
9
|
+
attribute :required, Types::Strict::Bool.default(false)
|
10
|
+
attribute :description, Types::Strict::String.optional.default(nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/struct"
|
4
|
+
require "modern/descriptor/content"
|
5
|
+
require "modern/descriptor/parameters"
|
6
|
+
|
7
|
+
module Modern
|
8
|
+
module Descriptor
|
9
|
+
class Response < Modern::Struct
|
10
|
+
# TODO: figure out a good way to handle header responses
|
11
|
+
# This is in part an API response type of thing; how do we ensure that
|
12
|
+
# an action defines the header that it says it's defining?
|
13
|
+
attribute :http_code, Types::Strict::Int | Types.Value(:default)
|
14
|
+
attribute :description, Types::Strict::String.default("No description provided.")
|
15
|
+
attribute :content, Types.array_of(Content)
|
16
|
+
|
17
|
+
attr_reader :content_by_type
|
18
|
+
|
19
|
+
def initialize(fields)
|
20
|
+
super
|
21
|
+
|
22
|
+
@content_by_type = content.map { |c| [c.media_type, c] }.to_h.freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "modern/struct"
|
6
|
+
|
7
|
+
require "modern/descriptor/converters"
|
8
|
+
require "modern/descriptor/response"
|
9
|
+
require "modern/descriptor/parameters"
|
10
|
+
require "modern/descriptor/request_body"
|
11
|
+
require "modern/descriptor/security"
|
12
|
+
|
13
|
+
module Modern
|
14
|
+
module Descriptor
|
15
|
+
class Route < Modern::Struct
|
16
|
+
# TODO: define OpenAPI-style callbacks
|
17
|
+
TEMPLATE_TOKEN = %r|\{.+\}|
|
18
|
+
OPENAPI_CAPTURE = %r|/\{(?<name>.+?)\}|
|
19
|
+
|
20
|
+
attribute :id, Types::String
|
21
|
+
|
22
|
+
attribute :http_method, Types::HttpMethod
|
23
|
+
attribute :path, Types::HttpPath
|
24
|
+
|
25
|
+
attribute :summary, Types::Strict::String.optional.default(nil)
|
26
|
+
attribute :description, Types::Strict::String.optional.default(nil)
|
27
|
+
attribute :deprecated, Types::Strict::Bool.default(false)
|
28
|
+
attribute :tags, Types.array_of(Types::Coercible::String)
|
29
|
+
|
30
|
+
attribute :parameters, Types.array_of(Parameters::Base)
|
31
|
+
attribute :request_body, RequestBody.optional.default(nil)
|
32
|
+
attribute :responses, Types.array_of(Response)
|
33
|
+
|
34
|
+
attribute :input_converters, Types.array_of(Modern::Descriptor::Converters::Input::Base)
|
35
|
+
attribute :output_converters, Types.array_of(Modern::Descriptor::Converters::Output::Base)
|
36
|
+
|
37
|
+
attribute :security, Types.array_of(Security::Base)
|
38
|
+
attribute :helpers, Types.array_of(Types.Instance(Module))
|
39
|
+
attribute :action, Types::RouteAction
|
40
|
+
|
41
|
+
attr_reader :path_matcher
|
42
|
+
attr_reader :route_tokens
|
43
|
+
|
44
|
+
attr_reader :content_types
|
45
|
+
attr_reader :responses_by_code
|
46
|
+
|
47
|
+
attr_reader :input_converters_by_type
|
48
|
+
attr_reader :output_converters_by_type
|
49
|
+
|
50
|
+
attr_reader :request_container_class
|
51
|
+
|
52
|
+
def initialize(fields)
|
53
|
+
super
|
54
|
+
|
55
|
+
@path_matcher = Regexp.new("^" + path.gsub(OPENAPI_CAPTURE, "/(?<\\k<name>>[^/]+)") + "$")
|
56
|
+
@route_tokens =
|
57
|
+
path.sub(%r|^/|, "").split("/").map do |token|
|
58
|
+
TEMPLATE_TOKEN =~ token ? :templated : token
|
59
|
+
end
|
60
|
+
|
61
|
+
@content_types = responses.map { |r| r.content.map(&:media_type) }.flatten.to_set.freeze
|
62
|
+
@responses_by_code = responses.map { |r| [r.http_code, r] }.to_h.freeze
|
63
|
+
|
64
|
+
raise "Cannot create a Route without a Response where http_code = :default." \
|
65
|
+
unless @responses_by_code.key?(:default)
|
66
|
+
|
67
|
+
_nondefault_content = @content_types - @responses_by_code[:default].content.map(&:media_type).to_set
|
68
|
+
# TODO: figure out how to better validate these
|
69
|
+
# This might be a larger-scale problem. The DSL creates a route with this, and you can end
|
70
|
+
# up in a case where you try to add a new content type to another response type. This causes
|
71
|
+
# the commented test below to fail unless you defined the :default response, with that content
|
72
|
+
# type, higher in the DSL. We might need some sort of intermediate builder class, or a way to
|
73
|
+
# squelch this error somehow...
|
74
|
+
# require 'pry'; binding.pry
|
75
|
+
# raise "Missing content types in default HTTP response for #{id}: #{nondefault_content.to_a.join(', ')}" \
|
76
|
+
# unless nondefault_content.empty?
|
77
|
+
|
78
|
+
@input_converters_by_type = input_converters.map { |c| [c.media_type.downcase.strip, c] }.to_h.freeze
|
79
|
+
@output_converters_by_type = output_converters.map { |c| [c.media_type.downcase.strip, c] }.to_h.freeze
|
80
|
+
|
81
|
+
@request_container_class =
|
82
|
+
if helpers.empty?
|
83
|
+
Modern::App::RequestHandling::FullRequestContainer
|
84
|
+
else
|
85
|
+
rcc = Class.new(Modern::App::RequestHandling::FullRequestContainer)
|
86
|
+
helpers.each { |h| rcc.send(:include, h) }
|
87
|
+
|
88
|
+
rcc
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/descriptor/parameters"
|
4
|
+
require "modern/errors/web_errors"
|
5
|
+
require "modern/struct"
|
6
|
+
|
7
|
+
module Modern
|
8
|
+
module Descriptor
|
9
|
+
module Security
|
10
|
+
# TODO: implement OAuth2 security, with flow objects
|
11
|
+
# TODO: implement OpenID Connect security
|
12
|
+
|
13
|
+
# Securities in Modern allow for specifying the "plumbing" bits of the
|
14
|
+
# security in a predictable, unsurprising way. If you specify that you use
|
15
|
+
# HTTP authorization with the `Foobar` scheme--great. Modern finds the
|
16
|
+
# authorization header, checks to see if it's Foobar type, and retrieves
|
17
|
+
# the value that signifies its authentication. This value will be passed
|
18
|
+
# to the validator, which can determine whether or not its a valid bit of
|
19
|
+
# authentication.
|
20
|
+
#
|
21
|
+
# The idea is that, given that the validation gets access to a
|
22
|
+
# `PartialRequestContainer` that includes both the request and the
|
23
|
+
# application service set, it can connect to the auth server/user
|
24
|
+
# database/whatever and make sure it's atually a legitimate user. Since
|
25
|
+
# {Modern::Request} has a mutable store in {Modern::Request#local_store},
|
26
|
+
# the validator can then store a User object (or whatever) into it for use
|
27
|
+
# in the actual application.
|
28
|
+
class Base < Modern::Struct
|
29
|
+
attribute :name, Types::Strict::String
|
30
|
+
attribute :description, Types::Strict::String.optional.default(nil)
|
31
|
+
attribute :validation, Types::SecurityAction
|
32
|
+
|
33
|
+
def validate(container)
|
34
|
+
value = do_credential_fetch(container.request)
|
35
|
+
|
36
|
+
if value.nil?
|
37
|
+
false
|
38
|
+
else
|
39
|
+
!!container.instance_exec(value, &validation)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def do_credential_fetch(_request)
|
44
|
+
raise "#{self.class.name}#do_credential_fetch(request) must be implemented."
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_openapi3
|
48
|
+
{
|
49
|
+
description: description
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ApiKey < Base
|
55
|
+
attribute :parameter, Parameters::Query | Parameters::Header | Parameters::Cookie
|
56
|
+
|
57
|
+
def initialize(fields)
|
58
|
+
super
|
59
|
+
|
60
|
+
# I didn't want to rewrite all my parameter logic.
|
61
|
+
raise Modern::Errors::SetupError, "Parameter must not be 'required' (internal limitation)." \
|
62
|
+
if parameter.required
|
63
|
+
end
|
64
|
+
|
65
|
+
def do_credential_fetch(request)
|
66
|
+
parameter.do_retrieve(request)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_openapi3
|
70
|
+
parameter.to_openapi3(true).merge(type: "apiKey")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Http < Base
|
75
|
+
# aside: some people think that the Authorization field can support multiple sets of credentials,
|
76
|
+
# as RFC 7230 suggests that headers can be sent "multiple" times by using a comma to split them.
|
77
|
+
# however, this is for headers like Accept-Encoding. We don't need to split Authorization.
|
78
|
+
SPLITTER = %r,([^\s]+?)\s+(.*+),
|
79
|
+
|
80
|
+
attribute :scheme, Types::Strict::String
|
81
|
+
|
82
|
+
def do_credential_fetch(request)
|
83
|
+
header = request.env["HTTP_AUTHORIZATION"]
|
84
|
+
|
85
|
+
if header.nil?
|
86
|
+
nil
|
87
|
+
else
|
88
|
+
match = SPLITTER.match(header)
|
89
|
+
# yields #<MatchData "Bearer foo" 1:"Bearer" 2:"foo">
|
90
|
+
|
91
|
+
match[2].strip if !match.nil? && match[1].casecmp(scheme).zero?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_openapi3
|
96
|
+
super.merge(
|
97
|
+
type: "http",
|
98
|
+
scheme: scheme
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/struct"
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module Descriptor
|
7
|
+
class Server < Modern::Struct
|
8
|
+
attribute :url, Modern::Types::Strict::String
|
9
|
+
attribute :description, Modern::Types::Strict::String.optional.default(nil)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/struct'
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module Descriptor
|
7
|
+
def self.define(title, version, &block)
|
8
|
+
require 'modern/dsl'
|
9
|
+
|
10
|
+
Modern::DSL::Root.build(title, version, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Dir["#{__dir__}/descriptor/**/*.rb"].each { |f| require_relative f }
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modern
|
4
|
+
module DocGenerator
|
5
|
+
class OpenAPI3
|
6
|
+
module Operations
|
7
|
+
def _operation(route)
|
8
|
+
{
|
9
|
+
operationId: route.id,
|
10
|
+
summary: route.summary,
|
11
|
+
description: route.description,
|
12
|
+
deprecated: route.deprecated,
|
13
|
+
tags: route.tags.uniq,
|
14
|
+
|
15
|
+
security: route.security.map { |s| _security_requirement(s) },
|
16
|
+
|
17
|
+
parameters: route.parameters.map { |p| _parameter(p) },
|
18
|
+
requestBody: _request_body(route),
|
19
|
+
responses:
|
20
|
+
route.responses_by_code.map do |code, response|
|
21
|
+
[code, _response(response)]
|
22
|
+
end.to_h,
|
23
|
+
# TODO: implement callbacks
|
24
|
+
callbacks: nil
|
25
|
+
}.compact
|
26
|
+
end
|
27
|
+
|
28
|
+
def _security_requirement(security)
|
29
|
+
# TODO: OAuth2
|
30
|
+
ret = {}
|
31
|
+
ret[security.name] = []
|
32
|
+
|
33
|
+
ret
|
34
|
+
end
|
35
|
+
|
36
|
+
def _parameter(parameter)
|
37
|
+
# TODO: this should be in the parameter class, or that logic should be here.
|
38
|
+
|
39
|
+
parameter.to_openapi3.merge(
|
40
|
+
schema: _build_schema_value({}, {}, parameter.type)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def _request_body(route)
|
45
|
+
body = route.request_body
|
46
|
+
|
47
|
+
if body.nil?
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
schema =
|
51
|
+
if body.type.nil?
|
52
|
+
nil
|
53
|
+
elsif body.type.is_a?(Class) && body.type.ancestors.include?(Dry::Struct)
|
54
|
+
_struct_ref(body.type)
|
55
|
+
else
|
56
|
+
# TODO: make this less wasteful (see _response)
|
57
|
+
_build_schema_value({}, {}, body.type)
|
58
|
+
end
|
59
|
+
|
60
|
+
{
|
61
|
+
required: body.required,
|
62
|
+
content:
|
63
|
+
route.input_converters.map(&:media_type).map do |content_type|
|
64
|
+
[
|
65
|
+
content_type,
|
66
|
+
{
|
67
|
+
schema: schema
|
68
|
+
}.compact
|
69
|
+
]
|
70
|
+
end.to_h.compact
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def _response(response)
|
76
|
+
{
|
77
|
+
description: response.description,
|
78
|
+
# headers:
|
79
|
+
# response.headers.map do |header|
|
80
|
+
# [
|
81
|
+
# header.name,
|
82
|
+
# {
|
83
|
+
# description: header.description
|
84
|
+
# }.compact
|
85
|
+
# ]
|
86
|
+
# end.to_h,
|
87
|
+
content:
|
88
|
+
response.content_by_type.map do |content_type, content|
|
89
|
+
[
|
90
|
+
content_type,
|
91
|
+
{
|
92
|
+
schema:
|
93
|
+
if content.type.nil?
|
94
|
+
nil
|
95
|
+
elsif content.type.is_a?(Class) && content.type.ancestors.include?(Dry::Struct)
|
96
|
+
_struct_ref(content.type)
|
97
|
+
else
|
98
|
+
# TODO: make this less wasteful
|
99
|
+
# Right now, this reuses the schema walking stuff. It totally will re-walk
|
100
|
+
# existing schemas as far as `content.type` will reach. Which is slower
|
101
|
+
# during startup than we'd like, but it's fixable later by converting
|
102
|
+
# the methods to using state (that's why `OpenAPI3` is an object in the
|
103
|
+
# first place, but I wrote myself into a corner here).
|
104
|
+
_build_schema_value({}, {}, content.type)
|
105
|
+
end
|
106
|
+
}.compact
|
107
|
+
]
|
108
|
+
end.to_h
|
109
|
+
}.compact
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './operations'
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module DocGenerator
|
7
|
+
class OpenAPI3
|
8
|
+
module Paths
|
9
|
+
include Operations
|
10
|
+
|
11
|
+
def _paths(descriptor)
|
12
|
+
descriptor.routes_by_path.map do |path, routes_by_method|
|
13
|
+
[
|
14
|
+
path,
|
15
|
+
routes_by_method.map do |method, route|
|
16
|
+
[method.downcase, _operation(route)]
|
17
|
+
end.to_h
|
18
|
+
]
|
19
|
+
end.to_h
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modern
|
4
|
+
module DocGenerator
|
5
|
+
class OpenAPI3
|
6
|
+
module SchemaDefaultTypes
|
7
|
+
def _register_default_types!
|
8
|
+
# TODO: handle all default types
|
9
|
+
# This misses a few types, mostly because I don't yet know how to
|
10
|
+
# handle them:
|
11
|
+
# - I don't understand the Types::Form set of types well enough.
|
12
|
+
# - I have not provided a DateTime mapping because you shouldn't
|
13
|
+
# use DateTime and I have no idea how to do so sanely.
|
14
|
+
# - We can't coerce Symbols, so those are out.
|
15
|
+
[Types::String, Types::Strict::String, Types::Coercible::String].each do |t|
|
16
|
+
register_literal_type(t, type: "string")
|
17
|
+
end
|
18
|
+
|
19
|
+
[Types::Int, Types::Strict::Int, Types::Coercible::Int].each do |t|
|
20
|
+
register_literal_type(t, type: "integer", format: "int64")
|
21
|
+
end
|
22
|
+
|
23
|
+
[
|
24
|
+
Types::Bool, Types::Strict::Bool,
|
25
|
+
Types::True, Types::Strict::True,
|
26
|
+
Types::False, Types::Strict::False
|
27
|
+
].each do |t|
|
28
|
+
register_literal_type(t, type: "boolean")
|
29
|
+
end
|
30
|
+
|
31
|
+
[Types::Float, Types::Strict::Float, Types::Coercible::Float].each do |t|
|
32
|
+
register_literal_type(t, type: "number", format: "double")
|
33
|
+
end
|
34
|
+
|
35
|
+
[Types::Date, Types::Strict::Date, Types::Json::Date].each do |t|
|
36
|
+
register_literal_type(t, type: "string", format: "date")
|
37
|
+
end
|
38
|
+
|
39
|
+
[Types::Time, Types::Strict::Time, Types::Json::Time].each do |t|
|
40
|
+
register_literal_type(t, type: "string", format: "date")
|
41
|
+
end
|
42
|
+
|
43
|
+
[Types::Decimal, Types::Strict::Decimal, Types::Coercible::Decimal].each do |t|
|
44
|
+
register_literal_type(t, type: "number")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|