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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/CHANGELOG.md +94 -0
  5. data/CLEANUP_SUMMARY.md +155 -0
  6. data/CONTRIBUTING.md +280 -0
  7. data/LICENSE +21 -0
  8. data/README.md +485 -0
  9. data/debug_hash.rb +20 -0
  10. data/docs/EXTENSION_COMPARISON.md +388 -0
  11. data/docs/SINATRA_EXTENSION.md +467 -0
  12. data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
  13. data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
  14. data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
  15. data/docs/archive/PHASE_2_SUMMARY.md +209 -0
  16. data/docs/archive/REFACTORING_SUMMARY.md +184 -0
  17. data/docs/archive/phase_1_3_plan.md +136 -0
  18. data/docs/archive/sinatra_extension_summary.md +188 -0
  19. data/docs/archive/sinatra_working_solution.md +113 -0
  20. data/docs/archive/typescript-client-generator-summary.md +259 -0
  21. data/docs/auto-derivation.md +146 -0
  22. data/docs/blueprint.md +1091 -0
  23. data/docs/endpoint-definition.md +211 -0
  24. data/docs/github_pages_fix.md +52 -0
  25. data/docs/github_pages_setup.md +49 -0
  26. data/docs/implementation-status.md +357 -0
  27. data/docs/observability.md +647 -0
  28. data/docs/phase3-plan.md +108 -0
  29. data/docs/sinatra_rapitapir.md +87 -0
  30. data/docs/type_shortcuts.md +146 -0
  31. data/examples/README_ENTERPRISE.md +202 -0
  32. data/examples/authentication_example.rb +192 -0
  33. data/examples/auto_derivation_ruby_friendly.rb +163 -0
  34. data/examples/cli/user_api_endpoints.rb +56 -0
  35. data/examples/client/typescript_client_example.rb +102 -0
  36. data/examples/client/user-api-client.ts +193 -0
  37. data/examples/demo_api.rb +41 -0
  38. data/examples/docs/documentation_example.rb +112 -0
  39. data/examples/docs/user-api-docs.html +789 -0
  40. data/examples/docs/user-api-docs.md +403 -0
  41. data/examples/enhanced_auto_derivation_test.rb +83 -0
  42. data/examples/enterprise_extension_demo.rb +417 -0
  43. data/examples/enterprise_rapitapir_api.rb +662 -0
  44. data/examples/getting_started_extension.rb +218 -0
  45. data/examples/hello_world.rb +74 -0
  46. data/examples/oauth2/.env.example +19 -0
  47. data/examples/oauth2/README.md +205 -0
  48. data/examples/oauth2/generic_oauth2_api.rb +226 -0
  49. data/examples/oauth2/get_token.rb +72 -0
  50. data/examples/oauth2/songs_api_with_auth0.rb +320 -0
  51. data/examples/oauth2/test_api.sh +16 -0
  52. data/examples/oauth2/test_songs_api.sh +110 -0
  53. data/examples/observability/.env.example +35 -0
  54. data/examples/observability/README.md +230 -0
  55. data/examples/observability/README_HONEYCOMB.md +332 -0
  56. data/examples/observability/advanced_setup.rb +384 -0
  57. data/examples/observability/basic_setup.rb +192 -0
  58. data/examples/observability/complete_test.rb +121 -0
  59. data/examples/observability/honeycomb_example.rb +523 -0
  60. data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
  61. data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
  62. data/examples/observability/honeycomb_working_example.rb +489 -0
  63. data/examples/observability/quick_test.rb +78 -0
  64. data/examples/observability/simple_test.rb +14 -0
  65. data/examples/observability/test_honeycomb_demo.rb +354 -0
  66. data/examples/observability/test_live_honeycomb.rb +111 -0
  67. data/examples/observability/test_validation.rb +78 -0
  68. data/examples/observability/test_working_validation.rb +66 -0
  69. data/examples/openapi/user_api_schema.rb +132 -0
  70. data/examples/production_ready_example.rb +105 -0
  71. data/examples/rails/users_controller.rb +146 -0
  72. data/examples/readme/basic_sinatra_example.rb +128 -0
  73. data/examples/server/user_api.rb +179 -0
  74. data/examples/simple_auto_derivation_demo.rb +44 -0
  75. data/examples/simple_demo_api.rb +18 -0
  76. data/examples/sinatra/user_app.rb +127 -0
  77. data/examples/t_shortcut_demo.rb +59 -0
  78. data/examples/user_api.rb +190 -0
  79. data/examples/working_getting_started.rb +184 -0
  80. data/examples/working_simple_example.rb +195 -0
  81. data/lib/rapitapir/auth/configuration.rb +129 -0
  82. data/lib/rapitapir/auth/context.rb +122 -0
  83. data/lib/rapitapir/auth/errors.rb +104 -0
  84. data/lib/rapitapir/auth/middleware.rb +324 -0
  85. data/lib/rapitapir/auth/oauth2.rb +350 -0
  86. data/lib/rapitapir/auth/schemes.rb +420 -0
  87. data/lib/rapitapir/auth.rb +113 -0
  88. data/lib/rapitapir/cli/command.rb +535 -0
  89. data/lib/rapitapir/cli/server.rb +243 -0
  90. data/lib/rapitapir/cli/validator.rb +373 -0
  91. data/lib/rapitapir/client/generator_base.rb +272 -0
  92. data/lib/rapitapir/client/typescript_generator.rb +350 -0
  93. data/lib/rapitapir/core/endpoint.rb +158 -0
  94. data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
  95. data/lib/rapitapir/core/input.rb +182 -0
  96. data/lib/rapitapir/core/output.rb +164 -0
  97. data/lib/rapitapir/core/request.rb +19 -0
  98. data/lib/rapitapir/core/response.rb +17 -0
  99. data/lib/rapitapir/docs/html_generator.rb +780 -0
  100. data/lib/rapitapir/docs/markdown_generator.rb +464 -0
  101. data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
  102. data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
  103. data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
  104. data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
  105. data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
  106. data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
  107. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
  108. data/lib/rapitapir/dsl/http_verbs.rb +77 -0
  109. data/lib/rapitapir/dsl/input_methods.rb +47 -0
  110. data/lib/rapitapir/dsl/observability_methods.rb +81 -0
  111. data/lib/rapitapir/dsl/output_methods.rb +43 -0
  112. data/lib/rapitapir/dsl/type_resolution.rb +43 -0
  113. data/lib/rapitapir/observability/configuration.rb +108 -0
  114. data/lib/rapitapir/observability/health_check.rb +236 -0
  115. data/lib/rapitapir/observability/logging.rb +270 -0
  116. data/lib/rapitapir/observability/metrics.rb +203 -0
  117. data/lib/rapitapir/observability/middleware.rb +243 -0
  118. data/lib/rapitapir/observability/tracing.rb +143 -0
  119. data/lib/rapitapir/observability.rb +28 -0
  120. data/lib/rapitapir/openapi/schema_generator.rb +403 -0
  121. data/lib/rapitapir/schema.rb +136 -0
  122. data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
  123. data/lib/rapitapir/server/middleware.rb +120 -0
  124. data/lib/rapitapir/server/path_matcher.rb +45 -0
  125. data/lib/rapitapir/server/rack_adapter.rb +215 -0
  126. data/lib/rapitapir/server/rails_adapter.rb +17 -0
  127. data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
  128. data/lib/rapitapir/server/rails_controller.rb +72 -0
  129. data/lib/rapitapir/server/rails_input_processor.rb +73 -0
  130. data/lib/rapitapir/server/rails_response_handler.rb +29 -0
  131. data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
  132. data/lib/rapitapir/server/sinatra_integration.rb +93 -0
  133. data/lib/rapitapir/sinatra/configuration.rb +91 -0
  134. data/lib/rapitapir/sinatra/extension.rb +214 -0
  135. data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
  136. data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
  137. data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
  138. data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
  139. data/lib/rapitapir/types/array.rb +163 -0
  140. data/lib/rapitapir/types/auto_derivation.rb +265 -0
  141. data/lib/rapitapir/types/base.rb +146 -0
  142. data/lib/rapitapir/types/boolean.rb +46 -0
  143. data/lib/rapitapir/types/date.rb +92 -0
  144. data/lib/rapitapir/types/datetime.rb +98 -0
  145. data/lib/rapitapir/types/email.rb +32 -0
  146. data/lib/rapitapir/types/float.rb +134 -0
  147. data/lib/rapitapir/types/hash.rb +161 -0
  148. data/lib/rapitapir/types/integer.rb +143 -0
  149. data/lib/rapitapir/types/object.rb +156 -0
  150. data/lib/rapitapir/types/optional.rb +65 -0
  151. data/lib/rapitapir/types/string.rb +185 -0
  152. data/lib/rapitapir/types/uuid.rb +32 -0
  153. data/lib/rapitapir/types.rb +155 -0
  154. data/lib/rapitapir/version.rb +5 -0
  155. data/lib/rapitapir.rb +173 -0
  156. data/rapitapir.gemspec +66 -0
  157. 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('&', '&amp;')
773
+ .gsub('<', '&lt;')
774
+ .gsub('>', '&gt;')
775
+ .gsub('"', '&quot;')
776
+ .gsub("'", '&#39;')
777
+ end
778
+ end
779
+ end
780
+ end