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