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,403 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
module OpenAPI
|
7
|
+
# OpenAPI schema generator for RapiTapir endpoints
|
8
|
+
# Converts RapiTapir endpoint definitions to OpenAPI 3.0 specifications
|
9
|
+
class SchemaGenerator
|
10
|
+
attr_reader :endpoints, :info, :servers
|
11
|
+
|
12
|
+
def initialize(endpoints: [], info: {}, servers: [])
|
13
|
+
@endpoints = endpoints
|
14
|
+
@info = default_info.merge(info)
|
15
|
+
@servers = servers.empty? ? default_servers : servers
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generate complete OpenAPI 3.0 specification
|
19
|
+
def generate
|
20
|
+
{
|
21
|
+
openapi: '3.0.3',
|
22
|
+
info: @info,
|
23
|
+
servers: @servers,
|
24
|
+
paths: generate_paths,
|
25
|
+
components: generate_components
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Generate as JSON string
|
30
|
+
def to_json(pretty: true)
|
31
|
+
schema = generate
|
32
|
+
if pretty
|
33
|
+
JSON.pretty_generate(schema)
|
34
|
+
else
|
35
|
+
JSON.generate(schema)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Generate as YAML string
|
40
|
+
def to_yaml
|
41
|
+
require 'yaml'
|
42
|
+
YAML.dump(generate)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def default_info
|
48
|
+
{
|
49
|
+
title: 'RapiTapir API',
|
50
|
+
version: '1.0.0',
|
51
|
+
description: 'API generated by RapiTapir'
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_servers
|
56
|
+
[
|
57
|
+
{
|
58
|
+
url: 'http://localhost:4567',
|
59
|
+
description: 'Development server'
|
60
|
+
}
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_paths
|
65
|
+
paths = {}
|
66
|
+
|
67
|
+
@endpoints.each do |endpoint_data|
|
68
|
+
endpoint = endpoint_data.is_a?(Hash) ? endpoint_data[:endpoint] : endpoint_data
|
69
|
+
# Convert :id format to {id} format for OpenAPI
|
70
|
+
path = convert_path_to_openapi(endpoint.path)
|
71
|
+
method = endpoint.method.to_s.downcase
|
72
|
+
|
73
|
+
paths[path] ||= {}
|
74
|
+
paths[path][method] = generate_operation(endpoint)
|
75
|
+
end
|
76
|
+
|
77
|
+
paths
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_operation(endpoint)
|
81
|
+
operation = build_base_operation(endpoint)
|
82
|
+
|
83
|
+
add_parameters_to_operation(operation, endpoint)
|
84
|
+
add_request_body_to_operation(operation, endpoint)
|
85
|
+
add_responses_to_operation(operation, endpoint)
|
86
|
+
|
87
|
+
# Remove nil values
|
88
|
+
operation.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_base_operation(endpoint)
|
92
|
+
{
|
93
|
+
summary: endpoint.metadata[:summary] || generate_default_summary(endpoint),
|
94
|
+
description: endpoint.metadata[:description],
|
95
|
+
operationId: generate_operation_id(endpoint),
|
96
|
+
tags: endpoint.metadata[:tags]&.any? ? endpoint.metadata[:tags] : [generate_default_tag(endpoint)]
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_parameters_to_operation(operation, endpoint)
|
101
|
+
parameters = generate_parameters(endpoint)
|
102
|
+
operation[:parameters] = parameters if parameters.any?
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_request_body_to_operation(operation, endpoint)
|
106
|
+
request_body = generate_request_body(endpoint)
|
107
|
+
operation[:requestBody] = request_body if request_body
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_responses_to_operation(operation, endpoint)
|
111
|
+
operation[:responses] = generate_responses(endpoint)
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_parameters(endpoint)
|
115
|
+
parameters = []
|
116
|
+
|
117
|
+
endpoint.inputs.each do |input|
|
118
|
+
parameter = build_parameter_for_input(input)
|
119
|
+
parameters << parameter if parameter
|
120
|
+
end
|
121
|
+
|
122
|
+
parameters
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_parameter_for_input(input)
|
126
|
+
case input.kind
|
127
|
+
when :path
|
128
|
+
build_path_parameter(input)
|
129
|
+
when :query
|
130
|
+
build_query_parameter(input)
|
131
|
+
when :header
|
132
|
+
build_header_parameter(input)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_path_parameter(input)
|
137
|
+
{
|
138
|
+
name: input.name.to_s,
|
139
|
+
in: 'path',
|
140
|
+
required: true,
|
141
|
+
description: "Path parameter: #{input.name}",
|
142
|
+
schema: type_to_schema(input.type)
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_query_parameter(input)
|
147
|
+
{
|
148
|
+
name: input.name.to_s,
|
149
|
+
in: 'query',
|
150
|
+
required: input.required?,
|
151
|
+
description: "Query parameter: #{input.name}",
|
152
|
+
schema: type_to_schema(input.type)
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def build_header_parameter(input)
|
157
|
+
{
|
158
|
+
name: input.name.to_s,
|
159
|
+
in: 'header',
|
160
|
+
required: input.required?,
|
161
|
+
description: "Header parameter: #{input.name}",
|
162
|
+
schema: type_to_schema(input.type)
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def generate_request_body(endpoint)
|
167
|
+
body_input = endpoint.inputs.find { |input| input.kind == :body }
|
168
|
+
return nil unless body_input
|
169
|
+
|
170
|
+
content_type = determine_content_type(body_input)
|
171
|
+
|
172
|
+
{
|
173
|
+
description: 'Request body',
|
174
|
+
required: body_input.required?,
|
175
|
+
content: {
|
176
|
+
content_type => {
|
177
|
+
schema: type_to_schema(body_input.type)
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def generate_responses(endpoint)
|
184
|
+
responses = {}
|
185
|
+
|
186
|
+
generate_success_responses(endpoint, responses)
|
187
|
+
generate_error_responses(endpoint, responses)
|
188
|
+
add_default_response_if_needed(responses)
|
189
|
+
|
190
|
+
responses
|
191
|
+
end
|
192
|
+
|
193
|
+
def generate_success_responses(endpoint, responses)
|
194
|
+
endpoint.outputs.each do |output|
|
195
|
+
status_code = determine_status_code(output)
|
196
|
+
content_type = determine_output_content_type(output)
|
197
|
+
|
198
|
+
responses[status_code.to_s] = {
|
199
|
+
description: 'Successful response',
|
200
|
+
content: {
|
201
|
+
content_type => {
|
202
|
+
schema: type_to_schema(output.type)
|
203
|
+
}
|
204
|
+
}
|
205
|
+
}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def generate_error_responses(endpoint, responses)
|
210
|
+
endpoint.errors.each do |error_entry|
|
211
|
+
error_response = build_error_response(error_entry)
|
212
|
+
responses[error_response[:status_code].to_s] = error_response[:response]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def build_error_response(error_entry)
|
217
|
+
if error_entry.respond_to?(:status_code)
|
218
|
+
build_enhanced_error_response(error_entry)
|
219
|
+
else
|
220
|
+
build_legacy_error_response(error_entry)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def build_enhanced_error_response(error_entry)
|
225
|
+
status_code = error_entry.status_code
|
226
|
+
description = error_entry.description || 'Error response'
|
227
|
+
schema = type_to_schema(error_entry.type || { error: :string })
|
228
|
+
|
229
|
+
{
|
230
|
+
status_code: status_code,
|
231
|
+
response: {
|
232
|
+
description: description,
|
233
|
+
content: {
|
234
|
+
'application/json' => {
|
235
|
+
schema: schema
|
236
|
+
}
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
def build_legacy_error_response(error_entry)
|
243
|
+
status_code = error_entry[:code] || 400
|
244
|
+
description = error_entry[:description] || 'Error response'
|
245
|
+
schema = type_to_schema(error_entry[:output]&.type || { error: :string })
|
246
|
+
|
247
|
+
{
|
248
|
+
status_code: status_code,
|
249
|
+
response: {
|
250
|
+
description: description,
|
251
|
+
content: {
|
252
|
+
'application/json' => {
|
253
|
+
schema: schema
|
254
|
+
}
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
def add_default_response_if_needed(responses)
|
261
|
+
return unless responses.empty?
|
262
|
+
|
263
|
+
responses['200'] = {
|
264
|
+
description: 'Successful response'
|
265
|
+
}
|
266
|
+
end
|
267
|
+
|
268
|
+
def generate_components
|
269
|
+
{
|
270
|
+
schemas: generate_schema_components
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
def generate_schema_components
|
275
|
+
# For now, return empty. In the future, we can extract common schemas
|
276
|
+
{}
|
277
|
+
end
|
278
|
+
|
279
|
+
def type_to_schema(type)
|
280
|
+
case type
|
281
|
+
when RapiTapir::Types::String, :string, String
|
282
|
+
{ type: 'string' }
|
283
|
+
when RapiTapir::Types::Integer, :integer, Integer
|
284
|
+
{ type: 'integer' }
|
285
|
+
when RapiTapir::Types::Float, :number, Float
|
286
|
+
{ type: 'number' }
|
287
|
+
when RapiTapir::Types::Boolean, :boolean
|
288
|
+
{ type: 'boolean' }
|
289
|
+
else
|
290
|
+
generate_complex_type_schema(type)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def generate_complex_type_schema(type)
|
295
|
+
case type
|
296
|
+
when RapiTapir::Types::Array
|
297
|
+
generate_array_schema(type)
|
298
|
+
when RapiTapir::Types::Hash
|
299
|
+
generate_hash_schema(type)
|
300
|
+
when :date, :datetime
|
301
|
+
generate_date_schema(type)
|
302
|
+
when Array
|
303
|
+
generate_ruby_array_schema(type)
|
304
|
+
else # Default for unknown types, :string, String, and Hash
|
305
|
+
{ type: 'string' }
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def generate_array_schema(type)
|
310
|
+
{ type: 'array', items: type_to_schema(type.item_type) }
|
311
|
+
end
|
312
|
+
|
313
|
+
def generate_hash_schema(type)
|
314
|
+
return { type: 'object' } if type.field_types.empty?
|
315
|
+
|
316
|
+
properties = {}
|
317
|
+
required = []
|
318
|
+
|
319
|
+
type.field_types.each do |key, value|
|
320
|
+
properties[key.to_s] = type_to_schema(value)
|
321
|
+
required << key.to_s unless value.nil?
|
322
|
+
end
|
323
|
+
|
324
|
+
schema = { type: 'object', properties: properties }
|
325
|
+
schema[:required] = required if required.any?
|
326
|
+
schema
|
327
|
+
end
|
328
|
+
|
329
|
+
def generate_date_schema(type)
|
330
|
+
format = type == :date ? 'date' : 'date-time'
|
331
|
+
{ type: 'string', format: format }
|
332
|
+
end
|
333
|
+
|
334
|
+
def generate_ruby_array_schema(type)
|
335
|
+
if type.length == 1
|
336
|
+
{ type: 'array', items: type_to_schema(type.first) }
|
337
|
+
else
|
338
|
+
{ type: 'array', items: { type: 'string' } }
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def determine_content_type(input)
|
343
|
+
case input.type
|
344
|
+
when Hash
|
345
|
+
# For hash types, default to JSON
|
346
|
+
return 'application/json'
|
347
|
+
end
|
348
|
+
'application/json'
|
349
|
+
end
|
350
|
+
|
351
|
+
def determine_output_content_type(output)
|
352
|
+
case output.kind
|
353
|
+
when :xml
|
354
|
+
'application/xml'
|
355
|
+
when :html
|
356
|
+
'text/html'
|
357
|
+
else # Default for :json and unknown formats
|
358
|
+
'application/json'
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def determine_status_code(output)
|
363
|
+
if output.kind == :status && output.type.is_a?(Integer)
|
364
|
+
output.type
|
365
|
+
else
|
366
|
+
# Default status codes based on output type
|
367
|
+
case output.kind
|
368
|
+
when :json, :xml
|
369
|
+
# JSON and XML responses default to 200
|
370
|
+
return 200
|
371
|
+
end
|
372
|
+
200
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def generate_operation_id(endpoint)
|
377
|
+
method = endpoint.method.to_s.downcase
|
378
|
+
path_parts = endpoint.path.split('/').reject(&:empty?).map do |part|
|
379
|
+
part.start_with?(':') ? part[1..] : part
|
380
|
+
end
|
381
|
+
|
382
|
+
"#{method}_#{path_parts.join('_')}"
|
383
|
+
end
|
384
|
+
|
385
|
+
def generate_default_summary(endpoint)
|
386
|
+
method = endpoint.method.to_s.upcase
|
387
|
+
path = endpoint.path
|
388
|
+
"#{method} #{path}"
|
389
|
+
end
|
390
|
+
|
391
|
+
def generate_default_tag(endpoint)
|
392
|
+
# Extract the first path segment as a tag
|
393
|
+
first_segment = endpoint.path.split('/').reject(&:empty?).first
|
394
|
+
first_segment&.capitalize || 'default'
|
395
|
+
end
|
396
|
+
|
397
|
+
def convert_path_to_openapi(path)
|
398
|
+
# Convert "/users/:id" to "/users/{id}" for OpenAPI
|
399
|
+
path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'types'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
# Schema definition and validation module
|
7
|
+
# Provides tools for defining and validating data schemas
|
8
|
+
module Schema
|
9
|
+
# Error for schema validation failures
|
10
|
+
# Raised when data does not conform to defined schema constraints
|
11
|
+
class ValidationError < StandardError
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
def initialize(errors)
|
15
|
+
@errors = errors
|
16
|
+
super(build_message)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def build_message
|
22
|
+
"Schema validation failed:\n#{errors.map { |error| " - #{error}" }.join("\n")}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Define a schema using a block
|
27
|
+
def self.define(&block)
|
28
|
+
builder = SchemaBuilder.new
|
29
|
+
builder.instance_eval(&block)
|
30
|
+
builder.build
|
31
|
+
end
|
32
|
+
|
33
|
+
# Validate a value against a type
|
34
|
+
def self.validate!(value, type)
|
35
|
+
result = type.validate(value)
|
36
|
+
return value if result[:valid]
|
37
|
+
|
38
|
+
raise ValidationError, result[:errors]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Validate a value against a type, returning result
|
42
|
+
def self.validate(value, type)
|
43
|
+
type.validate(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Coerce a value using a type
|
47
|
+
def self.coerce(value, type)
|
48
|
+
type.coerce(value)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a type from a simplified definition
|
52
|
+
def self.from_definition(definition)
|
53
|
+
case definition
|
54
|
+
when Symbol
|
55
|
+
create_primitive_type(definition)
|
56
|
+
when ::Hash
|
57
|
+
create_type_from_hash(definition)
|
58
|
+
when ::Array
|
59
|
+
create_array_type_from_definition(definition)
|
60
|
+
when Class
|
61
|
+
# Assume it's already a type class
|
62
|
+
definition
|
63
|
+
else
|
64
|
+
raise ArgumentError, "Unknown definition type: #{definition.class}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.create_type_from_hash(definition)
|
69
|
+
if definition.keys == [:type] && definition[:type].is_a?(Symbol)
|
70
|
+
create_primitive_type(definition[:type])
|
71
|
+
else
|
72
|
+
create_object_from_hash(definition)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.create_array_type_from_definition(definition)
|
77
|
+
raise ArgumentError, 'Array definition must have exactly one element type' unless definition.length == 1
|
78
|
+
|
79
|
+
Types.array(from_definition(definition.first))
|
80
|
+
end
|
81
|
+
|
82
|
+
PRIMITIVE_TYPE_MAP = {
|
83
|
+
string: -> { Types.string },
|
84
|
+
integer: -> { Types.integer },
|
85
|
+
float: -> { Types.float },
|
86
|
+
boolean: -> { Types.boolean },
|
87
|
+
date: -> { Types.date },
|
88
|
+
datetime: -> { Types.datetime },
|
89
|
+
uuid: -> { Types.uuid },
|
90
|
+
email: -> { Types.email }
|
91
|
+
}.freeze
|
92
|
+
|
93
|
+
def self.create_primitive_type(type_symbol)
|
94
|
+
type_creator = PRIMITIVE_TYPE_MAP[type_symbol]
|
95
|
+
return type_creator.call if type_creator
|
96
|
+
|
97
|
+
raise ArgumentError, "Unknown primitive type: #{type_symbol}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.create_object_from_hash(hash_definition)
|
101
|
+
object_type = Types.object
|
102
|
+
|
103
|
+
hash_definition.each do |field_name, field_definition|
|
104
|
+
field_type = from_definition(field_definition)
|
105
|
+
object_type.field(field_name, field_type)
|
106
|
+
end
|
107
|
+
|
108
|
+
object_type
|
109
|
+
end
|
110
|
+
|
111
|
+
# Builder for constructing complex schemas from definitions
|
112
|
+
# Provides a fluent interface for schema creation
|
113
|
+
class SchemaBuilder
|
114
|
+
def initialize
|
115
|
+
@object_type = Types.object
|
116
|
+
end
|
117
|
+
|
118
|
+
def field(name, type_def, required: true, **options)
|
119
|
+
type = Schema.from_definition(type_def)
|
120
|
+
@object_type.field(name, type, required: required, **options)
|
121
|
+
end
|
122
|
+
|
123
|
+
def required_field(name, type_def, **options)
|
124
|
+
field(name, type_def, required: true, **options)
|
125
|
+
end
|
126
|
+
|
127
|
+
def optional_field(name, type_def, **options)
|
128
|
+
field(name, type_def, required: false, **options)
|
129
|
+
end
|
130
|
+
|
131
|
+
def build
|
132
|
+
@object_type
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|