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,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'webrick'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module RapiTapir
|
7
|
+
module CLI
|
8
|
+
# Development server for testing RapiTapir APIs
|
9
|
+
# Provides a simple HTTP server for development and testing
|
10
|
+
class Server
|
11
|
+
attr_reader :endpoints_file, :port, :config
|
12
|
+
|
13
|
+
def initialize(endpoints_file:, port: 3000, config: {})
|
14
|
+
@endpoints_file = endpoints_file
|
15
|
+
@port = port
|
16
|
+
@config = {
|
17
|
+
title: 'API Documentation',
|
18
|
+
description: 'Live API documentation',
|
19
|
+
auto_reload: true,
|
20
|
+
include_try_it: true
|
21
|
+
}.merge(config)
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
server = create_webrick_server
|
26
|
+
setup_request_handlers(server)
|
27
|
+
setup_shutdown_handling(server)
|
28
|
+
server.start
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_webrick_server
|
34
|
+
WEBrick::HTTPServer.new(
|
35
|
+
Port: @port,
|
36
|
+
DocumentRoot: Dir.pwd,
|
37
|
+
Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
|
38
|
+
AccessLog: []
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup_request_handlers(server)
|
43
|
+
server.mount_proc '/' do |req, res|
|
44
|
+
handle_request(req, res)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_request(req, res)
|
49
|
+
case req.path
|
50
|
+
when '/'
|
51
|
+
serve_documentation(res)
|
52
|
+
when '/api.json'
|
53
|
+
serve_openapi_json(res)
|
54
|
+
when '/reload'
|
55
|
+
serve_reload_endpoint(res)
|
56
|
+
else
|
57
|
+
res.status = 404
|
58
|
+
res.body = 'Not Found'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_shutdown_handling(server)
|
63
|
+
trap('INT') { server.shutdown }
|
64
|
+
puts "Starting documentation server on port #{@port}..."
|
65
|
+
puts "Documentation server running at http://localhost:#{@port}"
|
66
|
+
puts 'Press Ctrl+C to stop'
|
67
|
+
end
|
68
|
+
|
69
|
+
def serve_documentation(response)
|
70
|
+
endpoints = load_endpoints
|
71
|
+
html_content = generate_documentation_html(endpoints)
|
72
|
+
html_content = add_auto_reload_if_enabled(html_content)
|
73
|
+
|
74
|
+
set_successful_response(response, html_content)
|
75
|
+
rescue StandardError => e
|
76
|
+
set_error_response(response, e)
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_documentation_html(endpoints)
|
80
|
+
require_relative '../docs/html_generator'
|
81
|
+
generator = RapiTapir::Docs::HtmlGenerator.new(
|
82
|
+
endpoints: endpoints,
|
83
|
+
config: config.merge(include_reload: true)
|
84
|
+
)
|
85
|
+
generator.generate
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_auto_reload_if_enabled(html_content)
|
89
|
+
return html_content unless config[:auto_reload]
|
90
|
+
|
91
|
+
html_content.gsub('</body>', "#{auto_reload_script}</body>")
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_successful_response(response, html_content)
|
95
|
+
response['Content-Type'] = 'text/html'
|
96
|
+
response.body = html_content
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_error_response(response, error)
|
100
|
+
response.status = 500
|
101
|
+
response['Content-Type'] = 'text/html'
|
102
|
+
response.body = error_page(error)
|
103
|
+
end
|
104
|
+
|
105
|
+
def serve_openapi_json(response)
|
106
|
+
endpoints = load_endpoints
|
107
|
+
|
108
|
+
require_relative '../openapi/schema_generator'
|
109
|
+
generator = RapiTapir::OpenAPI::SchemaGenerator.new(endpoints: endpoints)
|
110
|
+
|
111
|
+
response['Content-Type'] = 'application/json'
|
112
|
+
response.body = generator.to_json
|
113
|
+
rescue StandardError => e
|
114
|
+
response.status = 500
|
115
|
+
response['Content-Type'] = 'application/json'
|
116
|
+
response.body = JSON.generate({ error: e.message })
|
117
|
+
end
|
118
|
+
|
119
|
+
def serve_reload_endpoint(response)
|
120
|
+
# This endpoint is called by the auto-reload script
|
121
|
+
# Return current file modification time
|
122
|
+
mtime = File.exist?(input_file) ? File.mtime(input_file).to_i : 0
|
123
|
+
|
124
|
+
response['Content-Type'] = 'application/json'
|
125
|
+
response.body = JSON.generate({ mtime: mtime })
|
126
|
+
end
|
127
|
+
|
128
|
+
def load_endpoints
|
129
|
+
raise "Error loading endpoints: File '#{@endpoints_file}' not found" unless File.exist?(@endpoints_file)
|
130
|
+
|
131
|
+
# Create a new binding to evaluate the endpoints file
|
132
|
+
evaluation_context = Object.new
|
133
|
+
evaluation_context.extend(RapiTapir::DSL)
|
134
|
+
|
135
|
+
begin
|
136
|
+
code = File.read(@endpoints_file)
|
137
|
+
evaluation_context.instance_eval(code, @endpoints_file)
|
138
|
+
|
139
|
+
# Return the registered endpoints
|
140
|
+
RapiTapir.endpoints
|
141
|
+
rescue StandardError => e
|
142
|
+
raise "Error loading endpoints from '#{@endpoints_file}': #{e.message}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def auto_reload_script
|
147
|
+
<<~JAVASCRIPT
|
148
|
+
<script>
|
149
|
+
let lastMtime = 0;
|
150
|
+
#{' '}
|
151
|
+
async function checkForUpdates() {
|
152
|
+
try {
|
153
|
+
const response = await fetch('/reload');
|
154
|
+
const data = await response.json();
|
155
|
+
#{' '}
|
156
|
+
if (lastMtime === 0) {
|
157
|
+
lastMtime = data.mtime;
|
158
|
+
} else if (data.mtime > lastMtime) {
|
159
|
+
console.log('File changed, reloading...');
|
160
|
+
window.location.reload();
|
161
|
+
}
|
162
|
+
} catch (error) {
|
163
|
+
console.log('Auto-reload check failed:', error);
|
164
|
+
}
|
165
|
+
}
|
166
|
+
#{' '}
|
167
|
+
// Check for updates every 2 seconds
|
168
|
+
setInterval(checkForUpdates, 2000);
|
169
|
+
checkForUpdates(); // Initial check
|
170
|
+
</script>
|
171
|
+
JAVASCRIPT
|
172
|
+
end
|
173
|
+
|
174
|
+
def error_page(error)
|
175
|
+
<<~HTML
|
176
|
+
<!DOCTYPE html>
|
177
|
+
<html>
|
178
|
+
<head>
|
179
|
+
<title>Error - API Documentation</title>
|
180
|
+
<style>
|
181
|
+
body {#{' '}
|
182
|
+
font-family: Arial, sans-serif;#{' '}
|
183
|
+
margin: 40px;#{' '}
|
184
|
+
background-color: #f8f9fa;
|
185
|
+
}
|
186
|
+
.error {
|
187
|
+
background: #fff;
|
188
|
+
border: 1px solid #dc3545;
|
189
|
+
border-radius: 8px;
|
190
|
+
padding: 20px;
|
191
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
192
|
+
}
|
193
|
+
.error h1 {
|
194
|
+
color: #dc3545;
|
195
|
+
margin-top: 0;
|
196
|
+
}
|
197
|
+
.error pre {
|
198
|
+
background: #f8f9fa;
|
199
|
+
padding: 15px;
|
200
|
+
border-radius: 4px;
|
201
|
+
overflow-x: auto;
|
202
|
+
border: 1px solid #dee2e6;
|
203
|
+
}
|
204
|
+
.retry-button {
|
205
|
+
background: #007bff;
|
206
|
+
color: white;
|
207
|
+
border: none;
|
208
|
+
padding: 10px 20px;
|
209
|
+
border-radius: 4px;
|
210
|
+
cursor: pointer;
|
211
|
+
margin-top: 15px;
|
212
|
+
}
|
213
|
+
.retry-button:hover {
|
214
|
+
background: #0056b3;
|
215
|
+
}
|
216
|
+
</style>
|
217
|
+
</head>
|
218
|
+
<body>
|
219
|
+
<div class="error">
|
220
|
+
<h1>Error Loading Documentation</h1>
|
221
|
+
<p>There was an error processing your endpoint definitions:</p>
|
222
|
+
<pre>#{error.message}</pre>
|
223
|
+
<p>Please check your input file and try again.</p>
|
224
|
+
<button class="retry-button" onclick="window.location.reload()">Retry</button>
|
225
|
+
</div>
|
226
|
+
</body>
|
227
|
+
</html>
|
228
|
+
HTML
|
229
|
+
end
|
230
|
+
|
231
|
+
def mime_type(extension)
|
232
|
+
case extension
|
233
|
+
when '.html' then 'text/html'
|
234
|
+
when '.css' then 'text/css'
|
235
|
+
when '.js' then 'application/javascript'
|
236
|
+
when '.json' then 'application/json'
|
237
|
+
when '.xml' then 'application/xml'
|
238
|
+
else 'text/plain'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module CLI
|
5
|
+
# Validator for RapiTapir endpoint definitions
|
6
|
+
# Validates endpoint configurations for correctness and completeness
|
7
|
+
class Validator
|
8
|
+
attr_reader :errors, :endpoints
|
9
|
+
|
10
|
+
def initialize(endpoints = [])
|
11
|
+
@endpoints = endpoints
|
12
|
+
@errors = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
@errors.clear
|
17
|
+
|
18
|
+
return false if @endpoints.nil? || @endpoints.empty?
|
19
|
+
|
20
|
+
@endpoints.each_with_index do |endpoint, index|
|
21
|
+
validate_endpoint(endpoint, index)
|
22
|
+
end
|
23
|
+
|
24
|
+
@errors.empty?
|
25
|
+
end
|
26
|
+
alias validate valid?
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_endpoint(endpoint, index)
|
31
|
+
context = "Endpoint #{index + 1}"
|
32
|
+
|
33
|
+
return unless valid_endpoint_structure?(endpoint, context)
|
34
|
+
|
35
|
+
validate_endpoint_basics(endpoint, context)
|
36
|
+
validate_endpoint_content(endpoint, context)
|
37
|
+
validate_endpoint_consistency(endpoint, context)
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid_endpoint_structure?(endpoint, context)
|
41
|
+
unless endpoint.respond_to?(:method) && endpoint.respond_to?(:path)
|
42
|
+
@errors << "#{context}: Missing method or path"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_endpoint_basics(endpoint, context)
|
49
|
+
validate_http_method(endpoint, context)
|
50
|
+
validate_summary(endpoint, context)
|
51
|
+
validate_output_definition(endpoint, context)
|
52
|
+
validate_parameters(endpoint) if endpoint.respond_to?(:input_specs) && endpoint.input_specs
|
53
|
+
validate_path(endpoint.path, context)
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_endpoint_content(endpoint, context)
|
57
|
+
validate_endpoint_inputs(endpoint, context)
|
58
|
+
validate_endpoint_outputs(endpoint, context)
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_endpoint_consistency(endpoint, context)
|
62
|
+
validate_path_parameters_consistency(endpoint, context)
|
63
|
+
validate_metadata(endpoint, context)
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_http_method(endpoint, context)
|
67
|
+
valid_methods = %w[GET POST PUT PATCH DELETE HEAD OPTIONS]
|
68
|
+
return if valid_methods.include?(endpoint.method.to_s.upcase)
|
69
|
+
|
70
|
+
@errors << "#{context}: Invalid HTTP method '#{endpoint.method}'"
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_summary(endpoint, context)
|
74
|
+
return unless !endpoint.metadata || !endpoint.metadata[:summary] || endpoint.metadata[:summary].empty?
|
75
|
+
|
76
|
+
@errors << "#{context}: missing summary"
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_output_definition(endpoint, context)
|
80
|
+
return unless !endpoint.respond_to?(:outputs) || endpoint.outputs.nil? || endpoint.outputs.empty?
|
81
|
+
|
82
|
+
@errors << "#{context}: missing output definition"
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_endpoint_inputs(endpoint, context)
|
86
|
+
return unless endpoint.respond_to?(:inputs)
|
87
|
+
|
88
|
+
endpoint.inputs.each_with_index do |input, input_index|
|
89
|
+
validate_input(input, "#{context}, Input #{input_index + 1}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_endpoint_outputs(endpoint, context)
|
94
|
+
return unless endpoint.respond_to?(:outputs)
|
95
|
+
|
96
|
+
endpoint.outputs.each_with_index do |output, output_index|
|
97
|
+
validate_output(output, "#{context}, Output #{output_index + 1}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_path(path, context)
|
102
|
+
unless path.is_a?(String) && !path.empty?
|
103
|
+
@errors << "#{context}: Path must be a non-empty string"
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
@errors << "#{context}: Path must start with '/'" unless path.start_with?('/')
|
108
|
+
|
109
|
+
# Check for invalid characters
|
110
|
+
@errors << "#{context}: Path contains invalid characters" if path.match?(%r{[^a-zA-Z0-9/_:-]})
|
111
|
+
|
112
|
+
# Check path parameter format
|
113
|
+
path.scan(/:(\w+)/).each do |param_match|
|
114
|
+
param_name = param_match[0]
|
115
|
+
unless param_name.match?(/^[a-zA-Z][a-zA-Z0-9_]*$/)
|
116
|
+
@errors << "#{context}: Invalid path parameter name '#{param_name}'"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate_input(input, context)
|
122
|
+
unless input.respond_to?(:kind) && input.respond_to?(:name) && input.respond_to?(:type)
|
123
|
+
@errors << "#{context}: Input missing required methods (kind, name, type)"
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
validate_input_kind(input, context)
|
128
|
+
validate_input_name(input, context)
|
129
|
+
validate_input_type(input, context)
|
130
|
+
validate_input_options_if_present(input, context)
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_input_kind(input, context)
|
134
|
+
valid_kinds = %i[query path header body]
|
135
|
+
return if valid_kinds.include?(input.kind)
|
136
|
+
|
137
|
+
@errors << "#{context}: Invalid input kind '#{input.kind}'"
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_input_name(input, context)
|
141
|
+
return if input.name.is_a?(Symbol) || input.name.is_a?(String)
|
142
|
+
|
143
|
+
@errors << "#{context}: Input name must be a symbol or string"
|
144
|
+
end
|
145
|
+
|
146
|
+
def validate_input_type(input, context)
|
147
|
+
validate_type(input.type, "#{context} type")
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_input_options_if_present(input, context)
|
151
|
+
return unless input.respond_to?(:options) && input.options
|
152
|
+
|
153
|
+
validate_input_options(input.options, context)
|
154
|
+
end
|
155
|
+
|
156
|
+
def validate_output(output, context)
|
157
|
+
unless output.respond_to?(:kind) && output.respond_to?(:type)
|
158
|
+
@errors << "#{context}: Output missing required methods (kind, type)"
|
159
|
+
return
|
160
|
+
end
|
161
|
+
|
162
|
+
validate_output_kind(output, context)
|
163
|
+
validate_output_type_for_kind(output, context)
|
164
|
+
end
|
165
|
+
|
166
|
+
def validate_output_kind(output, context)
|
167
|
+
valid_kinds = %i[json xml status header]
|
168
|
+
return if valid_kinds.include?(output.kind)
|
169
|
+
|
170
|
+
@errors << "#{context}: Invalid output kind '#{output.kind}'"
|
171
|
+
end
|
172
|
+
|
173
|
+
def validate_output_type_for_kind(output, context)
|
174
|
+
case output.kind
|
175
|
+
when :status
|
176
|
+
validate_status_code_type(output.type, context)
|
177
|
+
when :json, :xml
|
178
|
+
validate_type(output.type, "#{context} schema")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate_status_code_type(status_code, context)
|
183
|
+
return if status_code.is_a?(Integer) && status_code >= 100 && status_code <= 599
|
184
|
+
|
185
|
+
@errors << "#{context}: Status code must be an integer between 100-599"
|
186
|
+
end
|
187
|
+
|
188
|
+
def validate_type(type, context)
|
189
|
+
case type
|
190
|
+
when Symbol
|
191
|
+
validate_symbol_type(type, context)
|
192
|
+
when Hash
|
193
|
+
validate_hash_schema(type, context)
|
194
|
+
when Array
|
195
|
+
validate_array_type(type, context)
|
196
|
+
when Class, RapiTapir::Types::Base
|
197
|
+
# Allow custom classes and enhanced types - these are valid
|
198
|
+
else
|
199
|
+
@errors << "#{context}: Invalid type '#{type}'"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def validate_symbol_type(type, context)
|
204
|
+
valid_simple_types = %i[string integer float boolean date datetime]
|
205
|
+
return if valid_simple_types.include?(type)
|
206
|
+
|
207
|
+
@errors << "#{context}: Unknown type '#{type}'"
|
208
|
+
end
|
209
|
+
|
210
|
+
def validate_array_type(type, context)
|
211
|
+
if type.empty?
|
212
|
+
@errors << "#{context}: Array type cannot be empty"
|
213
|
+
else
|
214
|
+
type.each_with_index do |element_type, index|
|
215
|
+
validate_type(element_type, "#{context}[#{index}]")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def validate_hash_schema(schema, context)
|
221
|
+
schema.each do |key, value|
|
222
|
+
unless key.is_a?(Symbol) || key.is_a?(String)
|
223
|
+
@errors << "#{context}: Hash key '#{key}' must be a symbol or string"
|
224
|
+
end
|
225
|
+
|
226
|
+
validate_type(value, "#{context}.#{key}")
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def validate_input_options(options, context)
|
231
|
+
unless options.is_a?(Hash)
|
232
|
+
@errors << "#{context}: Options must be a hash"
|
233
|
+
return
|
234
|
+
end
|
235
|
+
|
236
|
+
# Check for conflicting options
|
237
|
+
@errors << "#{context}: Cannot be both required and optional" if options[:required] && options[:optional]
|
238
|
+
|
239
|
+
# Validate description if present
|
240
|
+
return unless options[:description] && !options[:description].is_a?(String)
|
241
|
+
|
242
|
+
@errors << "#{context}: Description must be a string"
|
243
|
+
end
|
244
|
+
|
245
|
+
def validate_path_parameters_consistency(endpoint, context)
|
246
|
+
path_param_names = extract_path_parameter_names(endpoint)
|
247
|
+
input_path_params = extract_input_path_parameters(endpoint)
|
248
|
+
|
249
|
+
validate_missing_path_inputs(path_param_names, input_path_params, context)
|
250
|
+
validate_extra_path_inputs(path_param_names, input_path_params, context)
|
251
|
+
end
|
252
|
+
|
253
|
+
def extract_path_parameter_names(endpoint)
|
254
|
+
endpoint.path.scan(/:(\w+)/).flatten.map(&:to_sym)
|
255
|
+
end
|
256
|
+
|
257
|
+
def extract_input_path_parameters(endpoint)
|
258
|
+
return [] unless endpoint.respond_to?(:inputs)
|
259
|
+
|
260
|
+
endpoint.inputs
|
261
|
+
.select { |input| input.kind == :path }
|
262
|
+
.map(&:name)
|
263
|
+
.map(&:to_sym)
|
264
|
+
end
|
265
|
+
|
266
|
+
def validate_missing_path_inputs(path_param_names, input_path_params, context)
|
267
|
+
missing_inputs = path_param_names - input_path_params
|
268
|
+
return if missing_inputs.empty?
|
269
|
+
|
270
|
+
@errors << "#{context}: Missing input definitions for path parameters: #{missing_inputs.join(', ')}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def validate_extra_path_inputs(path_param_names, input_path_params, context)
|
274
|
+
extra_inputs = input_path_params - path_param_names
|
275
|
+
return if extra_inputs.empty?
|
276
|
+
|
277
|
+
@errors << "#{context}: Extra path input definitions (not in path): #{extra_inputs.join(', ')}"
|
278
|
+
end
|
279
|
+
|
280
|
+
def validate_metadata(endpoint, context)
|
281
|
+
return unless endpoint.respond_to?(:metadata)
|
282
|
+
|
283
|
+
metadata = endpoint.metadata
|
284
|
+
return unless metadata.is_a?(Hash)
|
285
|
+
|
286
|
+
validate_metadata_summary(metadata, context)
|
287
|
+
validate_metadata_description(metadata, context)
|
288
|
+
validate_metadata_tags(metadata, context)
|
289
|
+
validate_metadata_deprecated_flag(metadata, context)
|
290
|
+
end
|
291
|
+
|
292
|
+
def validate_metadata_summary(metadata, context)
|
293
|
+
return unless metadata[:summary] && !metadata[:summary].is_a?(String)
|
294
|
+
|
295
|
+
@errors << "#{context}: Summary must be a string"
|
296
|
+
end
|
297
|
+
|
298
|
+
def validate_metadata_description(metadata, context)
|
299
|
+
return unless metadata[:description] && !metadata[:description].is_a?(String)
|
300
|
+
|
301
|
+
@errors << "#{context}: Description must be a string"
|
302
|
+
end
|
303
|
+
|
304
|
+
def validate_metadata_tags(metadata, context)
|
305
|
+
return unless metadata[:tags]
|
306
|
+
return if metadata[:tags].is_a?(Array) && metadata[:tags].all? { |tag| tag.is_a?(String) }
|
307
|
+
|
308
|
+
@errors << "#{context}: Tags must be an array of strings"
|
309
|
+
end
|
310
|
+
|
311
|
+
def validate_metadata_deprecated_flag(metadata, context)
|
312
|
+
return unless metadata[:deprecated] && ![true, false].include?(metadata[:deprecated])
|
313
|
+
|
314
|
+
@errors << "#{context}: Deprecated must be a boolean"
|
315
|
+
end
|
316
|
+
|
317
|
+
def validate_parameters(endpoint)
|
318
|
+
return unless endpoint.input_specs
|
319
|
+
|
320
|
+
validate_body_parameter_count(endpoint)
|
321
|
+
validate_parameter_types(endpoint)
|
322
|
+
end
|
323
|
+
|
324
|
+
def validate_body_parameter_count(endpoint)
|
325
|
+
body_params = endpoint.input_specs.select { |spec| spec.type == :body }
|
326
|
+
return unless body_params.length > 1
|
327
|
+
|
328
|
+
@errors << "#{endpoint.path}: multiple body parameters not allowed"
|
329
|
+
end
|
330
|
+
|
331
|
+
def validate_parameter_types(endpoint)
|
332
|
+
endpoint.input_specs.each do |input_spec|
|
333
|
+
next unless input_spec.respond_to?(:param_type)
|
334
|
+
next if valid_param_type?(input_spec.param_type)
|
335
|
+
|
336
|
+
@errors << "#{endpoint.path}: invalid parameter type '#{input_spec.param_type}'"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def valid_param_type?(type)
|
341
|
+
valid_types = [:string, :integer, :float, :boolean, :date, :datetime, Hash, Array]
|
342
|
+
valid_types.include?(type)
|
343
|
+
end
|
344
|
+
|
345
|
+
def validate_basic_properties(endpoint)
|
346
|
+
validate_summary_property(endpoint)
|
347
|
+
validate_output_property(endpoint)
|
348
|
+
end
|
349
|
+
|
350
|
+
def validate_summary_property(endpoint)
|
351
|
+
return if endpoint.metadata&.dig(:summary) && !endpoint.metadata[:summary].empty?
|
352
|
+
|
353
|
+
@errors << "#{endpoint.path}: missing summary"
|
354
|
+
end
|
355
|
+
|
356
|
+
def validate_output_property(endpoint)
|
357
|
+
return if valid_outputs?(endpoint)
|
358
|
+
|
359
|
+
@errors << "#{endpoint.path}: missing output definition"
|
360
|
+
end
|
361
|
+
|
362
|
+
def valid_outputs?(endpoint)
|
363
|
+
endpoint.respond_to?(:outputs) && endpoint.outputs&.any?
|
364
|
+
end
|
365
|
+
|
366
|
+
def valid_output_definition?(endpoint)
|
367
|
+
return true if endpoint.respond_to?(:outputs) && endpoint.outputs && !endpoint.outputs.empty?
|
368
|
+
|
369
|
+
false
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|