explicit 0.2.1 → 0.2.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -67
  3. data/app/helpers/explicit/application_helper.rb +32 -0
  4. data/app/views/explicit/documentation/_attribute.html.erb +38 -0
  5. data/app/views/explicit/documentation/_page.html.erb +166 -0
  6. data/app/views/explicit/documentation/_request.html.erb +87 -0
  7. data/app/views/explicit/documentation/request/_examples.html.erb +50 -0
  8. data/app/views/explicit/documentation/type/_agreement.html.erb +7 -0
  9. data/app/views/explicit/documentation/type/_array.html.erb +3 -0
  10. data/app/views/explicit/documentation/type/_big_decimal.html.erb +4 -0
  11. data/app/views/explicit/documentation/type/_boolean.html.erb +7 -0
  12. data/app/views/explicit/documentation/type/_date_time_iso8601.html.erb +3 -0
  13. data/app/views/explicit/documentation/type/_date_time_posix.html.erb +3 -0
  14. data/app/views/explicit/documentation/type/_enum.html.erb +7 -0
  15. data/app/views/explicit/documentation/type/_file.html.erb +9 -0
  16. data/app/views/explicit/documentation/type/_hash.html.erb +4 -0
  17. data/app/views/explicit/documentation/type/_integer.html.erb +25 -0
  18. data/app/views/explicit/documentation/type/_one_of.html.erb +11 -0
  19. data/app/views/explicit/documentation/type/_record.html.erb +9 -0
  20. data/app/views/explicit/documentation/type/_string.html.erb +21 -0
  21. data/config/locales/en.yml +27 -11
  22. data/lib/explicit/configuration.rb +1 -1
  23. data/lib/explicit/documentation/builder.rb +80 -0
  24. data/lib/explicit/documentation/markdown.rb +2 -13
  25. data/lib/explicit/documentation/output/swagger.rb +176 -0
  26. data/lib/explicit/documentation/output/webpage.rb +31 -0
  27. data/lib/explicit/documentation/page/partial.rb +20 -0
  28. data/lib/explicit/documentation/page/request.rb +27 -0
  29. data/lib/explicit/documentation/section.rb +9 -0
  30. data/lib/explicit/documentation.rb +12 -145
  31. data/lib/explicit/request/example.rb +50 -1
  32. data/lib/explicit/request/invalid_params_error.rb +1 -3
  33. data/lib/explicit/request/invalid_response_error.rb +2 -15
  34. data/lib/explicit/request/route.rb +18 -0
  35. data/lib/explicit/request.rb +43 -24
  36. data/lib/explicit/test_helper/example_recorder.rb +7 -2
  37. data/lib/explicit/test_helper.rb +25 -7
  38. data/lib/explicit/type/agreement.rb +39 -0
  39. data/lib/explicit/type/array.rb +56 -0
  40. data/lib/explicit/type/big_decimal.rb +58 -0
  41. data/lib/explicit/type/boolean.rb +47 -0
  42. data/lib/explicit/type/date_time_iso8601.rb +41 -0
  43. data/lib/explicit/type/date_time_posix.rb +44 -0
  44. data/lib/explicit/type/enum.rb +41 -0
  45. data/lib/explicit/type/file.rb +60 -0
  46. data/lib/explicit/type/hash.rb +57 -0
  47. data/lib/explicit/type/integer.rb +79 -0
  48. data/lib/explicit/type/literal.rb +45 -0
  49. data/lib/explicit/type/modifiers/default.rb +24 -0
  50. data/lib/explicit/type/modifiers/description.rb +11 -0
  51. data/lib/explicit/type/modifiers/nilable.rb +19 -0
  52. data/lib/explicit/type/modifiers/param_location.rb +11 -0
  53. data/lib/explicit/type/one_of.rb +46 -0
  54. data/lib/explicit/type/record.rb +96 -0
  55. data/lib/explicit/type/string.rb +68 -0
  56. data/lib/explicit/type.rb +112 -0
  57. data/lib/explicit/version.rb +1 -1
  58. data/lib/explicit.rb +28 -18
  59. metadata +47 -25
  60. data/app/views/explicit/application/_documentation.html.erb +0 -136
  61. data/app/views/explicit/application/_request.html.erb +0 -37
  62. data/lib/explicit/documentation/property.rb +0 -19
  63. data/lib/explicit/spec/agreement.rb +0 -17
  64. data/lib/explicit/spec/array.rb +0 -28
  65. data/lib/explicit/spec/bigdecimal.rb +0 -27
  66. data/lib/explicit/spec/boolean.rb +0 -30
  67. data/lib/explicit/spec/date_time_iso8601.rb +0 -17
  68. data/lib/explicit/spec/date_time_posix.rb +0 -21
  69. data/lib/explicit/spec/default.rb +0 -20
  70. data/lib/explicit/spec/error.rb +0 -63
  71. data/lib/explicit/spec/hash.rb +0 -30
  72. data/lib/explicit/spec/inclusion.rb +0 -15
  73. data/lib/explicit/spec/integer.rb +0 -53
  74. data/lib/explicit/spec/literal.rb +0 -15
  75. data/lib/explicit/spec/nilable.rb +0 -15
  76. data/lib/explicit/spec/one_of.rb +0 -40
  77. data/lib/explicit/spec/record.rb +0 -33
  78. data/lib/explicit/spec/string.rb +0 -50
  79. 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,7 @@
1
+ The following values are accepted:
2
+
3
+ <div class="inline-flex flex-wrap gap-2">
4
+ <% Explicit::Type::Agreement::VALUES.each do |value| %>
5
+ <code class="font-mono text-xs bg-neutral-200 p-px"><%= value.inspect %></code>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="border divide-y">
2
+ <%= type_attribute_render name: "Items", type: type.itemtype %>
3
+ </div>
@@ -0,0 +1,4 @@
1
+ <%= type_constraints do %>
2
+ <%= type_constraint "min:", type.min if type.min.present? %>
3
+ <%= type_constraint "max:", type.max if type.max.present? %>
4
+ <% end %>
@@ -0,0 +1,7 @@
1
+ The following values are accepted:
2
+
3
+ <div class="inline-flex flex-wrap gap-2">
4
+ <% Explicit::Type::Boolean::VALUES.each do |value, _| %>
5
+ <code class="font-mono text-xs bg-neutral-200 p-px"><%= value.inspect %></code>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="text-sm">
2
+ Value must be a valid date time according to ISO 8601. For example: <code>"2024-12-10T14:21:00Z"</code>
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="text-sm">
2
+ Value must be a valid POSIX timestamp. For example: <code>1733923153</code> represents "Dec 11 2024 13:19:13"
3
+ </div>
@@ -0,0 +1,7 @@
1
+ The following values are accepted:
2
+
3
+ <div class="inline-flex flex-wrap gap-2">
4
+ <% type.allowed_values.each do |value, _| %>
5
+ <code class="font-mono text-xs bg-neutral-200 p-px"><%= value.inspect %></code>
6
+ <% end %>
7
+ </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,4 @@
1
+ <div class="border divide-y">
2
+ <%= type_attribute_render name: "Keys", type: type.keytype %>
3
+ <%= type_attribute_render name: "Values", type: type.valuetype %>
4
+ </div>
@@ -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 %>
@@ -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 timestamps"
10
- hash: "must be a map"
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
- inclusion: "must be one of: %{values}"
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 bigger than or equal to %{min}"
16
- max: "must be smaller than or equal to %{max}"
17
- negative: "must not be negative"
18
- positive: "must not be positive"
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 greater than or equal to %{minlength}"
23
- maxlength: "length must be smaller than or equal to %{maxlength}"
25
+ minlength: "length must be at least %{minlength}"
26
+ maxlength: "length must be at most %{maxlength}"
24
27
  format: "must have format %{regex}"
25
- documentation:
26
- params_header: Request params
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"
@@ -17,7 +17,7 @@ module Explicit
17
17
  end
18
18
 
19
19
  def request_examples_persistance_enabled?
20
- ENV["EXPLICIT_PERSIST_EXAMPLES"].in? %w[true 1 on]
20
+ ENV["UPDATE_REQUEST_EXAMPLES"].in? %w[true 1 on ok]
21
21
  end
22
22
 
23
23
  def rescue_from_invalid_params=(enabled)
@@ -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 render(markdown_text)
7
- offset = 0
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