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,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Docs
5
+ # Markdown documentation generator for APIs
6
+ # Generates human-readable Markdown documentation from endpoint definitions
7
+ class MarkdownGenerator
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
+ [
17
+ generate_header,
18
+ generate_table_of_contents,
19
+ generate_endpoints_documentation,
20
+ generate_footer
21
+ ].join("\n\n")
22
+ end
23
+
24
+ def save_to_file(filename)
25
+ content = generate
26
+ File.write(filename, content)
27
+ puts "Documentation saved to #{filename}"
28
+ end
29
+
30
+ private
31
+
32
+ def default_config
33
+ {
34
+ title: 'API Documentation',
35
+ description: 'Auto-generated API documentation',
36
+ version: '1.0.0',
37
+ base_url: 'http://localhost:4567',
38
+ include_toc: true,
39
+ include_examples: true
40
+ }
41
+ end
42
+
43
+ def generate_header
44
+ <<~MARKDOWN
45
+ # #{config[:title]}
46
+
47
+ #{config[:description]}
48
+
49
+ **Version:** #{config[:version]}#{' '}
50
+ **Base URL:** `#{config[:base_url]}`
51
+
52
+ ---
53
+ MARKDOWN
54
+ end
55
+
56
+ def generate_table_of_contents
57
+ return '' unless config[:include_toc]
58
+
59
+ toc_items = endpoints.map do |endpoint|
60
+ method = endpoint.method.to_s.upcase
61
+ path = endpoint.path
62
+ summary = endpoint.metadata[:summary] || "#{method} #{path}"
63
+ anchor = generate_anchor(method, path)
64
+
65
+ "- [#{method} #{path}](##{anchor}) - #{summary}"
66
+ end
67
+
68
+ <<~MARKDOWN
69
+ ## Table of Contents
70
+
71
+ #{toc_items.join("\n")}
72
+
73
+ ---
74
+ MARKDOWN
75
+ end
76
+
77
+ def generate_endpoints_documentation
78
+ endpoints.map { |endpoint| generate_endpoint_doc(endpoint) }.join("\n\n---\n\n")
79
+ end
80
+
81
+ def generate_endpoint_doc(endpoint)
82
+ doc = []
83
+
84
+ doc << generate_endpoint_header(endpoint)
85
+ doc.concat(generate_all_endpoint_sections(endpoint))
86
+ doc << generate_endpoint_examples(endpoint) if config[:include_examples]
87
+
88
+ doc.join("\n\n")
89
+ end
90
+
91
+ def generate_endpoint_header(endpoint)
92
+ method = endpoint.method.to_s.upcase
93
+ path = endpoint.path
94
+ anchor = generate_anchor(method, path)
95
+ "## #{method} #{path} {##{anchor}}"
96
+ end
97
+
98
+ def generate_all_endpoint_sections(endpoint)
99
+ sections = []
100
+ sections.concat(generate_metadata_section(endpoint))
101
+ sections.concat(generate_path_parameters_section(endpoint))
102
+ sections.concat(generate_query_parameters_section(endpoint))
103
+ sections.concat(generate_request_body_section(endpoint))
104
+ sections.concat(generate_response_section(endpoint))
105
+ sections
106
+ end
107
+
108
+ def generate_metadata_section(endpoint)
109
+ doc = []
110
+
111
+ # Summary and description
112
+ doc << "**#{endpoint.metadata[:summary]}**" if endpoint.metadata[:summary]
113
+ doc << endpoint.metadata[:description] if endpoint.metadata[:description]
114
+
115
+ doc
116
+ end
117
+
118
+ def generate_path_parameters_section(endpoint)
119
+ path_params = endpoint.inputs.select { |input| input.kind == :path }
120
+ return [] unless path_params.any?
121
+
122
+ doc = []
123
+ doc << '### Path Parameters'
124
+ doc << ''
125
+ doc.concat(generate_path_parameters_table_header)
126
+ doc.concat(generate_path_parameters_rows(path_params))
127
+
128
+ doc
129
+ end
130
+
131
+ def generate_path_parameters_table_header
132
+ [
133
+ '| Parameter | Type | Description |',
134
+ '|-----------|------|-------------|'
135
+ ]
136
+ end
137
+
138
+ def generate_path_parameters_rows(path_params)
139
+ path_params.map do |param|
140
+ description = extract_parameter_description(param)
141
+ "| `#{param.name}` | #{format_type(param.type)} | #{description} |"
142
+ end
143
+ end
144
+
145
+ def generate_query_parameters_section(endpoint)
146
+ query_params = endpoint.inputs.select { |input| input.kind == :query }
147
+ return [] unless query_params.any?
148
+
149
+ doc = []
150
+ doc << '### Query Parameters'
151
+ doc << ''
152
+ doc.concat(generate_parameters_table_header)
153
+ doc.concat(generate_query_parameters_rows(query_params))
154
+
155
+ doc
156
+ end
157
+
158
+ def generate_parameters_table_header
159
+ [
160
+ '| Parameter | Type | Required | Description |',
161
+ '|-----------|------|----------|-------------|'
162
+ ]
163
+ end
164
+
165
+ def generate_query_parameters_rows(query_params)
166
+ query_params.map do |param|
167
+ required = param.required? ? 'Yes' : 'No'
168
+ description = extract_parameter_description(param)
169
+ "| `#{param.name}` | #{format_type(param.type)} | #{required} | #{description} |"
170
+ end
171
+ end
172
+
173
+ def extract_parameter_description(param)
174
+ (param.options && param.options[:description]) || 'No description'
175
+ end
176
+
177
+ def generate_request_body_section(endpoint)
178
+ body_param = endpoint.inputs.find { |input| input.kind == :body }
179
+ return [] unless body_param
180
+
181
+ doc = []
182
+ doc << '### Request Body'
183
+ doc << ''
184
+ doc << '**Content-Type:** `application/json`'
185
+ doc << ''
186
+ doc << '**Schema:**'
187
+ doc << '```json'
188
+ doc << format_schema_example(body_param.type)
189
+ doc << '```'
190
+
191
+ doc
192
+ end
193
+
194
+ def generate_response_section(endpoint)
195
+ return [] unless endpoint.outputs.any?
196
+
197
+ doc = []
198
+ doc << '### Response'
199
+ doc << ''
200
+ doc.concat(generate_response_outputs(endpoint.outputs))
201
+
202
+ doc
203
+ end
204
+
205
+ def generate_response_outputs(outputs)
206
+ outputs.flat_map do |output|
207
+ generate_single_response_output(output)
208
+ end
209
+ end
210
+
211
+ def generate_single_response_output(output)
212
+ case output.kind
213
+ when :json
214
+ generate_json_response_output(output)
215
+ when :status
216
+ ["**Status Code:** #{output.type}"]
217
+ else
218
+ []
219
+ end
220
+ end
221
+
222
+ def generate_json_response_output(output)
223
+ [
224
+ '**Content-Type:** `application/json`',
225
+ '',
226
+ '**Schema:**',
227
+ '```json',
228
+ format_schema_example(output.type),
229
+ '```'
230
+ ]
231
+ end
232
+
233
+ def generate_endpoint_examples(endpoint)
234
+ curl_example = generate_curl_example(endpoint)
235
+ response_example = generate_response_example(endpoint)
236
+
237
+ examples = []
238
+ examples << '### Example'
239
+ examples << ''
240
+ examples.concat(build_request_example_section(curl_example))
241
+ examples.concat(build_response_example_section(response_example)) if response_example
242
+
243
+ examples.join("\n")
244
+ end
245
+
246
+ def build_request_example_section(curl_example)
247
+ [
248
+ '**Request:**',
249
+ '```bash',
250
+ curl_example,
251
+ '```'
252
+ ]
253
+ end
254
+
255
+ def build_response_example_section(response_example)
256
+ [
257
+ '',
258
+ '**Response:**',
259
+ '```json',
260
+ response_example,
261
+ '```'
262
+ ]
263
+ end
264
+
265
+ def generate_curl_example(endpoint)
266
+ method = endpoint.method.to_s.upcase
267
+ example_path = build_example_path(endpoint.path, endpoint)
268
+
269
+ curl_parts = build_curl_parts(method, endpoint, example_path)
270
+ curl_parts.join(' \\\n ')
271
+ end
272
+
273
+ def build_example_path(path, endpoint)
274
+ example_path = replace_path_parameters(path)
275
+ add_query_parameters(example_path, endpoint)
276
+ end
277
+
278
+ def replace_path_parameters(path)
279
+ path.gsub(/:(\w+)/) do |_match|
280
+ param_name = ::Regexp.last_match(1)
281
+ case param_name
282
+ when 'id' then '123'
283
+ when 'slug' then 'example-slug'
284
+ else 'example-value'
285
+ end
286
+ end
287
+ end
288
+
289
+ def add_query_parameters(example_path, endpoint)
290
+ query_params = endpoint.inputs.select { |input| input.kind == :query }
291
+ return example_path unless query_params.any?
292
+
293
+ query_string = build_query_string(query_params)
294
+ "#{example_path}?#{query_string}"
295
+ end
296
+
297
+ def build_query_string(query_params)
298
+ query_params.map do |param|
299
+ example_value = generate_param_example_value(param.type)
300
+ "#{param.name}=#{example_value}"
301
+ end.join('&')
302
+ end
303
+
304
+ def generate_param_example_value(param_type)
305
+ case param_type
306
+ when :string then 'example'
307
+ when :integer then '10'
308
+ when :boolean then 'true'
309
+ else 'value'
310
+ end
311
+ end
312
+
313
+ def build_curl_parts(method, endpoint, example_path)
314
+ curl_parts = ["curl -X #{method}"]
315
+
316
+ curl_parts.concat(build_curl_headers)
317
+ curl_parts.concat(build_curl_body(endpoint))
318
+ curl_parts << "'#{config[:base_url]}#{example_path}'"
319
+
320
+ curl_parts
321
+ end
322
+
323
+ def build_curl_headers
324
+ [
325
+ "-H 'Content-Type: application/json'",
326
+ "-H 'Accept: application/json'"
327
+ ]
328
+ end
329
+
330
+ def build_curl_body(endpoint)
331
+ body_param = endpoint.inputs.find { |input| input.kind == :body }
332
+ return [] unless body_param
333
+
334
+ body_example = format_schema_example(body_param.type)
335
+ ["-d '#{body_example}'"]
336
+ end
337
+
338
+ def generate_response_example(endpoint)
339
+ output = endpoint.outputs.find { |o| o.kind == :json }
340
+ return nil unless output
341
+
342
+ format_schema_example(output.type)
343
+ end
344
+
345
+ def format_schema_example(schema, indent_level = 0)
346
+ case schema
347
+ when Hash
348
+ format_hash_schema_example(schema, indent_level)
349
+ when Array
350
+ format_array_schema_example(schema, indent_level)
351
+ else
352
+ generate_example_value(schema)
353
+ end
354
+ end
355
+
356
+ def format_hash_schema_example(schema, indent_level)
357
+ indent = ' ' * indent_level
358
+ lines = ['{']
359
+
360
+ schema.each_with_index do |(key, value), index|
361
+ comma = index < schema.size - 1 ? ',' : ''
362
+ formatted_line = format_hash_property_line(key, value, indent_level, comma)
363
+ lines << formatted_line
364
+ end
365
+
366
+ lines << "#{indent}}"
367
+ lines.join("\n")
368
+ end
369
+
370
+ def format_hash_property_line(key, value, indent_level, comma)
371
+ indent = ' ' * indent_level
372
+
373
+ if value.is_a?(Hash) || value.is_a?(Array)
374
+ nested_example = format_schema_example(value, indent_level + 1)
375
+ "#{indent} \"#{key}\": #{nested_example}#{comma}"
376
+ else
377
+ example_value = generate_example_value(value)
378
+ "#{indent} \"#{key}\": #{example_value}#{comma}"
379
+ end
380
+ end
381
+
382
+ def format_array_schema_example(schema, indent_level)
383
+ if schema.length == 1
384
+ indent = ' ' * indent_level
385
+ element_example = format_schema_example(schema.first, indent_level)
386
+ "[\n#{indent} #{element_example}\n#{indent}]"
387
+ else
388
+ '[]'
389
+ end
390
+ end
391
+
392
+ def generate_example_value(type)
393
+ case type
394
+ when :string, String then '"example string"'
395
+ when :integer, Integer then '123'
396
+ when :float, Float then '123.45'
397
+ when :boolean then 'true'
398
+ when :date then '"2025-01-15"'
399
+ when :datetime then '"2025-01-15T10:30:00Z"'
400
+ else '"example"'
401
+ end
402
+ end
403
+
404
+ def format_type(type)
405
+ case type
406
+ when RapiTapir::Types::String, :string
407
+ 'string'
408
+ when RapiTapir::Types::Integer, :integer
409
+ 'integer'
410
+ when RapiTapir::Types::Float, :float
411
+ 'number'
412
+ when RapiTapir::Types::Boolean, :boolean
413
+ 'boolean'
414
+ else
415
+ format_advanced_type(type)
416
+ end
417
+ end
418
+
419
+ def format_advanced_type(type)
420
+ case type
421
+ when RapiTapir::Types::Date, :date
422
+ 'date'
423
+ when RapiTapir::Types::DateTime, :datetime
424
+ 'datetime'
425
+ when RapiTapir::Types::Array
426
+ 'array'
427
+ when RapiTapir::Types::Hash
428
+ 'object'
429
+ else
430
+ format_builtin_type(type)
431
+ end
432
+ end
433
+
434
+ def format_builtin_type(type)
435
+ case type
436
+ when String then 'string'
437
+ when Integer then 'integer'
438
+ when Float then 'number'
439
+ else
440
+ format_class_type(type)
441
+ end
442
+ end
443
+
444
+ def format_class_type(type)
445
+ return 'object' if type == Hash
446
+ return 'array' if type == Array
447
+
448
+ type.to_s
449
+ end
450
+
451
+ def generate_anchor(method, path)
452
+ "#{method.downcase}-#{path.gsub('/', '').gsub(':', '')}"
453
+ end
454
+
455
+ def generate_footer
456
+ <<~MARKDOWN
457
+ ---
458
+
459
+ *Generated by RapiTapir Documentation Generator*
460
+ MARKDOWN
461
+ end
462
+ end
463
+ end
464
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/input'
4
+ require_relative '../core/output'
5
+
6
+ module RapiTapir
7
+ # Domain Specific Language (DSL) components for RapiTapir
8
+ # Provides convenient methods for defining API endpoints and their properties
9
+ module DSL
10
+ # DSL helpers for endpoint input definitions
11
+ def query(name, type, options = {})
12
+ validate_input_params!(name, type)
13
+ Core::Input.new(kind: :query, name: name, type: type, options: options)
14
+ end
15
+
16
+ def path_param(name, type, options = {})
17
+ validate_input_params!(name, type)
18
+ Core::Input.new(kind: :path, name: name, type: type, options: options)
19
+ end
20
+
21
+ def header(name, type, options = {})
22
+ validate_input_params!(name, type)
23
+ Core::Input.new(kind: :header, name: name, type: type, options: options)
24
+ end
25
+
26
+ def body(type, options = {})
27
+ validate_type!(type)
28
+ Core::Input.new(kind: :body, name: :body, type: type, options: options)
29
+ end
30
+
31
+ # DSL helpers for endpoint output definitions
32
+ def json_body(schema)
33
+ validate_schema!(schema)
34
+ Core::Output.new(kind: :json, type: schema)
35
+ end
36
+
37
+ def xml_body(schema)
38
+ validate_schema!(schema)
39
+ Core::Output.new(kind: :xml, type: schema)
40
+ end
41
+
42
+ def status_code(code)
43
+ validate_status_code!(code)
44
+ Core::Output.new(kind: :status, type: code)
45
+ end
46
+
47
+ # DSL helpers for endpoint metadata
48
+ def description(text)
49
+ validate_string!(text, 'description')
50
+ { description: text }
51
+ end
52
+
53
+ def summary(text)
54
+ validate_string!(text, 'summary')
55
+ { summary: text }
56
+ end
57
+
58
+ def tag(name)
59
+ validate_string!(name, 'tag')
60
+ { tag: name }
61
+ end
62
+
63
+ def example(data)
64
+ { example: data }
65
+ end
66
+
67
+ def deprecated(*args, **kwargs)
68
+ # Support both deprecated(true/false) and deprecated(flag: true/false)
69
+ flag_value = if args.length.positive?
70
+ args.first
71
+ else
72
+ kwargs.fetch(:flag, true)
73
+ end
74
+ { deprecated: flag_value }
75
+ end
76
+
77
+ def error_description(text)
78
+ validate_string!(text, 'error_description')
79
+ { error_description: text }
80
+ end
81
+
82
+ private
83
+
84
+ def validate_input_params!(name, type)
85
+ raise ArgumentError, 'Input name cannot be nil' if name.nil?
86
+ raise ArgumentError, 'Input name must be a symbol or string' unless name.is_a?(Symbol) || name.is_a?(String)
87
+
88
+ validate_type!(type)
89
+ end
90
+
91
+ def validate_type!(type)
92
+ valid_types = %i[string integer float boolean date datetime]
93
+ return if valid_types.include?(type) || type.is_a?(Hash) || type.is_a?(Class)
94
+
95
+ raise ArgumentError, "Invalid type: #{type}. Must be one of #{valid_types} or a Hash/Class"
96
+ end
97
+
98
+ def validate_schema!(schema)
99
+ return if schema.is_a?(Hash) || schema.is_a?(Class) || schema.is_a?(Symbol) || schema.is_a?(Array)
100
+
101
+ raise ArgumentError, "Invalid schema: #{schema}. Must be a Hash, Class, Symbol, or Array"
102
+ end
103
+
104
+ def validate_status_code!(code)
105
+ return if code.is_a?(Integer) && code >= 100 && code <= 599
106
+
107
+ raise ArgumentError, "Invalid status code: #{code}. Must be an integer between 100-599"
108
+ end
109
+
110
+ def validate_string!(value, name)
111
+ return if value.is_a?(String) && !value.empty?
112
+
113
+ raise ArgumentError, "#{name} must be a non-empty string"
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../types'
4
+ require_relative '../schema'
5
+ require_relative 'input_methods'
6
+ require_relative 'output_methods'
7
+ require_relative 'observability_methods'
8
+ require_relative 'type_resolution'
9
+ require_relative 'enhanced_input'
10
+ require_relative 'enhanced_output'
11
+
12
+ module RapiTapir
13
+ module DSL
14
+ # Enhanced DSL module that works with the new type system
15
+ module EnhancedEndpointDSL
16
+ include InputMethods
17
+ include OutputMethods
18
+ include ObservabilityMethods
19
+ include TypeResolution
20
+
21
+ def out_status(code)
22
+ create_output(:status, code)
23
+ end
24
+
25
+ # Authentication DSL methods
26
+ def bearer_auth(description = 'Bearer token authentication')
27
+ create_input(:header, :authorization, Types.string(pattern: /\ABearer .+\z/),
28
+ description: description, auth_type: :bearer)
29
+ end
30
+
31
+ def api_key_auth(header_name = 'X-API-Key', description = 'API key authentication')
32
+ create_input(:header, header_name.downcase.to_sym, Types.string,
33
+ description: description, auth_type: :api_key)
34
+ end
35
+
36
+ def basic_auth(description = 'Basic authentication')
37
+ create_input(:header, :authorization, Types.string(pattern: /\ABasic .+\z/),
38
+ description: description, auth_type: :basic)
39
+ end
40
+
41
+ # Validation DSL methods
42
+ def validate_with(validator_proc)
43
+ @custom_validators ||= []
44
+ @custom_validators << validator_proc
45
+ end
46
+
47
+ def validate_json_schema(schema_def)
48
+ validate_with(->(data) { Schema.validate!(data, resolve_type(schema_def)) })
49
+ end
50
+
51
+ private
52
+
53
+ def create_input(kind, name, type, **options)
54
+ EnhancedInput.new(kind: kind, name: name, type: type, options: options)
55
+ end
56
+
57
+ def create_output(kind, type, **options)
58
+ EnhancedOutput.new(kind: kind, type: type, options: options)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module DSL
5
+ # Enhanced Input class that uses the new type system
6
+ class EnhancedInput
7
+ attr_reader :kind, :name, :type, :options
8
+
9
+ def initialize(kind:, name:, type:, options: {})
10
+ @kind = kind
11
+ @name = name.to_sym
12
+ @type = type
13
+ @options = options.freeze
14
+ end
15
+
16
+ def required?
17
+ !type.optional? && !(options && options[:optional])
18
+ end
19
+
20
+ def optional?
21
+ type.optional? || (options && options[:optional])
22
+ end
23
+
24
+ def validate(value)
25
+ return { valid: true, errors: [] } if value.nil? && optional?
26
+
27
+ return { valid: false, errors: ["#{name} is required but got nil"] } if value.nil? && required?
28
+
29
+ type.validate(value)
30
+ end
31
+
32
+ def coerce(value)
33
+ return nil if value.nil? && optional?
34
+
35
+ type.coerce(value)
36
+ end
37
+
38
+ def to_openapi_parameter
39
+ schema = type.to_json_schema
40
+
41
+ {
42
+ name: name.to_s,
43
+ in: openapi_location,
44
+ required: required?,
45
+ description: options[:description],
46
+ schema: schema
47
+ }.compact
48
+ end
49
+
50
+ def to_h
51
+ {
52
+ kind: kind,
53
+ name: name,
54
+ type: type.to_s,
55
+ required: required?,
56
+ options: options
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ def openapi_location
63
+ case kind
64
+ when :query then 'query'
65
+ when :path then 'path'
66
+ when :header then 'header'
67
+ when :body then 'requestBody'
68
+ else kind.to_s
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end