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,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
|