rapitapir 0.1.0
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/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,780 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Docs
|
5
|
+
# HTML documentation generator for RapiTapir APIs
|
6
|
+
# Generates interactive HTML documentation with Try-It functionality
|
7
|
+
class HtmlGenerator
|
8
|
+
attr_reader :endpoints, :config
|
9
|
+
|
10
|
+
def initialize(endpoints: [], config: {})
|
11
|
+
@endpoints = endpoints
|
12
|
+
@config = default_config.merge(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate
|
16
|
+
<<~HTML
|
17
|
+
<!DOCTYPE html>
|
18
|
+
<html lang="en">
|
19
|
+
<head>
|
20
|
+
<meta charset="UTF-8">
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
22
|
+
<title>#{config[:title]}</title>
|
23
|
+
#{generate_styles}
|
24
|
+
</head>
|
25
|
+
<body>
|
26
|
+
<div class="container">
|
27
|
+
#{generate_header}
|
28
|
+
#{generate_sidebar}
|
29
|
+
#{generate_main_content}
|
30
|
+
</div>
|
31
|
+
#{generate_scripts}
|
32
|
+
</body>
|
33
|
+
</html>
|
34
|
+
HTML
|
35
|
+
end
|
36
|
+
|
37
|
+
def save_to_file(filename)
|
38
|
+
content = generate
|
39
|
+
File.write(filename, content)
|
40
|
+
puts "HTML documentation saved to #{filename}"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_config
|
46
|
+
{
|
47
|
+
title: 'API Documentation',
|
48
|
+
description: 'Auto-generated API documentation',
|
49
|
+
version: '1.0.0',
|
50
|
+
base_url: 'http://localhost:4567',
|
51
|
+
theme: 'light',
|
52
|
+
include_try_it: true
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_styles
|
57
|
+
<<~CSS
|
58
|
+
<style>
|
59
|
+
* {
|
60
|
+
margin: 0;
|
61
|
+
padding: 0;
|
62
|
+
box-sizing: border-box;
|
63
|
+
}
|
64
|
+
|
65
|
+
body {
|
66
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
67
|
+
line-height: 1.6;
|
68
|
+
color: #333;
|
69
|
+
background-color: #f8f9fa;
|
70
|
+
}
|
71
|
+
|
72
|
+
.container {
|
73
|
+
display: flex;
|
74
|
+
min-height: 100vh;
|
75
|
+
}
|
76
|
+
|
77
|
+
.header {
|
78
|
+
position: fixed;
|
79
|
+
top: 0;
|
80
|
+
left: 0;
|
81
|
+
right: 0;
|
82
|
+
background: #fff;
|
83
|
+
border-bottom: 1px solid #e1e5e9;
|
84
|
+
padding: 1rem 2rem;
|
85
|
+
z-index: 1000;
|
86
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
87
|
+
}
|
88
|
+
|
89
|
+
.header h1 {
|
90
|
+
color: #2c3e50;
|
91
|
+
font-size: 1.5rem;
|
92
|
+
margin-bottom: 0.5rem;
|
93
|
+
}
|
94
|
+
|
95
|
+
.header .meta {
|
96
|
+
color: #6c757d;
|
97
|
+
font-size: 0.9rem;
|
98
|
+
}
|
99
|
+
|
100
|
+
.sidebar {
|
101
|
+
width: 300px;
|
102
|
+
background: #fff;
|
103
|
+
border-right: 1px solid #e1e5e9;
|
104
|
+
position: fixed;
|
105
|
+
top: 80px;
|
106
|
+
bottom: 0;
|
107
|
+
overflow-y: auto;
|
108
|
+
padding: 1rem;
|
109
|
+
}
|
110
|
+
|
111
|
+
.sidebar h3 {
|
112
|
+
color: #2c3e50;
|
113
|
+
margin-bottom: 1rem;
|
114
|
+
font-size: 1.1rem;
|
115
|
+
}
|
116
|
+
|
117
|
+
.sidebar ul {
|
118
|
+
list-style: none;
|
119
|
+
}
|
120
|
+
|
121
|
+
.sidebar li {
|
122
|
+
margin-bottom: 0.5rem;
|
123
|
+
}
|
124
|
+
|
125
|
+
.sidebar a {
|
126
|
+
text-decoration: none;
|
127
|
+
color: #495057;
|
128
|
+
padding: 0.5rem;
|
129
|
+
border-radius: 4px;
|
130
|
+
display: block;
|
131
|
+
transition: background-color 0.2s;
|
132
|
+
}
|
133
|
+
|
134
|
+
.sidebar a:hover {
|
135
|
+
background-color: #f8f9fa;
|
136
|
+
}
|
137
|
+
|
138
|
+
.method-badge {
|
139
|
+
display: inline-block;
|
140
|
+
padding: 0.2rem 0.5rem;
|
141
|
+
border-radius: 3px;
|
142
|
+
font-size: 0.7rem;
|
143
|
+
font-weight: bold;
|
144
|
+
margin-right: 0.5rem;
|
145
|
+
min-width: 45px;
|
146
|
+
text-align: center;
|
147
|
+
}
|
148
|
+
|
149
|
+
.method-get { background-color: #28a745; color: white; }
|
150
|
+
.method-post { background-color: #007bff; color: white; }
|
151
|
+
.method-put { background-color: #ffc107; color: black; }
|
152
|
+
.method-delete { background-color: #dc3545; color: white; }
|
153
|
+
.method-patch { background-color: #17a2b8; color: white; }
|
154
|
+
|
155
|
+
.main-content {
|
156
|
+
flex: 1;
|
157
|
+
margin-left: 300px;
|
158
|
+
margin-top: 80px;
|
159
|
+
padding: 2rem;
|
160
|
+
}
|
161
|
+
|
162
|
+
.endpoint {
|
163
|
+
background: #fff;
|
164
|
+
border-radius: 8px;
|
165
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
166
|
+
margin-bottom: 2rem;
|
167
|
+
overflow: hidden;
|
168
|
+
}
|
169
|
+
|
170
|
+
.endpoint-header {
|
171
|
+
padding: 1.5rem;
|
172
|
+
border-bottom: 1px solid #e1e5e9;
|
173
|
+
}
|
174
|
+
|
175
|
+
.endpoint-title {
|
176
|
+
display: flex;
|
177
|
+
align-items: center;
|
178
|
+
margin-bottom: 0.5rem;
|
179
|
+
}
|
180
|
+
|
181
|
+
.endpoint-path {
|
182
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
183
|
+
font-size: 1.1rem;
|
184
|
+
margin-left: 0.5rem;
|
185
|
+
}
|
186
|
+
|
187
|
+
.endpoint-description {
|
188
|
+
color: #6c757d;
|
189
|
+
margin-top: 0.5rem;
|
190
|
+
}
|
191
|
+
|
192
|
+
.endpoint-content {
|
193
|
+
padding: 1.5rem;
|
194
|
+
}
|
195
|
+
|
196
|
+
.section {
|
197
|
+
margin-bottom: 2rem;
|
198
|
+
}
|
199
|
+
|
200
|
+
.section h4 {
|
201
|
+
color: #2c3e50;
|
202
|
+
margin-bottom: 1rem;
|
203
|
+
padding-bottom: 0.5rem;
|
204
|
+
border-bottom: 2px solid #e1e5e9;
|
205
|
+
}
|
206
|
+
|
207
|
+
.params-table {
|
208
|
+
width: 100%;
|
209
|
+
border-collapse: collapse;
|
210
|
+
margin-bottom: 1rem;
|
211
|
+
}
|
212
|
+
|
213
|
+
.params-table th,
|
214
|
+
.params-table td {
|
215
|
+
padding: 0.75rem;
|
216
|
+
text-align: left;
|
217
|
+
border-bottom: 1px solid #e1e5e9;
|
218
|
+
}
|
219
|
+
|
220
|
+
.params-table th {
|
221
|
+
background-color: #f8f9fa;
|
222
|
+
font-weight: 600;
|
223
|
+
}
|
224
|
+
|
225
|
+
.param-name {
|
226
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
227
|
+
background-color: #f8f9fa;
|
228
|
+
padding: 0.2rem 0.4rem;
|
229
|
+
border-radius: 3px;
|
230
|
+
}
|
231
|
+
|
232
|
+
.code-block {
|
233
|
+
background-color: #f8f9fa;
|
234
|
+
border: 1px solid #e1e5e9;
|
235
|
+
border-radius: 4px;
|
236
|
+
padding: 1rem;
|
237
|
+
overflow-x: auto;
|
238
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
239
|
+
font-size: 0.9rem;
|
240
|
+
}
|
241
|
+
|
242
|
+
.try-it-section {
|
243
|
+
background-color: #f8f9fa;
|
244
|
+
border-radius: 8px;
|
245
|
+
padding: 1.5rem;
|
246
|
+
margin-top: 2rem;
|
247
|
+
}
|
248
|
+
|
249
|
+
.try-it-form {
|
250
|
+
display: grid;
|
251
|
+
gap: 1rem;
|
252
|
+
}
|
253
|
+
|
254
|
+
.form-group {
|
255
|
+
display: flex;
|
256
|
+
flex-direction: column;
|
257
|
+
}
|
258
|
+
|
259
|
+
.form-group label {
|
260
|
+
margin-bottom: 0.5rem;
|
261
|
+
font-weight: 600;
|
262
|
+
}
|
263
|
+
|
264
|
+
.form-group input,
|
265
|
+
.form-group textarea {
|
266
|
+
padding: 0.5rem;
|
267
|
+
border: 1px solid #ced4da;
|
268
|
+
border-radius: 4px;
|
269
|
+
font-family: inherit;
|
270
|
+
}
|
271
|
+
|
272
|
+
.btn {
|
273
|
+
padding: 0.75rem 1.5rem;
|
274
|
+
border: none;
|
275
|
+
border-radius: 4px;
|
276
|
+
cursor: pointer;
|
277
|
+
font-weight: 600;
|
278
|
+
transition: background-color 0.2s;
|
279
|
+
}
|
280
|
+
|
281
|
+
.btn-primary {
|
282
|
+
background-color: #007bff;
|
283
|
+
color: white;
|
284
|
+
}
|
285
|
+
|
286
|
+
.btn-primary:hover {
|
287
|
+
background-color: #0056b3;
|
288
|
+
}
|
289
|
+
|
290
|
+
.response-section {
|
291
|
+
margin-top: 1rem;
|
292
|
+
padding: 1rem;
|
293
|
+
background-color: #fff;
|
294
|
+
border-radius: 4px;
|
295
|
+
border: 1px solid #e1e5e9;
|
296
|
+
}
|
297
|
+
|
298
|
+
.required-badge {
|
299
|
+
background-color: #dc3545;
|
300
|
+
color: white;
|
301
|
+
padding: 0.1rem 0.3rem;
|
302
|
+
border-radius: 3px;
|
303
|
+
font-size: 0.7rem;
|
304
|
+
margin-left: 0.5rem;
|
305
|
+
}
|
306
|
+
|
307
|
+
.optional-badge {
|
308
|
+
background-color: #6c757d;
|
309
|
+
color: white;
|
310
|
+
padding: 0.1rem 0.3rem;
|
311
|
+
border-radius: 3px;
|
312
|
+
font-size: 0.7rem;
|
313
|
+
margin-left: 0.5rem;
|
314
|
+
}
|
315
|
+
</style>
|
316
|
+
CSS
|
317
|
+
end
|
318
|
+
|
319
|
+
def generate_header
|
320
|
+
<<~HTML
|
321
|
+
<div class="header">
|
322
|
+
<h1>#{config[:title]}</h1>
|
323
|
+
<div class="meta">
|
324
|
+
Version: #{config[:version]} | Base URL: <code>#{config[:base_url]}</code>
|
325
|
+
</div>
|
326
|
+
</div>
|
327
|
+
HTML
|
328
|
+
end
|
329
|
+
|
330
|
+
def generate_sidebar
|
331
|
+
nav_items = endpoints.map do |endpoint|
|
332
|
+
method = endpoint.method.to_s.upcase
|
333
|
+
path = endpoint.path
|
334
|
+
summary = endpoint.metadata[:summary] || path
|
335
|
+
anchor = generate_anchor(method, path)
|
336
|
+
method_class = "method-#{method.downcase}"
|
337
|
+
|
338
|
+
<<~HTML
|
339
|
+
<li>
|
340
|
+
<a href="##{anchor}">
|
341
|
+
<span class="method-badge #{method_class}">#{method}</span>
|
342
|
+
#{summary}
|
343
|
+
</a>
|
344
|
+
</li>
|
345
|
+
HTML
|
346
|
+
end
|
347
|
+
|
348
|
+
<<~HTML
|
349
|
+
<div class="sidebar">
|
350
|
+
<h3>Endpoints</h3>
|
351
|
+
<ul>
|
352
|
+
#{nav_items.join}
|
353
|
+
</ul>
|
354
|
+
</div>
|
355
|
+
HTML
|
356
|
+
end
|
357
|
+
|
358
|
+
def generate_main_content
|
359
|
+
endpoint_docs = endpoints.map { |endpoint| generate_endpoint_html(endpoint) }.join
|
360
|
+
|
361
|
+
<<~HTML
|
362
|
+
<div class="main-content">
|
363
|
+
#{endpoint_docs}
|
364
|
+
</div>
|
365
|
+
HTML
|
366
|
+
end
|
367
|
+
|
368
|
+
def generate_endpoint_html(endpoint)
|
369
|
+
method = endpoint.method.to_s.upcase
|
370
|
+
path = endpoint.path
|
371
|
+
endpoint_data = prepare_endpoint_data(method, path, endpoint)
|
372
|
+
sections = build_endpoint_sections(endpoint)
|
373
|
+
|
374
|
+
generate_endpoint_html_template(endpoint_data, sections)
|
375
|
+
end
|
376
|
+
|
377
|
+
def build_endpoint_sections(endpoint)
|
378
|
+
sections = []
|
379
|
+
|
380
|
+
sections << build_path_params_section(endpoint)
|
381
|
+
sections << build_query_params_section(endpoint)
|
382
|
+
sections << build_body_section(endpoint)
|
383
|
+
sections << build_response_section(endpoint)
|
384
|
+
sections << build_try_it_section(endpoint)
|
385
|
+
|
386
|
+
sections.compact
|
387
|
+
end
|
388
|
+
|
389
|
+
def build_path_params_section(endpoint)
|
390
|
+
path_params = endpoint.inputs.select { |input| input.kind == :path }
|
391
|
+
return nil unless path_params.any?
|
392
|
+
|
393
|
+
generate_params_section('Path Parameters', path_params)
|
394
|
+
end
|
395
|
+
|
396
|
+
def build_query_params_section(endpoint)
|
397
|
+
query_params = endpoint.inputs.select { |input| input.kind == :query }
|
398
|
+
return nil unless query_params.any?
|
399
|
+
|
400
|
+
generate_params_section('Query Parameters', query_params)
|
401
|
+
end
|
402
|
+
|
403
|
+
def build_body_section(endpoint)
|
404
|
+
body_param = endpoint.inputs.find { |input| input.kind == :body }
|
405
|
+
return nil unless body_param
|
406
|
+
|
407
|
+
generate_body_section(body_param)
|
408
|
+
end
|
409
|
+
|
410
|
+
def build_response_section(endpoint)
|
411
|
+
return nil unless endpoint.outputs.any?
|
412
|
+
|
413
|
+
generate_response_section(endpoint.outputs)
|
414
|
+
end
|
415
|
+
|
416
|
+
def build_try_it_section(endpoint)
|
417
|
+
return nil unless config[:include_try_it]
|
418
|
+
|
419
|
+
generate_try_it_section(endpoint)
|
420
|
+
end
|
421
|
+
|
422
|
+
def prepare_endpoint_data(method, path, endpoint)
|
423
|
+
{
|
424
|
+
method: method,
|
425
|
+
path: path,
|
426
|
+
anchor: generate_anchor(method, path),
|
427
|
+
method_class: "method-#{method.downcase}",
|
428
|
+
endpoint: endpoint
|
429
|
+
}
|
430
|
+
end
|
431
|
+
|
432
|
+
def generate_endpoint_html_template(endpoint_data, sections)
|
433
|
+
method = endpoint_data[:method]
|
434
|
+
path = endpoint_data[:path]
|
435
|
+
anchor = endpoint_data[:anchor]
|
436
|
+
method_class = endpoint_data[:method_class]
|
437
|
+
endpoint = endpoint_data[:endpoint]
|
438
|
+
|
439
|
+
<<~HTML
|
440
|
+
<div class="endpoint" id="#{anchor}">
|
441
|
+
<div class="endpoint-header">
|
442
|
+
<div class="endpoint-title">
|
443
|
+
<span class="method-badge #{method_class}">#{method}</span>
|
444
|
+
<span class="endpoint-path">#{path}</span>
|
445
|
+
</div>
|
446
|
+
#{build_endpoint_summary(endpoint)}
|
447
|
+
#{build_endpoint_description(endpoint)}
|
448
|
+
</div>
|
449
|
+
<div class="endpoint-content">
|
450
|
+
#{sections.join}
|
451
|
+
</div>
|
452
|
+
</div>
|
453
|
+
HTML
|
454
|
+
end
|
455
|
+
|
456
|
+
def build_endpoint_summary(endpoint)
|
457
|
+
return '' unless endpoint.metadata[:summary]
|
458
|
+
|
459
|
+
"<div class=\"endpoint-summary\"><strong>#{endpoint.metadata[:summary]}</strong></div>"
|
460
|
+
end
|
461
|
+
|
462
|
+
def build_endpoint_description(endpoint)
|
463
|
+
return '' unless endpoint.metadata[:description]
|
464
|
+
|
465
|
+
"<div class=\"endpoint-description\">#{endpoint.metadata[:description]}</div>"
|
466
|
+
end
|
467
|
+
|
468
|
+
def generate_params_section(title, params)
|
469
|
+
rows = params.map do |param|
|
470
|
+
required_badge = if param.required?
|
471
|
+
'<span class="required-badge">Required</span>'
|
472
|
+
else
|
473
|
+
'<span class="optional-badge">Optional</span>'
|
474
|
+
end
|
475
|
+
|
476
|
+
<<~HTML
|
477
|
+
<tr>
|
478
|
+
<td><code class="param-name">#{param.name}</code></td>
|
479
|
+
<td>#{format_type(param.type)}</td>
|
480
|
+
<td>#{required_badge}</td>
|
481
|
+
<td>#{(param.options && param.options[:description]) || 'No description'}</td>
|
482
|
+
</tr>
|
483
|
+
HTML
|
484
|
+
end
|
485
|
+
|
486
|
+
<<~HTML
|
487
|
+
<div class="section">
|
488
|
+
<h4>#{title}</h4>
|
489
|
+
<table class="params-table">
|
490
|
+
<thead>
|
491
|
+
<tr>
|
492
|
+
<th>Parameter</th>
|
493
|
+
<th>Type</th>
|
494
|
+
<th>Required</th>
|
495
|
+
<th>Description</th>
|
496
|
+
</tr>
|
497
|
+
</thead>
|
498
|
+
<tbody>
|
499
|
+
#{rows.join}
|
500
|
+
</tbody>
|
501
|
+
</table>
|
502
|
+
</div>
|
503
|
+
HTML
|
504
|
+
end
|
505
|
+
|
506
|
+
def generate_body_section(body_param)
|
507
|
+
example = format_schema_example(body_param.type)
|
508
|
+
|
509
|
+
<<~HTML
|
510
|
+
<div class="section">
|
511
|
+
<h4>Request Body</h4>
|
512
|
+
<p><strong>Content-Type:</strong> <code>application/json</code></p>
|
513
|
+
<div class="code-block">#{html_escape(example)}</div>
|
514
|
+
</div>
|
515
|
+
HTML
|
516
|
+
end
|
517
|
+
|
518
|
+
def generate_response_section(outputs)
|
519
|
+
response_content = outputs.map do |output|
|
520
|
+
if output.kind == :json
|
521
|
+
example = format_schema_example(output.type)
|
522
|
+
<<~HTML
|
523
|
+
<p><strong>Content-Type:</strong> <code>application/json</code></p>
|
524
|
+
<div class="code-block">#{html_escape(example)}</div>
|
525
|
+
HTML
|
526
|
+
elsif output.kind == :status
|
527
|
+
"<p><strong>Status Code:</strong> #{output.type}</p>"
|
528
|
+
end
|
529
|
+
end.join
|
530
|
+
|
531
|
+
<<~HTML
|
532
|
+
<div class="section">
|
533
|
+
<h4>Response</h4>
|
534
|
+
#{response_content}
|
535
|
+
</div>
|
536
|
+
HTML
|
537
|
+
end
|
538
|
+
|
539
|
+
def generate_try_it_section(endpoint)
|
540
|
+
method = endpoint.method.to_s.upcase
|
541
|
+
path = endpoint.path
|
542
|
+
endpoint_id = generate_anchor(method, path).gsub('-', '_')
|
543
|
+
|
544
|
+
form_fields = build_try_it_form_fields(endpoint, endpoint_id)
|
545
|
+
|
546
|
+
<<~HTML
|
547
|
+
<div class="try-it-section">
|
548
|
+
<h4>Try it out</h4>
|
549
|
+
<form class="try-it-form" onsubmit="return tryRequest('#{endpoint_id}', '#{method}', '#{path}')">
|
550
|
+
#{form_fields.join}
|
551
|
+
<button type="submit" class="btn btn-primary">Send Request</button>
|
552
|
+
</form>
|
553
|
+
<div id="#{endpoint_id}_response" class="response-section" style="display: none;">
|
554
|
+
<h5>Response</h5>
|
555
|
+
<div class="code-block" id="#{endpoint_id}_response_content"></div>
|
556
|
+
</div>
|
557
|
+
</div>
|
558
|
+
HTML
|
559
|
+
end
|
560
|
+
|
561
|
+
def build_try_it_form_fields(endpoint, endpoint_id)
|
562
|
+
form_fields = []
|
563
|
+
|
564
|
+
form_fields.concat(build_path_parameter_fields(endpoint, endpoint_id))
|
565
|
+
form_fields.concat(build_query_parameter_fields(endpoint, endpoint_id))
|
566
|
+
form_fields.concat(build_body_parameter_field(endpoint, endpoint_id))
|
567
|
+
|
568
|
+
form_fields
|
569
|
+
end
|
570
|
+
|
571
|
+
def build_path_parameter_fields(endpoint, endpoint_id)
|
572
|
+
path_params = endpoint.inputs.select { |input| input.kind == :path }
|
573
|
+
path_params.map do |param|
|
574
|
+
<<~HTML
|
575
|
+
<div class="form-group">
|
576
|
+
<label for="#{endpoint_id}_#{param.name}">#{param.name} (path parameter)</label>
|
577
|
+
<input type="text" id="#{endpoint_id}_#{param.name}" name="#{param.name}" placeholder="Enter #{param.name}" required>
|
578
|
+
</div>
|
579
|
+
HTML
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
def build_query_parameter_fields(endpoint, endpoint_id)
|
584
|
+
query_params = endpoint.inputs.select { |input| input.kind == :query }
|
585
|
+
query_params.map do |param|
|
586
|
+
required = param.required? ? 'required' : ''
|
587
|
+
<<~HTML
|
588
|
+
<div class="form-group">
|
589
|
+
<label for="#{endpoint_id}_#{param.name}">#{param.name} (query parameter)</label>
|
590
|
+
<input type="text" id="#{endpoint_id}_#{param.name}" name="#{param.name}" placeholder="Enter #{param.name}" #{required}>
|
591
|
+
</div>
|
592
|
+
HTML
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
def build_body_parameter_field(endpoint, endpoint_id)
|
597
|
+
body_param = endpoint.inputs.find { |input| input.kind == :body }
|
598
|
+
return [] unless body_param
|
599
|
+
|
600
|
+
example = format_schema_example(body_param.type)
|
601
|
+
[<<~HTML]
|
602
|
+
<div class="form-group">
|
603
|
+
<label for="#{endpoint_id}_body">Request Body (JSON)</label>
|
604
|
+
<textarea id="#{endpoint_id}_body" name="body" rows="6" placeholder="Enter JSON body">#{html_escape(example)}</textarea>
|
605
|
+
</div>
|
606
|
+
HTML
|
607
|
+
end
|
608
|
+
|
609
|
+
def generate_scripts
|
610
|
+
<<~JAVASCRIPT
|
611
|
+
<script>
|
612
|
+
async function tryRequest(endpointId, method, path) {
|
613
|
+
event.preventDefault();
|
614
|
+
#{' '}
|
615
|
+
const form = event.target;
|
616
|
+
const formData = new FormData(form);
|
617
|
+
#{' '}
|
618
|
+
// Build URL with path parameters
|
619
|
+
let url = '#{config[:base_url]}' + path;
|
620
|
+
const pathParams = {};
|
621
|
+
const queryParams = {};
|
622
|
+
let body = null;
|
623
|
+
#{' '}
|
624
|
+
// Process form data
|
625
|
+
for (const [key, value] of formData.entries()) {
|
626
|
+
if (key === 'body') {
|
627
|
+
if (value.trim()) {
|
628
|
+
try {
|
629
|
+
body = JSON.parse(value);
|
630
|
+
} catch (e) {
|
631
|
+
alert('Invalid JSON in request body');
|
632
|
+
return false;
|
633
|
+
}
|
634
|
+
}
|
635
|
+
} else if (path.includes(':' + key)) {
|
636
|
+
pathParams[key] = value;
|
637
|
+
} else if (value.trim()) {
|
638
|
+
queryParams[key] = value;
|
639
|
+
}
|
640
|
+
}
|
641
|
+
#{' '}
|
642
|
+
// Replace path parameters
|
643
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
644
|
+
url = url.replace(':' + key, encodeURIComponent(value));
|
645
|
+
}
|
646
|
+
#{' '}
|
647
|
+
// Add query parameters
|
648
|
+
const queryString = new URLSearchParams(queryParams).toString();
|
649
|
+
if (queryString) {
|
650
|
+
url += '?' + queryString;
|
651
|
+
}
|
652
|
+
#{' '}
|
653
|
+
// Prepare request options
|
654
|
+
const options = {
|
655
|
+
method: method,
|
656
|
+
headers: {
|
657
|
+
'Content-Type': 'application/json',
|
658
|
+
'Accept': 'application/json'
|
659
|
+
}
|
660
|
+
};
|
661
|
+
#{' '}
|
662
|
+
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
663
|
+
options.body = JSON.stringify(body);
|
664
|
+
}
|
665
|
+
#{' '}
|
666
|
+
// Show loading state
|
667
|
+
const responseDiv = document.getElementById(endpointId + '_response');
|
668
|
+
const responseContent = document.getElementById(endpointId + '_response_content');
|
669
|
+
responseDiv.style.display = 'block';
|
670
|
+
responseContent.textContent = 'Loading...';
|
671
|
+
#{' '}
|
672
|
+
try {
|
673
|
+
const response = await fetch(url, options);
|
674
|
+
const responseText = await response.text();
|
675
|
+
#{' '}
|
676
|
+
let responseData;
|
677
|
+
try {
|
678
|
+
responseData = JSON.parse(responseText);
|
679
|
+
responseContent.innerHTML = `
|
680
|
+
<strong>Status:</strong> ${response.status} ${response.statusText}<br><br>
|
681
|
+
<strong>Response:</strong><br>
|
682
|
+
${JSON.stringify(responseData, null, 2)}
|
683
|
+
`;
|
684
|
+
} catch (e) {
|
685
|
+
responseContent.innerHTML = `
|
686
|
+
<strong>Status:</strong> ${response.status} ${response.statusText}<br><br>
|
687
|
+
<strong>Response:</strong><br>
|
688
|
+
${responseText}
|
689
|
+
`;
|
690
|
+
}
|
691
|
+
} catch (error) {
|
692
|
+
responseContent.innerHTML = `
|
693
|
+
<strong>Error:</strong><br>
|
694
|
+
${error.message}
|
695
|
+
`;
|
696
|
+
}
|
697
|
+
#{' '}
|
698
|
+
return false;
|
699
|
+
}
|
700
|
+
</script>
|
701
|
+
JAVASCRIPT
|
702
|
+
end
|
703
|
+
|
704
|
+
def format_schema_example(schema)
|
705
|
+
case schema
|
706
|
+
when Hash
|
707
|
+
JSON.pretty_generate(
|
708
|
+
schema.transform_values { |v| generate_example_value(v) }
|
709
|
+
)
|
710
|
+
when Array
|
711
|
+
if schema.length == 1
|
712
|
+
JSON.pretty_generate([generate_example_value(schema.first)])
|
713
|
+
else
|
714
|
+
'[]'
|
715
|
+
end
|
716
|
+
else
|
717
|
+
generate_example_value(schema).to_s
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
def generate_example_value(type)
|
722
|
+
case type
|
723
|
+
when :string, String then 'example string'
|
724
|
+
when :integer, Integer then 123
|
725
|
+
when :float, Float then 123.45
|
726
|
+
when :boolean then true
|
727
|
+
when :date then '2025-01-15'
|
728
|
+
when :datetime then '2025-01-15T10:30:00Z'
|
729
|
+
else
|
730
|
+
generate_complex_example_value(type)
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
def generate_complex_example_value(type)
|
735
|
+
case type
|
736
|
+
when Hash then type.transform_values { |v| generate_example_value(v) }
|
737
|
+
when Array then generate_array_example_value(type)
|
738
|
+
else 'example'
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
def generate_array_example_value(type)
|
743
|
+
type.length == 1 ? [generate_example_value(type.first)] : []
|
744
|
+
end
|
745
|
+
|
746
|
+
def format_type(type)
|
747
|
+
case type
|
748
|
+
when :string, String then 'string'
|
749
|
+
when :integer, Integer then 'integer'
|
750
|
+
when :float, Float then 'number'
|
751
|
+
when :boolean then 'boolean'
|
752
|
+
when :date then 'date'
|
753
|
+
when :datetime then 'datetime'
|
754
|
+
else
|
755
|
+
format_complex_type(type)
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
def format_complex_type(type)
|
760
|
+
return 'object' if type == Hash
|
761
|
+
return 'array' if type == Array
|
762
|
+
|
763
|
+
type.to_s
|
764
|
+
end
|
765
|
+
|
766
|
+
def generate_anchor(method, path)
|
767
|
+
"#{method.downcase}-#{path.gsub('/', '').gsub(':', '')}"
|
768
|
+
end
|
769
|
+
|
770
|
+
def html_escape(text)
|
771
|
+
text.to_s
|
772
|
+
.gsub('&', '&')
|
773
|
+
.gsub('<', '<')
|
774
|
+
.gsub('>', '>')
|
775
|
+
.gsub('"', '"')
|
776
|
+
.gsub("'", ''')
|
777
|
+
end
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|