explicit 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +75 -67
- data/app/helpers/explicit/application_helper.rb +32 -0
- data/app/views/explicit/documentation/_attribute.html.erb +38 -0
- data/app/views/explicit/documentation/_page.html.erb +166 -0
- data/app/views/explicit/documentation/_request.html.erb +87 -0
- data/app/views/explicit/documentation/request/_examples.html.erb +50 -0
- data/app/views/explicit/documentation/type/_agreement.html.erb +7 -0
- data/app/views/explicit/documentation/type/_array.html.erb +3 -0
- data/app/views/explicit/documentation/type/_big_decimal.html.erb +4 -0
- data/app/views/explicit/documentation/type/_boolean.html.erb +7 -0
- data/app/views/explicit/documentation/type/_date_time_iso8601.html.erb +3 -0
- data/app/views/explicit/documentation/type/_date_time_posix.html.erb +3 -0
- data/app/views/explicit/documentation/type/_enum.html.erb +7 -0
- data/app/views/explicit/documentation/type/_file.html.erb +9 -0
- data/app/views/explicit/documentation/type/_hash.html.erb +4 -0
- data/app/views/explicit/documentation/type/_integer.html.erb +25 -0
- data/app/views/explicit/documentation/type/_one_of.html.erb +11 -0
- data/app/views/explicit/documentation/type/_record.html.erb +9 -0
- data/app/views/explicit/documentation/type/_string.html.erb +21 -0
- data/config/locales/en.yml +27 -11
- data/lib/explicit/configuration.rb +1 -1
- data/lib/explicit/documentation/builder.rb +80 -0
- data/lib/explicit/documentation/markdown.rb +2 -13
- data/lib/explicit/documentation/output/swagger.rb +176 -0
- data/lib/explicit/documentation/output/webpage.rb +31 -0
- data/lib/explicit/documentation/page/partial.rb +20 -0
- data/lib/explicit/documentation/page/request.rb +27 -0
- data/lib/explicit/documentation/section.rb +9 -0
- data/lib/explicit/documentation.rb +12 -145
- data/lib/explicit/request/example.rb +50 -1
- data/lib/explicit/request/invalid_params_error.rb +1 -3
- data/lib/explicit/request/invalid_response_error.rb +2 -15
- data/lib/explicit/request/route.rb +18 -0
- data/lib/explicit/request.rb +43 -24
- data/lib/explicit/test_helper/example_recorder.rb +7 -2
- data/lib/explicit/test_helper.rb +25 -7
- data/lib/explicit/type/agreement.rb +39 -0
- data/lib/explicit/type/array.rb +56 -0
- data/lib/explicit/type/big_decimal.rb +58 -0
- data/lib/explicit/type/boolean.rb +47 -0
- data/lib/explicit/type/date_time_iso8601.rb +41 -0
- data/lib/explicit/type/date_time_posix.rb +44 -0
- data/lib/explicit/type/enum.rb +41 -0
- data/lib/explicit/type/file.rb +60 -0
- data/lib/explicit/type/hash.rb +57 -0
- data/lib/explicit/type/integer.rb +79 -0
- data/lib/explicit/type/literal.rb +45 -0
- data/lib/explicit/type/modifiers/default.rb +24 -0
- data/lib/explicit/type/modifiers/description.rb +11 -0
- data/lib/explicit/type/modifiers/nilable.rb +19 -0
- data/lib/explicit/type/modifiers/param_location.rb +11 -0
- data/lib/explicit/type/one_of.rb +46 -0
- data/lib/explicit/type/record.rb +96 -0
- data/lib/explicit/type/string.rb +68 -0
- data/lib/explicit/type.rb +112 -0
- data/lib/explicit/version.rb +1 -1
- data/lib/explicit.rb +28 -18
- metadata +47 -25
- data/app/views/explicit/application/_documentation.html.erb +0 -136
- data/app/views/explicit/application/_request.html.erb +0 -37
- data/lib/explicit/documentation/property.rb +0 -19
- data/lib/explicit/spec/agreement.rb +0 -17
- data/lib/explicit/spec/array.rb +0 -28
- data/lib/explicit/spec/bigdecimal.rb +0 -27
- data/lib/explicit/spec/boolean.rb +0 -30
- data/lib/explicit/spec/date_time_iso8601.rb +0 -17
- data/lib/explicit/spec/date_time_posix.rb +0 -21
- data/lib/explicit/spec/default.rb +0 -20
- data/lib/explicit/spec/error.rb +0 -63
- data/lib/explicit/spec/hash.rb +0 -30
- data/lib/explicit/spec/inclusion.rb +0 -15
- data/lib/explicit/spec/integer.rb +0 -53
- data/lib/explicit/spec/literal.rb +0 -15
- data/lib/explicit/spec/nilable.rb +0 -15
- data/lib/explicit/spec/one_of.rb +0 -40
- data/lib/explicit/spec/record.rb +0 -33
- data/lib/explicit/spec/string.rb +0 -50
- data/lib/explicit/spec.rb +0 -72
@@ -0,0 +1,87 @@
|
|
1
|
+
<h1
|
2
|
+
class="text-4xl font-bold my-4"
|
3
|
+
id="<%= page.anchor %>"
|
4
|
+
x-intersect="activeLink = '<%= page.anchor %>'"
|
5
|
+
>
|
6
|
+
<%= page.title %>
|
7
|
+
</h1>
|
8
|
+
|
9
|
+
<div class="bg-neutral-800 text-white p-2 rounded font-mono">
|
10
|
+
<% page.request.routes.each do |route| %>
|
11
|
+
<div>
|
12
|
+
<span class="font-bold"><%= route.method.to_s.upcase %></span>
|
13
|
+
|
14
|
+
<span class="text-neutral-300"><%= page.request.get_base_url %><%= page.request.get_base_path %></span><span class="font-bold"><%= route.path %></span>
|
15
|
+
</div>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<% if page.request.accepts_file_upload? %>
|
20
|
+
<div class="bg-neutral-800 text-white rounded py-1 px-2 text-sm mt-2 inline-flex items-center gap-2">
|
21
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
22
|
+
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
23
|
+
</svg>
|
24
|
+
|
25
|
+
This request requires <span class="font-mono font-bold">multipart/form-data</span> encoding for file upload
|
26
|
+
</div>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<div class="flex mt-4 gap-8">
|
30
|
+
<div class="page__request space-y-4">
|
31
|
+
<% if (description = page.request.get_description) %>
|
32
|
+
<div class="markdown">
|
33
|
+
<%= Explicit::Documentation::Markdown.to_html(description) %>
|
34
|
+
</div>
|
35
|
+
<% end %>
|
36
|
+
|
37
|
+
<% if page.request.headers.any? %>
|
38
|
+
<div>
|
39
|
+
<div class="bg-gray-100 border-t border-x text-center text-xs p-1 uppercase text-neutral-500">
|
40
|
+
Headers
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<%= type_render page.request.headers_type %>
|
44
|
+
</div>
|
45
|
+
<% end %>
|
46
|
+
|
47
|
+
<% if page.request.params.any? %>
|
48
|
+
<div>
|
49
|
+
<div class="bg-gray-100 border-t border-x text-center text-xs p-1 uppercase text-neutral-500">
|
50
|
+
Params
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<%= type_render page.request.params_type %>
|
54
|
+
</div>
|
55
|
+
<% end %>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<% statuses = page.request.responses.keys.sort %>
|
59
|
+
|
60
|
+
<div
|
61
|
+
class="page__response"
|
62
|
+
x-data="{ active: <%= statuses.first %> }"
|
63
|
+
>
|
64
|
+
<div class="flex">
|
65
|
+
<% statuses.each do |status| %>
|
66
|
+
<button
|
67
|
+
class="bg-white py-2 px-6"
|
68
|
+
x-bind:class="{ 'border-l border-t border-r -mb-px': active == <%= status %> }"
|
69
|
+
x-on:click="active = <%= status %>"
|
70
|
+
>
|
71
|
+
<span class="text-neutral-400">Resp.</span>
|
72
|
+
<span class="font-bold"><%= status %></span>
|
73
|
+
</button>
|
74
|
+
<% end %>
|
75
|
+
</div>
|
76
|
+
|
77
|
+
<div class="border">
|
78
|
+
<% statuses.each do |status| %>
|
79
|
+
<div class="p-4" x-show="active == <%= status %>">
|
80
|
+
<%= type_render page.request.responses_type(status:) %>
|
81
|
+
|
82
|
+
<%= render partial: "explicit/documentation/request/examples", locals: { request: page.request, status: } %>
|
83
|
+
</div>
|
84
|
+
<% end %>
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
</div>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<% examples = request.examples[status] %>
|
2
|
+
|
3
|
+
<div
|
4
|
+
class="responses__examples"
|
5
|
+
x-data="{ example: 0, maxsize: <%= examples.size %> }"
|
6
|
+
>
|
7
|
+
<% if examples.any? %>
|
8
|
+
<div class="flex items-center -mx-4 my-4 p-4 bg-neutral-100">
|
9
|
+
<div class="text-sm uppercase font-medium tracking-wide text-neutral-600">
|
10
|
+
<% if examples.size > 1 %>
|
11
|
+
Examples (<span x-text="example+1"></span>/<span x-text="maxsize"></span>)
|
12
|
+
<% else %>
|
13
|
+
Example
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<% if examples.size > 1 %>
|
18
|
+
<div class="ml-auto flex">
|
19
|
+
<button
|
20
|
+
class="p-1 text-neutral-800 hover:text-black"
|
21
|
+
x-bind:class="{'opacity-30': example === 0}"
|
22
|
+
x-bind:disabled="example === 0"
|
23
|
+
x-on:click="example -= 1"
|
24
|
+
>
|
25
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
26
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
27
|
+
</svg>
|
28
|
+
</button>
|
29
|
+
|
30
|
+
<button
|
31
|
+
class="p-1 text-neutral-800 hover:text-black"
|
32
|
+
x-bind:class="{'opacity-30': example === maxsize-1}"
|
33
|
+
x-bind:disabled="example === maxsize-1"
|
34
|
+
x-on:click="example += 1"
|
35
|
+
>
|
36
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
37
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
38
|
+
</svg>
|
39
|
+
</button>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
</div>
|
43
|
+
<% end %>
|
44
|
+
|
45
|
+
<% request.examples[status].each.with_index do |example, index| %>
|
46
|
+
<% route = request.routes.first %>
|
47
|
+
|
48
|
+
<pre class="font-mono text-sm overflow-auto" x-show="example == <%= index %>"><%= format_request_example(request:, example:) %></pre>
|
49
|
+
<% end %>
|
50
|
+
</div>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<%= type_constraints do %>
|
2
|
+
<% if type.max_size.present? %>
|
3
|
+
<%= type_constraint "max size:", number_to_human_size(type.max_size, precision: 2) %>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<% if type.content_types.any? %>
|
7
|
+
<%= type_constraint "content types:", type.content_types.join(", ") %>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<%= type_constraints do %>
|
2
|
+
<% if type.min.present? %>
|
3
|
+
<%= type_constraint "min", type.min %>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<% if type.max.present? %>
|
7
|
+
<%= type_constraint "max", type.max %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<% if type.negative == false %>
|
11
|
+
<%= type_constraint "not", "negative" %>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<% if type.negative == true %>
|
15
|
+
<%= type_constraint "only", "negative" %>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<% if type.positive == false %>
|
19
|
+
<%= type_constraint "not", "positive" %>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<% if type.positive == true %>
|
23
|
+
<%= type_constraint "only", "positive" %>
|
24
|
+
<% end %>
|
25
|
+
<% end %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<% type.subtypes.each.with_index do |subtype, index| %>
|
2
|
+
<%= type_render subtype %>
|
3
|
+
|
4
|
+
<% if index < type.subtypes.size - 1 %>
|
5
|
+
<div class="flex items-center my-4">
|
6
|
+
<div class="h-px w-full flex-grow bg-neutral-200"></div>
|
7
|
+
<div class="mx-4 text-neutral-400 text-xs">OR</div>
|
8
|
+
<div class="h-px w-full flex-grow bg-neutral-300"></div>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
11
|
+
<% end %>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<div class="border divide-y">
|
2
|
+
<% if type.attributes.empty? %>
|
3
|
+
<div class="p-4 text-neutral-400 text-center text-sm">--Empty body--</div>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<% type.attributes.each do |name, type| %>
|
7
|
+
<%= type_attribute_render(name:, type:) %>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<%= type_constraints do %>
|
2
|
+
<% if type.empty == false %>
|
3
|
+
<%= type_constraint "not", "empty" %>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<% if type.minlength %>
|
7
|
+
<%= type_constraint "minlength", type.minlength %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<% if type.maxlength %>
|
11
|
+
<%= type_constraint "maxlength", type.maxlength %>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<% if type.format %>
|
15
|
+
<%= type_constraint "format", type.format.inspect %>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<% if type.downcase %>
|
19
|
+
<%= type_constraint "case", "insensitive" %>
|
20
|
+
<% end %>
|
21
|
+
<% end %>
|
data/config/locales/en.yml
CHANGED
@@ -6,21 +6,37 @@ en:
|
|
6
6
|
bigdecimal: "must be a string-encoded decimal number"
|
7
7
|
boolean: "must be a boolean"
|
8
8
|
date_time_iso8601: "must be a valid iso8601 date time"
|
9
|
-
date_time_posix: "must be a valid posix
|
10
|
-
hash: "must be
|
9
|
+
date_time_posix: "must be a valid posix timestamp"
|
10
|
+
hash: "must be an object"
|
11
11
|
hash_key: "invalid key (%{key}): %{error}"
|
12
12
|
hash_value: "invalid value at key (%{key}): %{error}"
|
13
|
-
|
13
|
+
enum: "must be one of: %{allowed_values}"
|
14
|
+
file: "must be a file"
|
15
|
+
file_max_size: "must be smaller than %{max_size}"
|
16
|
+
file_content_type: "file content type must be one of: %{allowed_content_types}"
|
14
17
|
integer: "must be an integer"
|
15
|
-
min: "must be
|
16
|
-
max: "must be
|
17
|
-
|
18
|
-
|
18
|
+
min: "must be at least %{min}"
|
19
|
+
max: "must be at most %{max}"
|
20
|
+
not_negative: "must not be negative"
|
21
|
+
not_positive: "must not be positive"
|
19
22
|
literal: "must be %{value}"
|
20
23
|
string: "must be a string"
|
21
24
|
empty: "must not be empty"
|
22
|
-
minlength: "length must be
|
23
|
-
maxlength: "length must be
|
25
|
+
minlength: "length must be at least %{minlength}"
|
26
|
+
maxlength: "length must be at most %{maxlength}"
|
24
27
|
format: "must have format %{regex}"
|
25
|
-
|
26
|
-
|
28
|
+
swagger:
|
29
|
+
agreement: "* Must be accepted (true)"
|
30
|
+
big_decimal_min: "* Minimum: %{min}"
|
31
|
+
big_decimal_max: "* Maximum: %{max}"
|
32
|
+
date_time_iso8601: "* Must be valid according to ISO 8601"
|
33
|
+
date_time_posix: "* POSIX time or Unix epoch is the amount of seconds since 1970-01-01"
|
34
|
+
file_max_size: "* Max size: %{max_size}"
|
35
|
+
file_content_types: "* Content types: %{content_types}"
|
36
|
+
hash_not_empty: "* Must have at least one value"
|
37
|
+
integer_not_positive: "* Must not be positive"
|
38
|
+
integer_only_positive: "* Must be positive"
|
39
|
+
integer_not_negative: "* Must not be negative"
|
40
|
+
integer_only_negative: "* Must be negative"
|
41
|
+
string_not_empty: "* Must not be empty"
|
42
|
+
string_downcase: "* Case insensitive"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::Documentation
|
4
|
+
class Builder
|
5
|
+
attr_reader :rails_engine, :sections, :swagger, :webpage
|
6
|
+
|
7
|
+
def initialize(rails_engine)
|
8
|
+
@rails_engine = rails_engine
|
9
|
+
@sections = []
|
10
|
+
@current_section = nil
|
11
|
+
@version = "1.0"
|
12
|
+
@swagger = Output::Swagger.new(self)
|
13
|
+
@webpage = Output::Webpage.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def page_title(text) = (@page_title = text)
|
17
|
+
def get_page_title = @page_title
|
18
|
+
|
19
|
+
def company_logo_url(url) = (@company_logo_url = url)
|
20
|
+
def get_company_logo_url = @company_logo_url
|
21
|
+
|
22
|
+
def version(version) = (@version = version)
|
23
|
+
def get_version = @version
|
24
|
+
|
25
|
+
def section(name, &block)
|
26
|
+
@current_section = Section.new(name:, pages: [])
|
27
|
+
|
28
|
+
block.call
|
29
|
+
|
30
|
+
@sections << @current_section
|
31
|
+
|
32
|
+
@current_section = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def requests
|
36
|
+
@sections.flat_map(&:pages).filter(&:request?).map(&:request)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(*requests, **opts)
|
40
|
+
raise ArgumentError(<<-MD) if @current_section.nil?
|
41
|
+
You must define a section before adding a page. For example:
|
42
|
+
|
43
|
+
section "Customers" do
|
44
|
+
add CustomersController::CreateRequest
|
45
|
+
end
|
46
|
+
MD
|
47
|
+
|
48
|
+
if requests.one?
|
49
|
+
@current_section.pages << Page::Request.new(request: requests.first)
|
50
|
+
elsif opts[:partial]
|
51
|
+
@current_section.pages << Page::Partial.new(title: opts[:title], partial: opts[:partial])
|
52
|
+
else
|
53
|
+
raise ArgumentError("expected request or a partial")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def merge_request_examples_from_file!
|
58
|
+
return if !Explicit.configuration.request_examples_file_path
|
59
|
+
|
60
|
+
encoded = ::File.read(Explicit.configuration.request_examples_file_path)
|
61
|
+
|
62
|
+
examples = ::JSON.parse(encoded)
|
63
|
+
|
64
|
+
requests.each do |request|
|
65
|
+
examples[request.gid]&.each do |example|
|
66
|
+
request.add_example(
|
67
|
+
params: example["params"].with_indifferent_access,
|
68
|
+
headers: example["headers"],
|
69
|
+
response: {
|
70
|
+
status: example.dig("response", "status"),
|
71
|
+
data: example.dig("response", "data").with_indifferent_access
|
72
|
+
}
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
rescue JSON::ParserError
|
77
|
+
::Rails.logger.error("[Explicit] Could not parse JSON in request examples file at #{Explicit.configuration.request_examples_file_path}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -3,19 +3,8 @@
|
|
3
3
|
module Explicit::Documentation::Markdown
|
4
4
|
extend self
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
markdown_text.each_char do |ch|
|
9
|
-
break if ch != " "
|
10
|
-
|
11
|
-
offset += 1
|
12
|
-
end
|
13
|
-
|
14
|
-
markdown_text = markdown_text.each_line.map do |line|
|
15
|
-
line[offset..-1] || "\n"
|
16
|
-
end
|
17
|
-
|
18
|
-
::Commonmarker.to_html(markdown_text.join, options: {
|
6
|
+
def to_html(markdown_text)
|
7
|
+
::Commonmarker.to_html(markdown_text, options: {
|
19
8
|
parse: { smart: true },
|
20
9
|
render: { hardbreaks: false }
|
21
10
|
}).html_safe
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::Documentation::Output
|
4
|
+
class Swagger
|
5
|
+
InconsistentBasePathError = Class.new(RuntimeError)
|
6
|
+
InconsistentBaseURLError = Class.new(RuntimeError)
|
7
|
+
|
8
|
+
attr_reader :builder
|
9
|
+
|
10
|
+
def initialize(builder)
|
11
|
+
@builder = builder
|
12
|
+
end
|
13
|
+
|
14
|
+
def swagger_document
|
15
|
+
{
|
16
|
+
openapi: "3.0.1",
|
17
|
+
info: {
|
18
|
+
title: builder.get_page_title,
|
19
|
+
version: builder.get_version
|
20
|
+
},
|
21
|
+
servers: [
|
22
|
+
{
|
23
|
+
url: get_base_url
|
24
|
+
}
|
25
|
+
],
|
26
|
+
tags: build_tags_from_sections,
|
27
|
+
paths: build_paths_from_requests
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(request)
|
32
|
+
@swagger_document ||= swagger_document
|
33
|
+
|
34
|
+
[200, {"Content-Type" => "application/json"}, [@swagger_document.to_json]]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def get_base_url
|
39
|
+
base_urls = builder.requests.map(&:get_base_url).uniq
|
40
|
+
base_paths = builder.requests.map(&:get_base_path).uniq
|
41
|
+
|
42
|
+
if !base_urls.one?
|
43
|
+
raise InconsistentBaseURLError.new <<~TXT
|
44
|
+
There are requests with different base URLs in the same documentation,
|
45
|
+
which is not supported by Swagger.
|
46
|
+
|
47
|
+
The following base URLs are present:
|
48
|
+
|
49
|
+
#{base_urls.join("\n ")}
|
50
|
+
|
51
|
+
Please make sure all requests have the same base URL.
|
52
|
+
TXT
|
53
|
+
end
|
54
|
+
|
55
|
+
if !base_paths.one?
|
56
|
+
raise InconsistentBasePathError.new <<~TXT
|
57
|
+
There are requests with different base paths in the same documentation,
|
58
|
+
which is not supported by Swagger.
|
59
|
+
|
60
|
+
The following base paths are present:
|
61
|
+
|
62
|
+
#{base_paths.join("\n ")}
|
63
|
+
|
64
|
+
Please make sure all requests have the same base path.
|
65
|
+
TXT
|
66
|
+
end
|
67
|
+
|
68
|
+
base_urls.first + base_paths.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_tags_from_sections
|
72
|
+
builder.sections.filter(&:contains_request?).map do |section|
|
73
|
+
{ name: section.name }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_paths_from_requests
|
78
|
+
paths = Hash.new { |hash, key| hash[key] = {} }
|
79
|
+
|
80
|
+
builder.sections.filter(&:contains_request?).each do |section|
|
81
|
+
section.pages.filter(&:request?).each do |page|
|
82
|
+
request = page.request
|
83
|
+
route = request.routes.first
|
84
|
+
|
85
|
+
paths[route.path_with_curly_syntax][route.method.to_s] = {
|
86
|
+
tags: [section.name],
|
87
|
+
summary: request.get_title,
|
88
|
+
description: request.get_description,
|
89
|
+
parameters: build_parameters(request),
|
90
|
+
requestBody: build_request_body(request),
|
91
|
+
responses: build_responses(request)
|
92
|
+
}.compact_blank
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
paths
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_parameters(request)
|
100
|
+
headers =
|
101
|
+
request.headers_type.attributes.map do |name, type|
|
102
|
+
{
|
103
|
+
name: name.to_s,
|
104
|
+
in: "header",
|
105
|
+
required: type.required?,
|
106
|
+
schema: type.swagger_schema,
|
107
|
+
style: "simple"
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
path_params =
|
112
|
+
request.params_type.path_params_type.attributes.map do |name, type|
|
113
|
+
{
|
114
|
+
name: name.to_s,
|
115
|
+
in: "path",
|
116
|
+
required: type.required?,
|
117
|
+
schema: type.swagger_schema,
|
118
|
+
style: "simple"
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
headers + path_params
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_request_body(request)
|
126
|
+
body_params_type = request.params_type.body_params_type
|
127
|
+
content_type = request.accepts_file_upload? ? "multipart/form-data" : "application/json"
|
128
|
+
|
129
|
+
return nil if body_params_type.attributes.empty?
|
130
|
+
|
131
|
+
examples =
|
132
|
+
request.examples
|
133
|
+
.flat_map { |_, examples| examples }
|
134
|
+
.filter_map do |example|
|
135
|
+
case body_params_type.validate(example.params)
|
136
|
+
in [:ok, validated_data] then validated_data
|
137
|
+
in [:error, _] then nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
.map.with_index { |example, index| [index, { value: example }] }
|
141
|
+
.to_h
|
142
|
+
|
143
|
+
{
|
144
|
+
content: {
|
145
|
+
content_type => {
|
146
|
+
schema: body_params_type.swagger_schema,
|
147
|
+
examples:
|
148
|
+
}.compact_blank
|
149
|
+
},
|
150
|
+
required: body_params_type.attributes.any?
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def build_responses(request)
|
155
|
+
responses = {}
|
156
|
+
|
157
|
+
request.responses.each do |status, _|
|
158
|
+
examples = request.examples[status].map.with_index do |example, index|
|
159
|
+
[index.to_s, { value: example.response.data }]
|
160
|
+
end.to_h
|
161
|
+
|
162
|
+
responses[status] = {
|
163
|
+
content: {
|
164
|
+
"application/json" => {
|
165
|
+
schema: request.responses_type(status:).swagger_schema,
|
166
|
+
examples: examples
|
167
|
+
}.compact_blank
|
168
|
+
},
|
169
|
+
description: Rack::Utils::HTTP_STATUS_CODES[status]
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
responses
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::Documentation::Output
|
4
|
+
class Webpage
|
5
|
+
attr_reader :builder
|
6
|
+
|
7
|
+
def initialize(builder)
|
8
|
+
@builder = builder
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(request)
|
12
|
+
@html ||= render_documentation_page
|
13
|
+
|
14
|
+
[200, {}, [@html]]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def render_documentation_page
|
19
|
+
Explicit::ApplicationController.render(
|
20
|
+
partial: "explicit/documentation/page",
|
21
|
+
locals: {
|
22
|
+
url_helpers: @builder.rails_engine.routes.url_helpers,
|
23
|
+
page_title: builder.get_page_title,
|
24
|
+
company_logo_url: builder.get_company_logo_url,
|
25
|
+
version: builder.get_version,
|
26
|
+
sections: builder.sections,
|
27
|
+
}
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::Documentation::Page
|
4
|
+
class Partial
|
5
|
+
attr_reader :title, :partial
|
6
|
+
|
7
|
+
def initialize(title:, partial:)
|
8
|
+
@title = title
|
9
|
+
@partial = partial
|
10
|
+
end
|
11
|
+
|
12
|
+
def request?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def anchor
|
17
|
+
title.gsub(" ", "-").downcase
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|