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,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './schema_default_types'
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module DocGenerator
|
7
|
+
class OpenAPI3
|
8
|
+
module Schemas
|
9
|
+
# TODO: make all this not awful!
|
10
|
+
# I am not a type theorist. I am also not a compiler writer
|
11
|
+
# (though I've pretended to be in my day once or twice). dry-types
|
12
|
+
# is a very dense language and parsing it to emit OpenAPI schemas is
|
13
|
+
# really, really hard for me. This is a brute-force approach. There
|
14
|
+
# is probably a better one. My approach is basically to allow for
|
15
|
+
# the registration of literal types (which serve as my terminals) and
|
16
|
+
# try to build rules on top of those literal types for more complex
|
17
|
+
# ideas.
|
18
|
+
# TODO: parse the dry-logic in predicates to properly fill out the rest of
|
19
|
+
# the JSON schema
|
20
|
+
|
21
|
+
include SchemaDefaultTypes
|
22
|
+
|
23
|
+
def register_literal_type(type, oapi3_value)
|
24
|
+
raise "`type` must be a Dry::Types::Type." unless type.is_a?(Dry::Types::Type)
|
25
|
+
|
26
|
+
@type_registry[type] = oapi3_value
|
27
|
+
end
|
28
|
+
|
29
|
+
# Only Dry::Struct
|
30
|
+
def _struct_schemas(descriptor)
|
31
|
+
ret = {}
|
32
|
+
name_to_class = {}
|
33
|
+
|
34
|
+
descriptor.root_schemas \
|
35
|
+
.select { |type_or_structclass| type_or_structclass.is_a?(Class) } \
|
36
|
+
.each do |structclass|
|
37
|
+
_build_struct(ret, name_to_class, structclass)
|
38
|
+
end
|
39
|
+
|
40
|
+
ret
|
41
|
+
end
|
42
|
+
|
43
|
+
def _build_struct(ret, name_to_class, structclass)
|
44
|
+
# TODO: allow overriding the name of the struct in #/components/schemas
|
45
|
+
# This is actually trickier than it looks, because we need to also
|
46
|
+
# make it referenceable in responses/contents. It probably means an
|
47
|
+
# indirect mapping of classes to names and back again.
|
48
|
+
|
49
|
+
raise "not actually a Dry::Struct class" \
|
50
|
+
unless structclass.ancestors.include?(Dry::Struct)
|
51
|
+
|
52
|
+
name = structclass.name.split("::").last
|
53
|
+
|
54
|
+
if name_to_class[name] == structclass
|
55
|
+
name
|
56
|
+
else
|
57
|
+
if !name_to_class[name].nil?
|
58
|
+
raise "Duplicate schema name: '#{name}'. Only one class, regardless " \
|
59
|
+
"of namespace, can be called this."
|
60
|
+
end
|
61
|
+
|
62
|
+
ret[name] = _build_object_from_schema(ret, name_to_class, structclass.schema)
|
63
|
+
end
|
64
|
+
|
65
|
+
name # necessary for recursive calls in _build_schema_value
|
66
|
+
end
|
67
|
+
|
68
|
+
def _build_object_from_schema(ret, name_to_class, dt_schema)
|
69
|
+
{
|
70
|
+
type: "object",
|
71
|
+
properties: dt_schema.map do |k, v|
|
72
|
+
[k, _build_schema_value(ret, name_to_class, v)]
|
73
|
+
end.to_h
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def _struct_ref(structclass)
|
78
|
+
{ "$ref": "#/components/schemas/#{structclass.name.split('::').last}" }
|
79
|
+
end
|
80
|
+
|
81
|
+
def _build_schema_value(ret, name_to_class, entry)
|
82
|
+
registered_type = @type_registry[entry]
|
83
|
+
|
84
|
+
if !registered_type.nil?
|
85
|
+
registered_type
|
86
|
+
elsif entry.is_a?(Dry::Types::Sum::Constrained)
|
87
|
+
if entry.left.type.primitive == NilClass
|
88
|
+
# it's a nullable field
|
89
|
+
_build_schema_value(ret, name_to_class, entry.right).merge(nullable: true)
|
90
|
+
else
|
91
|
+
{
|
92
|
+
anyOf: _flatten_any_of(
|
93
|
+
[
|
94
|
+
_build_schema_value(ret, name_to_class, entry.left),
|
95
|
+
_build_schema_value(ret, name_to_class, entry.right)
|
96
|
+
]
|
97
|
+
)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
elsif entry.is_a?(Dry::Types::Constrained)
|
101
|
+
# TODO: dig deeper into the actual behavior of Constrained (dry-logic)
|
102
|
+
# This is probably a can of worms. More:
|
103
|
+
# http://dry-rb.org/gems/dry-types/constraints/
|
104
|
+
|
105
|
+
_build_schema_value(ret, name_to_class, entry.type)
|
106
|
+
elsif entry.is_a?(Dry::Types::Default)
|
107
|
+
# this just unwraps the default value
|
108
|
+
_build_schema_value(ret, name_to_class, entry.type)
|
109
|
+
elsif entry.is_a?(Dry::Types::Definition)
|
110
|
+
primitive = entry.primitive
|
111
|
+
|
112
|
+
if primitive.ancestors.include?(Dry::Struct)
|
113
|
+
# TODO: make sure I'm understanding this correctly
|
114
|
+
# It feels weird to have to oneOf a $ref, but I can't figure out a
|
115
|
+
# syntax that doesn't require it.
|
116
|
+
_build_struct(ret, name_to_class, primitive)
|
117
|
+
|
118
|
+
{
|
119
|
+
oneOf: [
|
120
|
+
_struct_ref(primitive)
|
121
|
+
]
|
122
|
+
}
|
123
|
+
elsif primitive.ancestors.include?(Hash)
|
124
|
+
_build_object_from_schema(ret, name_to_class, entry.member_types)
|
125
|
+
elsif primitive.ancestors.include?(Array)
|
126
|
+
{
|
127
|
+
type: "array",
|
128
|
+
items: _build_schema_value(ret, name_to_class, entry.member)
|
129
|
+
}
|
130
|
+
else
|
131
|
+
raise "unrecognized primitive definition '#{primitive.name}'; probably needs a literal."
|
132
|
+
end
|
133
|
+
else
|
134
|
+
raise "Unrecognized schema class: #{entry.class.name}: #{entry.inspect}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def _flatten_any_of(typehash_array)
|
139
|
+
# This is hacky, but it removes the incidence of something like this:
|
140
|
+
#
|
141
|
+
# :exsub => {
|
142
|
+
# :anyOf => [
|
143
|
+
# [0] {
|
144
|
+
# :oneOf => [
|
145
|
+
# [0] {
|
146
|
+
# :$ref => "#/components/schemas/ExclusiveSubA"
|
147
|
+
# }
|
148
|
+
# ]
|
149
|
+
# },
|
150
|
+
# [1] {
|
151
|
+
# :oneOf => [
|
152
|
+
# [0] {
|
153
|
+
# :$ref => "#/components/schemas/ExclusiveSubB"
|
154
|
+
# }
|
155
|
+
# ]
|
156
|
+
# }
|
157
|
+
# ]
|
158
|
+
# }
|
159
|
+
|
160
|
+
typehash_array.map do |typehash|
|
161
|
+
if typehash.length == 1 && typehash.first.first == :oneOf
|
162
|
+
typehash.first.last
|
163
|
+
else
|
164
|
+
typehash
|
165
|
+
end
|
166
|
+
end.flatten
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modern
|
4
|
+
module DocGenerator
|
5
|
+
class OpenAPI3
|
6
|
+
module Schemas
|
7
|
+
def _security_schemes(descriptor)
|
8
|
+
descriptor.securities_by_name.map do |name, security|
|
9
|
+
[name, security.to_openapi3.compact]
|
10
|
+
end.to_h
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'modern/descriptor'
|
7
|
+
require 'modern/descriptor/converters'
|
8
|
+
|
9
|
+
require 'modern/version'
|
10
|
+
|
11
|
+
Dir["#{__dir__}/open_api3/*.rb"].each { |f| require_relative f }
|
12
|
+
|
13
|
+
module Modern
|
14
|
+
module DocGenerator
|
15
|
+
class OpenAPI3
|
16
|
+
include Schemas
|
17
|
+
include Paths
|
18
|
+
|
19
|
+
# TODO: this is pretty inflexible. Eventually, consumers may want to
|
20
|
+
# subclass Route, Content, etc., for their own use cases, and that
|
21
|
+
# would make using an external/visiting doc generator impractical.
|
22
|
+
# It's simple enough, though, so let's roll with it for now.
|
23
|
+
|
24
|
+
OPENAPI_VERSION = Modern::OPENAPI_VERSION
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@type_registry = {}
|
28
|
+
_register_default_types!
|
29
|
+
end
|
30
|
+
|
31
|
+
def json(descriptor)
|
32
|
+
JSON.pretty_generate(hash(descriptor))
|
33
|
+
end
|
34
|
+
|
35
|
+
def yaml(descriptor)
|
36
|
+
# TODO: this hack exists just to de-symbolize the output without spending
|
37
|
+
# a bunch of time on it, because we don't have ActiveSupport and
|
38
|
+
# #deep_stringify_keys. It happens once at startup, it's not a big
|
39
|
+
# deal.
|
40
|
+
YAML.dump(JSON.parse(json(descriptor)))
|
41
|
+
end
|
42
|
+
|
43
|
+
def both(descriptor)
|
44
|
+
j = JSON.pretty_generate(hash(descriptor))
|
45
|
+
|
46
|
+
{
|
47
|
+
json: j,
|
48
|
+
yaml: YAML.dump(JSON.parse(j))
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash(descriptor)
|
53
|
+
ret = {
|
54
|
+
openapi: OPENAPI_VERSION,
|
55
|
+
|
56
|
+
info: _info(descriptor.info),
|
57
|
+
servers: descriptor.servers.empty? ? nil : descriptor.servers.map(&:to_h),
|
58
|
+
paths: _paths(descriptor),
|
59
|
+
components: _components(descriptor)
|
60
|
+
}.compact
|
61
|
+
|
62
|
+
ret
|
63
|
+
end
|
64
|
+
|
65
|
+
def decorate_with_openapi_routes(configuration, descriptor)
|
66
|
+
docs = both(descriptor)
|
67
|
+
|
68
|
+
openapi3_json = docs[:json].freeze
|
69
|
+
openapi3_yaml = docs[:yaml].freeze
|
70
|
+
|
71
|
+
serve_json = Modern::Descriptor::Route.new(
|
72
|
+
id: "serveOpenApi3Json",
|
73
|
+
http_method: :GET,
|
74
|
+
path: configuration.open_api_json_path,
|
75
|
+
summary: "Serves the OpenAPI3 application spec in JSON form.",
|
76
|
+
responses: [
|
77
|
+
Modern::Descriptor::Response.new(
|
78
|
+
http_code: :default,
|
79
|
+
content: [Modern::Descriptor::Content.new(media_type: "application/json")]
|
80
|
+
)
|
81
|
+
],
|
82
|
+
output_converters: [
|
83
|
+
Modern::Descriptor::Converters::Output::JSONBypass
|
84
|
+
],
|
85
|
+
action:
|
86
|
+
proc do
|
87
|
+
response.bypass!
|
88
|
+
response.write(openapi3_json)
|
89
|
+
end
|
90
|
+
)
|
91
|
+
|
92
|
+
serve_yaml = Modern::Descriptor::Route.new(
|
93
|
+
id: "serveOpenApi3Yaml",
|
94
|
+
http_method: :GET,
|
95
|
+
path: configuration.open_api_yaml_path,
|
96
|
+
summary: "Serves the OpenAPI3 application spec in YAML form.",
|
97
|
+
responses: [
|
98
|
+
Modern::Descriptor::Response.new(
|
99
|
+
http_code: :default,
|
100
|
+
content: [Modern::Descriptor::Content.new(media_type: "application/yaml")]
|
101
|
+
)
|
102
|
+
],
|
103
|
+
output_converters: [
|
104
|
+
Modern::Descriptor::Converters::Output::YAMLBypass
|
105
|
+
],
|
106
|
+
action:
|
107
|
+
proc do
|
108
|
+
response.bypass!
|
109
|
+
response.write(openapi3_yaml)
|
110
|
+
end
|
111
|
+
)
|
112
|
+
|
113
|
+
[serve_json, serve_yaml, descriptor.routes].flatten
|
114
|
+
end
|
115
|
+
|
116
|
+
def _info(obj)
|
117
|
+
obj.to_h.compact
|
118
|
+
end
|
119
|
+
|
120
|
+
def _components(descriptor)
|
121
|
+
# TODO: figure out if we can omit the empty hashes below. We're probably never
|
122
|
+
# going to emit a "minimal" doc; I can't see ever trying to dedupe request
|
123
|
+
# bodies or whatever. (We do security schemes because it's easier and we
|
124
|
+
# do schemas to provide a reason for using dry-struct over a bunch of
|
125
|
+
# hashes.)
|
126
|
+
ret = {
|
127
|
+
schemas: _struct_schemas(descriptor),
|
128
|
+
responses: {},
|
129
|
+
parameters: {},
|
130
|
+
examples: {},
|
131
|
+
requestBodies: {},
|
132
|
+
securitySchemes: _security_schemes(descriptor),
|
133
|
+
links: {},
|
134
|
+
callbacks: {}
|
135
|
+
}
|
136
|
+
|
137
|
+
ret
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/descriptor/info'
|
4
|
+
|
5
|
+
require 'docile'
|
6
|
+
|
7
|
+
module Modern
|
8
|
+
module DSL
|
9
|
+
class Info
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
def initialize(title, version)
|
13
|
+
@value = Modern::Descriptor::Info.new(title: title, version: version)
|
14
|
+
end
|
15
|
+
|
16
|
+
def description(v)
|
17
|
+
@value = @value.copy(description: v)
|
18
|
+
end
|
19
|
+
|
20
|
+
def terms_of_service(v)
|
21
|
+
@value = @value.copy(terms_of_service: v)
|
22
|
+
end
|
23
|
+
|
24
|
+
def contact(name: nil, url: nil, email: nil)
|
25
|
+
@value = @value.copy(contact: { name: name, url: url, email: email }.compact)
|
26
|
+
end
|
27
|
+
|
28
|
+
def license(name, url: nil)
|
29
|
+
@value = @value.copy(license: { name: name, url: url }.compact)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.build(title, version, &block)
|
33
|
+
i = Info.new(title, version)
|
34
|
+
i.instance_exec(&block)
|
35
|
+
i.value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/descriptor/response'
|
4
|
+
require 'modern/descriptor/content'
|
5
|
+
|
6
|
+
require 'docile'
|
7
|
+
|
8
|
+
module Modern
|
9
|
+
module DSL
|
10
|
+
class ResponseBuilder
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
def initialize(http_code_or_response)
|
14
|
+
@value =
|
15
|
+
if http_code_or_response.is_a?(Modern::Descriptor::Response)
|
16
|
+
http_code_or_response
|
17
|
+
else
|
18
|
+
Modern::Descriptor::Response.new(http_code: http_code_or_response)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def description(s)
|
23
|
+
@value = @value.copy(description: s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def content(media_type, type = nil)
|
27
|
+
raise "Duplicate content type: #{media_type}" \
|
28
|
+
if @value.content.any? { |c| c.media_type.casecmp(media_type).zero? }
|
29
|
+
|
30
|
+
new_content = Modern::Descriptor::Content.new(media_type: media_type, type: type)
|
31
|
+
@value = @value.copy(content: @value.content + [new_content])
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.evaluate(http_code_or_response, &block)
|
35
|
+
builder = ResponseBuilder.new(http_code_or_response)
|
36
|
+
builder.instance_exec(&block)
|
37
|
+
builder.value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/dsl/info'
|
4
|
+
require 'modern/dsl/scope'
|
5
|
+
|
6
|
+
module Modern
|
7
|
+
module DSL
|
8
|
+
class Root < Scope
|
9
|
+
attr_reader :descriptor
|
10
|
+
|
11
|
+
def initialize(descriptor)
|
12
|
+
super(descriptor, nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def info(&block)
|
16
|
+
@descriptor = @descriptor.copy(
|
17
|
+
info: Info.build(descriptor.info.title, descriptor.info.version, &block).to_h
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def server(url, description: nil)
|
22
|
+
raise "url is required for a server declaration." if url.nil?
|
23
|
+
|
24
|
+
@descriptor = @descriptor.copy(
|
25
|
+
servers: @descriptor.servers + [Modern::Descriptor::Server.new(url: url, description: description)]
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build(title, version, &block)
|
30
|
+
d = Modern::Descriptor::Core.new(info: { title: title, version: version })
|
31
|
+
|
32
|
+
r = Root.new(d)
|
33
|
+
r.instance_exec(&block)
|
34
|
+
r.descriptor
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/descriptor/route'
|
4
|
+
|
5
|
+
require 'modern/dsl/response_builder'
|
6
|
+
|
7
|
+
require 'docile'
|
8
|
+
|
9
|
+
module Modern
|
10
|
+
module DSL
|
11
|
+
class RouteBuilder
|
12
|
+
attr_reader :value
|
13
|
+
|
14
|
+
def initialize(id, http_method, path, settings)
|
15
|
+
new_path_segments = path&.split("/") || []
|
16
|
+
@value = Modern::Descriptor::Route.new(
|
17
|
+
id: id.to_s,
|
18
|
+
path: "/" + ([settings.path_segments] + new_path_segments).flatten.compact.join("/"),
|
19
|
+
http_method: http_method.upcase,
|
20
|
+
summary: nil,
|
21
|
+
description: nil,
|
22
|
+
deprecated: settings.deprecated,
|
23
|
+
tags: settings.tags,
|
24
|
+
parameters: settings.parameters,
|
25
|
+
request_body: nil,
|
26
|
+
responses: [settings.default_response],
|
27
|
+
input_converters: settings.input_converters,
|
28
|
+
output_converters: settings.output_converters,
|
29
|
+
security: settings.security,
|
30
|
+
helpers: settings.helpers,
|
31
|
+
action: proc {}
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def summary(s)
|
36
|
+
@value = @value.copy(summary: s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def description(s)
|
40
|
+
@value = @value.copy(description: s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def deprecate!
|
44
|
+
@value = @value.copy(deprecated: true)
|
45
|
+
end
|
46
|
+
|
47
|
+
def tag(s)
|
48
|
+
@value = @value.copy(tags: @value.tags + [s])
|
49
|
+
end
|
50
|
+
|
51
|
+
def parameter(name, parameter_type, opts)
|
52
|
+
param = Modern::Descriptor::Parameters.from_inputs(name, parameter_type, opts)
|
53
|
+
raise "Duplicate parameter '#{name}'.'" if @value.parameters.any? { |p| p.name.casecmp(param.name).zero? }
|
54
|
+
|
55
|
+
@value = @value.copy(parameters: @value.parameters + [param])
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_body(type, opts = { required: true })
|
59
|
+
opts = { required: true }.merge(opts).merge(type: type)
|
60
|
+
@value = @value.copy(request_body: Modern::Descriptor::RequestBody.new(opts))
|
61
|
+
end
|
62
|
+
|
63
|
+
def response(http_code, &block)
|
64
|
+
existing_response = @value.responses.find { |r| r.http_code == http_code }
|
65
|
+
|
66
|
+
resp = ResponseBuilder.evaluate(existing_response || http_code, &block)
|
67
|
+
@value = @value.copy(responses: @value.responses + [resp])
|
68
|
+
end
|
69
|
+
|
70
|
+
def input_converter(media_type_or_converter, &block)
|
71
|
+
if media_type_or_converter.is_a?(Modern::Descriptor::Converters::Input::Base)
|
72
|
+
@value = @value.copy(input_converters: @value.input_converters + [media_type_or_converter])
|
73
|
+
elsif media_type_or_converter.is_a?(String) && !block.nil?
|
74
|
+
input_converter(
|
75
|
+
Modern::Descriptor::Converters::Input::Base.new(
|
76
|
+
media_type: media_type_or_converter, converter: block
|
77
|
+
)
|
78
|
+
)
|
79
|
+
else
|
80
|
+
raise "must pass a String and block or a Modern::Descriptor::Converters::Input::Base."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def clear_input_converters!
|
85
|
+
@value = @value.copy(input_converters: [])
|
86
|
+
end
|
87
|
+
|
88
|
+
def output_converter(media_type_or_converter, &block)
|
89
|
+
if media_type_or_converter.is_a?(Modern::Descriptor::Converters::Output::Base)
|
90
|
+
@value = @value.copy(output_converters: @value.output_converters + [media_type_or_converter])
|
91
|
+
elsif media_type_or_converter.is_a?(String) && !block.nil?
|
92
|
+
output_converter(
|
93
|
+
Modern::Descriptor::Converters::Output::Base.new(
|
94
|
+
media_type: media_type_or_converter, converter: block
|
95
|
+
)
|
96
|
+
)
|
97
|
+
else
|
98
|
+
raise "must pass a String and block or a Modern::Descriptor::Converters::Output::Base."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def clear_output_converters!
|
103
|
+
@value = @value.copy(output_converters: [])
|
104
|
+
end
|
105
|
+
|
106
|
+
def clear_security!
|
107
|
+
@value = @value.copy(security: [])
|
108
|
+
end
|
109
|
+
|
110
|
+
def security(sec)
|
111
|
+
@value = @value.copy(security: @value.security + [sec])
|
112
|
+
end
|
113
|
+
|
114
|
+
def helper(h)
|
115
|
+
@value = @value.copy(helpers: @value.helpers + [h])
|
116
|
+
end
|
117
|
+
|
118
|
+
def action(&block)
|
119
|
+
@value = @value.copy(action: block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.evaluate(id, http_method, path, settings, &block)
|
123
|
+
builder = RouteBuilder.new(id, http_method, path, settings)
|
124
|
+
builder.instance_exec(&block)
|
125
|
+
|
126
|
+
builder.value
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|