rapitapir 0.1.2 → 2.0.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. metadata +74 -2
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RapiTapir Validation Error Examples
4
+ #
5
+ # This file demonstrates the improved validation error messages in RapiTapir v2.0
6
+ # Run this script to see the enhanced developer experience for API validation.
7
+
8
+ require_relative '../lib/rapitapir'
9
+
10
+ puts "🔍 RapiTapir v2.0 - Enhanced Validation Error Messages"
11
+ puts "=" * 60
12
+
13
+ # Define a realistic API schema
14
+ USER_SCHEMA = RapiTapir::Types.hash({
15
+ 'name' => RapiTapir::Types.string(min_length: 2, max_length: 50),
16
+ 'email' => RapiTapir::Types.email,
17
+ 'age' => RapiTapir::Types.integer(minimum: 18, maximum: 120),
18
+ 'profile' => RapiTapir::Types.hash({
19
+ 'bio' => RapiTapir::Types.string(max_length: 200),
20
+ 'website' => RapiTapir::Types.string(format: :uri),
21
+ 'newsletter' => RapiTapir::Types.boolean
22
+ })
23
+ })
24
+
25
+ def show_validation_example(title, data, schema = USER_SCHEMA)
26
+ puts "\n#{title}"
27
+ puts "-" * 40
28
+ puts "📥 Input: #{data.inspect}"
29
+
30
+ begin
31
+ # First try coercion (type conversion + missing field checks)
32
+ coerced = schema.coerce(data)
33
+
34
+ # Then validate the coerced data (constraint checks)
35
+ validation_result = schema.validate(coerced)
36
+
37
+ if validation_result[:valid]
38
+ puts "✅ Success: #{coerced.inspect}"
39
+ else
40
+ puts "❌ Validation Failed: #{validation_result[:errors].first}"
41
+ puts "📋 All errors: #{validation_result[:errors].join(', ')}"
42
+ end
43
+
44
+ rescue RapiTapir::Types::CoercionError => e
45
+ puts "❌ Coercion Error: #{e.message}"
46
+ puts "🎯 Field: #{extract_field_from_error(e)}" if extract_field_from_error(e)
47
+ puts "📋 Expected: #{e.type}"
48
+ puts "💭 Received: #{e.value.inspect}"
49
+ end
50
+ end
51
+
52
+ def extract_field_from_error(error)
53
+ if error.reason =~ /Field '([^']+)':/
54
+ $1
55
+ elsif error.reason =~ /Required field '([^']+)'/
56
+ $1
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ # Example 1: Missing required field
63
+ show_validation_example(
64
+ "1️⃣ Missing Required Field",
65
+ {
66
+ 'name' => 'John Doe',
67
+ 'age' => 25,
68
+ 'profile' => {
69
+ 'bio' => 'Software developer',
70
+ 'newsletter' => true
71
+ }
72
+ # Missing 'email' field
73
+ }
74
+ )
75
+
76
+ # Example 2: Invalid email format
77
+ show_validation_example(
78
+ "2️⃣ Invalid Email Format",
79
+ {
80
+ 'name' => 'John Doe',
81
+ 'email' => 'not-an-email',
82
+ 'age' => 25,
83
+ 'profile' => {
84
+ 'bio' => 'Software developer',
85
+ 'website' => 'https://example.com',
86
+ 'newsletter' => true
87
+ }
88
+ }
89
+ )
90
+
91
+ # Example 3: Age constraint violation
92
+ show_validation_example(
93
+ "3️⃣ Age Constraint Violation",
94
+ {
95
+ 'name' => 'John Doe',
96
+ 'email' => 'john@example.com',
97
+ 'age' => 16, # Under 18
98
+ 'profile' => {
99
+ 'bio' => 'High school student',
100
+ 'website' => 'https://example.com',
101
+ 'newsletter' => false
102
+ }
103
+ }
104
+ )
105
+
106
+ # Example 4: Invalid nested field
107
+ show_validation_example(
108
+ "4️⃣ Invalid Nested Field",
109
+ {
110
+ 'name' => 'John Doe',
111
+ 'email' => 'john@example.com',
112
+ 'age' => 25,
113
+ 'profile' => {
114
+ 'bio' => 'Software developer',
115
+ 'website' => 'not-a-valid-url',
116
+ 'newsletter' => 'yes' # Should be boolean
117
+ }
118
+ }
119
+ )
120
+
121
+ # Example 5: Missing nested required field
122
+ show_validation_example(
123
+ "5️⃣ Missing Nested Required Field",
124
+ {
125
+ 'name' => 'John Doe',
126
+ 'email' => 'john@example.com',
127
+ 'age' => 25,
128
+ 'profile' => {
129
+ 'bio' => 'Software developer',
130
+ 'website' => 'https://example.com'
131
+ # Missing 'newsletter' field
132
+ }
133
+ }
134
+ )
135
+
136
+ # Example 6: String length validation
137
+ show_validation_example(
138
+ "6️⃣ String Length Validation",
139
+ {
140
+ 'name' => 'J', # Too short (min 2 chars)
141
+ 'email' => 'john@example.com',
142
+ 'age' => 25,
143
+ 'profile' => {
144
+ 'bio' => 'A' * 250, # Too long (max 200 chars)
145
+ 'website' => 'https://example.com',
146
+ 'newsletter' => true
147
+ }
148
+ }
149
+ )
150
+
151
+ # Example 7: Successful validation
152
+ show_validation_example(
153
+ "7️⃣ Successful Validation ✨",
154
+ {
155
+ 'name' => 'John Doe',
156
+ 'email' => 'john@example.com',
157
+ 'age' => 25,
158
+ 'profile' => {
159
+ 'bio' => 'Software developer passionate about APIs',
160
+ 'website' => 'https://johndoe.dev',
161
+ 'newsletter' => true
162
+ }
163
+ }
164
+ )
165
+
166
+ puts "\n🎉 Enhanced Error Messages Summary:"
167
+ puts " ✅ Specific field names in error messages"
168
+ puts " ✅ Clear indication of missing vs invalid fields"
169
+ puts " ✅ Expected vs received value information"
170
+ puts " ✅ Nested object validation with field context"
171
+ puts " ✅ Constraint violation details (length, format, etc.)"
172
+ puts " ✅ Maintains backward compatibility"
173
+ puts "\n💡 This improves the developer experience by providing actionable feedback!"
@@ -0,0 +1,456 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RapiTapir::AI::LLMInstruction
4
+ #
5
+ # Generates LLM instructions and prompts from endpoint schemas and metadata.
6
+ # Supports multiple instruction purposes like validation, transformation, and analysis.
7
+ #
8
+ # Usage:
9
+ # - Use `.llm_instruction(purpose:, fields:)` in endpoint DSL to mark endpoints for instruction generation.
10
+ # - Use InstructionGenerator to create structured prompts from endpoint schemas.
11
+
12
+ module RapiTapir
13
+ module AI
14
+ module LLMInstruction
15
+ # Generates structured LLM instructions from endpoint definitions
16
+ class Generator
17
+ SUPPORTED_PURPOSES = %i[
18
+ validation # Generate validation prompts for input/output
19
+ transformation # Generate data transformation instructions
20
+ analysis # Generate analysis and summarization prompts
21
+ documentation # Generate documentation from schemas
22
+ testing # Generate test case instructions
23
+ completion # Generate field completion suggestions
24
+ ].freeze
25
+
26
+ def initialize(endpoints)
27
+ @endpoints = endpoints
28
+ end
29
+
30
+ # Generate instructions for all LLM-enabled endpoints
31
+ def generate_all_instructions
32
+ llm_endpoints = @endpoints.select(&:llm_instruction?)
33
+
34
+ instructions = llm_endpoints.map do |endpoint|
35
+ config = endpoint.llm_instruction_config
36
+ generate_instruction(endpoint, config)
37
+ end.compact
38
+
39
+ {
40
+ meta: {
41
+ generator: 'RapiTapir LLM Instruction Generator',
42
+ version: '1.0.0',
43
+ generated_at: Time.now.iso8601,
44
+ total_instructions: instructions.size
45
+ },
46
+ instructions: instructions
47
+ }
48
+ end
49
+
50
+ # Generate instruction for a single endpoint
51
+ def generate_instruction(endpoint, config)
52
+ purpose = config[:purpose]
53
+ fields = config[:fields] || :all
54
+
55
+ raise ArgumentError, "Unsupported purpose: #{purpose}. Supported: #{SUPPORTED_PURPOSES.join(', ')}" unless SUPPORTED_PURPOSES.include?(purpose)
56
+
57
+ {
58
+ endpoint_id: endpoint_id(endpoint),
59
+ method: endpoint.method&.to_s&.upcase,
60
+ path: endpoint.path,
61
+ purpose: purpose,
62
+ instruction: build_instruction(endpoint, purpose, fields),
63
+ schema_context: extract_schema_context(endpoint, fields),
64
+ examples: extract_examples(endpoint),
65
+ metadata: {
66
+ summary: endpoint.metadata[:summary],
67
+ description: endpoint.metadata[:description],
68
+ generated_at: Time.now.iso8601
69
+ }
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def endpoint_id(endpoint)
76
+ "#{endpoint.method}_#{endpoint.path}".gsub(/[^a-zA-Z0-9_]/, '_').downcase
77
+ end
78
+
79
+ def build_instruction(endpoint, purpose, fields)
80
+ case purpose
81
+ when :validation
82
+ build_validation_instruction(endpoint, fields)
83
+ when :transformation
84
+ build_transformation_instruction(endpoint, fields)
85
+ when :analysis
86
+ build_analysis_instruction(endpoint, fields)
87
+ when :documentation
88
+ build_documentation_instruction(endpoint, fields)
89
+ when :testing
90
+ build_testing_instruction(endpoint, fields)
91
+ when :completion
92
+ build_completion_instruction(endpoint, fields)
93
+ else
94
+ raise ArgumentError, "Unknown purpose: #{purpose}"
95
+ end
96
+ end
97
+
98
+ def build_validation_instruction(endpoint, fields)
99
+ input_schema = extract_input_fields(endpoint, fields)
100
+ output_schema = extract_output_fields(endpoint, fields)
101
+
102
+ [
103
+ "You are a data validation assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
104
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
105
+ '',
106
+ 'INPUT VALIDATION:',
107
+ 'Validate the following input data against these requirements:',
108
+ format_schema_for_validation(input_schema),
109
+ '',
110
+ 'OUTPUT VALIDATION:',
111
+ 'Ensure the response data matches this structure:',
112
+ format_schema_for_validation(output_schema),
113
+ '',
114
+ 'INSTRUCTIONS:',
115
+ '1. Check all required fields are present',
116
+ '2. Validate data types match the schema',
117
+ '3. Verify constraints (min/max length, patterns, etc.)',
118
+ '4. Report any validation errors with specific field names',
119
+ "5. Confirm successful validation with 'VALID' or list errors"
120
+ ].compact.join("\n")
121
+ end
122
+
123
+ def build_transformation_instruction(endpoint, fields)
124
+ input_schema = extract_input_fields(endpoint, fields)
125
+ output_schema = extract_output_fields(endpoint, fields)
126
+
127
+ [
128
+ "You are a data transformation assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
129
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
130
+ '',
131
+ 'TRANSFORMATION TASK:',
132
+ 'Transform input data from this format:',
133
+ format_schema_for_transformation(input_schema),
134
+ '',
135
+ 'To this output format:',
136
+ format_schema_for_transformation(output_schema),
137
+ '',
138
+ 'INSTRUCTIONS:',
139
+ '1. Map all relevant input fields to appropriate output fields',
140
+ '2. Apply any necessary data type conversions',
141
+ '3. Calculate derived fields based on business logic',
142
+ '4. Ensure all required output fields are populated',
143
+ '5. Return the transformed data in the exact output schema format'
144
+ ].compact.join("\n")
145
+ end
146
+
147
+ def build_analysis_instruction(endpoint, fields)
148
+ schema_context = extract_schema_context(endpoint, fields)
149
+
150
+ [
151
+ "You are a data analysis assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
152
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
153
+ '',
154
+ 'ANALYSIS CONTEXT:',
155
+ 'Analyze data related to this API endpoint structure:',
156
+ format_schema_for_analysis(schema_context),
157
+ '',
158
+ 'INSTRUCTIONS:',
159
+ '1. Identify patterns and trends in the data',
160
+ '2. Summarize key insights and findings',
161
+ '3. Highlight any anomalies or unusual values',
162
+ '4. Provide actionable recommendations based on the data',
163
+ '5. Format your analysis clearly with sections for patterns, insights, and recommendations'
164
+ ].compact.join("\n")
165
+ end
166
+
167
+ def build_documentation_instruction(endpoint, fields)
168
+ schema_context = extract_schema_context(endpoint, fields)
169
+
170
+ [
171
+ "You are a technical documentation assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
172
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
173
+ '',
174
+ 'DOCUMENTATION TASK:',
175
+ 'Generate comprehensive documentation for this API endpoint:',
176
+ format_schema_for_documentation(schema_context),
177
+ '',
178
+ 'INSTRUCTIONS:',
179
+ '1. Write a clear endpoint description and purpose',
180
+ '2. Document all input parameters with types and constraints',
181
+ '3. Describe the response format and all output fields',
182
+ '4. Include practical usage examples',
183
+ '5. Note any special requirements or business rules',
184
+ '6. Format as clean, readable API documentation'
185
+ ].compact.join("\n")
186
+ end
187
+
188
+ def build_testing_instruction(endpoint, fields)
189
+ schema_context = extract_schema_context(endpoint, fields)
190
+
191
+ [
192
+ "You are a test case generation assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
193
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
194
+ '',
195
+ 'TEST GENERATION TASK:',
196
+ 'Create comprehensive test cases for this API endpoint:',
197
+ format_schema_for_testing(schema_context),
198
+ '',
199
+ 'INSTRUCTIONS:',
200
+ '1. Generate positive test cases with valid inputs',
201
+ '2. Create negative test cases for invalid inputs',
202
+ '3. Test boundary conditions and edge cases',
203
+ '4. Include tests for required vs optional fields',
204
+ '5. Provide expected responses for each test case',
205
+ '6. Format test cases with clear descriptions and assertions'
206
+ ].compact.join("\n")
207
+ end
208
+
209
+ def build_completion_instruction(endpoint, fields)
210
+ input_schema = extract_input_fields(endpoint, fields)
211
+
212
+ [
213
+ "You are a smart completion assistant for the #{endpoint.method&.upcase} #{endpoint.path} API endpoint.",
214
+ endpoint.metadata[:summary] ? "Purpose: #{endpoint.metadata[:summary]}" : nil,
215
+ '',
216
+ 'COMPLETION TASK:',
217
+ 'Provide intelligent field completion suggestions based on this schema:',
218
+ format_schema_for_completion(input_schema),
219
+ '',
220
+ 'INSTRUCTIONS:',
221
+ '1. Analyze partial input data provided by the user',
222
+ '2. Suggest realistic values for incomplete fields',
223
+ '3. Ensure suggestions match the field types and constraints',
224
+ '4. Provide multiple options when appropriate',
225
+ '5. Explain the reasoning behind each suggestion',
226
+ '6. Format suggestions clearly with field names and proposed values'
227
+ ].compact.join("\n")
228
+ end
229
+
230
+ def extract_schema_context(endpoint, fields)
231
+ {
232
+ inputs: extract_input_fields(endpoint, fields),
233
+ outputs: extract_output_fields(endpoint, fields),
234
+ errors: extract_error_fields(endpoint)
235
+ }
236
+ end
237
+
238
+ def extract_input_fields(endpoint, fields)
239
+ return {} unless endpoint.inputs
240
+
241
+ input_schema = {}
242
+ endpoint.inputs.each do |input|
243
+ next unless include_field?(input.name, fields)
244
+
245
+ input_schema[input.name] = {
246
+ type: describe_type(input.type),
247
+ kind: input.kind,
248
+ required: input.respond_to?(:required?) ? input.required? : true,
249
+ description: input.respond_to?(:description) ? input.description : nil
250
+ }
251
+ end
252
+ input_schema
253
+ end
254
+
255
+ def extract_output_fields(endpoint, fields)
256
+ return {} unless endpoint.outputs
257
+
258
+ output_schema = {}
259
+ endpoint.outputs.each do |output|
260
+ next if output.kind == :status
261
+ next unless include_field?(output.kind, fields)
262
+
263
+ output_schema[output.kind] = {
264
+ type: describe_type(output.type),
265
+ description: output.respond_to?(:description) ? output.description : nil
266
+ }
267
+ end
268
+ output_schema
269
+ end
270
+
271
+ def extract_error_fields(endpoint)
272
+ return {} unless endpoint.errors
273
+
274
+ error_schema = {}
275
+ endpoint.errors.each do |error|
276
+ # Handle both old hash format and new EnhancedError objects
277
+ if error.respond_to?(:status_code)
278
+ # New EnhancedError object
279
+ error_schema[error.status_code] = {
280
+ type: describe_type(error.type),
281
+ description: error.description
282
+ }
283
+ elsif error.is_a?(Hash)
284
+ # Old hash format
285
+ error_schema[error[:code]] = {
286
+ type: describe_type(error[:output]&.type),
287
+ description: error[:description]
288
+ }
289
+ end
290
+ end
291
+ error_schema
292
+ end
293
+
294
+ def extract_examples(endpoint)
295
+ examples = endpoint.metadata[:examples] || []
296
+ examples.is_a?(Array) ? examples : [examples]
297
+ end
298
+
299
+ def include_field?(field_name, fields)
300
+ return true if fields == :all
301
+ return true if fields.nil?
302
+ return true if fields.empty?
303
+
304
+ fields = [fields] unless fields.is_a?(Array)
305
+ fields.include?(field_name) || fields.include?(field_name.to_s) || fields.include?(field_name.to_sym)
306
+ end
307
+
308
+ def describe_type(type)
309
+ return 'unknown' unless type
310
+
311
+ case type
312
+ when Types::String
313
+ constraints = []
314
+ constraints << "min_length: #{type.constraints[:min_length]}" if type.constraints[:min_length]
315
+ constraints << "max_length: #{type.constraints[:max_length]}" if type.constraints[:max_length]
316
+ constraints << "pattern: #{type.constraints[:pattern]}" if type.constraints[:pattern]
317
+
318
+ base = 'string'
319
+ constraints.empty? ? base : "#{base} (#{constraints.join(', ')})"
320
+ when Types::Integer
321
+ constraints = []
322
+ constraints << "min: #{type.constraints[:minimum]}" if type.constraints[:minimum]
323
+ constraints << "max: #{type.constraints[:maximum]}" if type.constraints[:maximum]
324
+
325
+ base = 'integer'
326
+ constraints.empty? ? base : "#{base} (#{constraints.join(', ')})"
327
+ when Types::Array
328
+ item_type = describe_type(type.item_type)
329
+ "array of #{item_type}"
330
+ when Types::Hash
331
+ if type.field_types.any?
332
+ fields = type.field_types.map { |k, v| "#{k}: #{describe_type(v)}" }.join(', ')
333
+ "object {#{fields}}"
334
+ else
335
+ 'object'
336
+ end
337
+ when Types::Optional
338
+ "optional #{describe_type(type.wrapped_type)}"
339
+ else
340
+ type.class.name.split('::').last.downcase
341
+ end
342
+ end
343
+
344
+ def format_schema_for_validation(schema)
345
+ return 'No schema defined' if schema.empty?
346
+
347
+ schema.map do |name, info|
348
+ required_text = info[:required] ? 'REQUIRED' : 'optional'
349
+ description_text = info[:description] ? " - #{info[:description]}" : ''
350
+ "- #{name}: #{info[:type]} (#{required_text})#{description_text}"
351
+ end.join("\n")
352
+ end
353
+
354
+ def format_schema_for_transformation(schema)
355
+ return 'No schema defined' if schema.empty?
356
+
357
+ schema.map do |name, info|
358
+ description_text = info[:description] ? " // #{info[:description]}" : ''
359
+ "#{name}: #{info[:type]}#{description_text}"
360
+ end.join("\n")
361
+ end
362
+
363
+ def format_schema_for_analysis(schema_context)
364
+ sections = []
365
+
366
+ unless schema_context[:inputs].empty?
367
+ sections << 'Input Fields:'
368
+ sections += schema_context[:inputs].map { |name, info| "- #{name}: #{info[:type]}" }
369
+ end
370
+
371
+ unless schema_context[:outputs].empty?
372
+ sections << "\nOutput Fields:"
373
+ sections += schema_context[:outputs].map { |name, info| "- #{name}: #{info[:type]}" }
374
+ end
375
+
376
+ sections.join("\n")
377
+ end
378
+
379
+ def format_schema_for_documentation(schema_context)
380
+ format_schema_for_analysis(schema_context)
381
+ end
382
+
383
+ def format_schema_for_testing(schema_context)
384
+ format_schema_for_analysis(schema_context)
385
+ end
386
+
387
+ def format_schema_for_completion(schema)
388
+ return 'No input fields defined' if schema.empty?
389
+
390
+ schema.map do |name, info|
391
+ required_text = info[:required] ? ' (required)' : ' (optional)'
392
+ description_text = info[:description] ? " - #{info[:description]}" : ''
393
+ "#{name}: #{info[:type]}#{required_text}#{description_text}"
394
+ end.join("\n")
395
+ end
396
+ end
397
+
398
+ # Export utility for different output formats
399
+ class Exporter
400
+ def initialize(instructions_data)
401
+ @data = instructions_data
402
+ end
403
+
404
+ def to_json(*_args)
405
+ require 'json'
406
+ JSON.pretty_generate(@data)
407
+ end
408
+
409
+ def to_yaml
410
+ require 'yaml'
411
+ YAML.dump(@data)
412
+ end
413
+
414
+ def to_markdown
415
+ md = []
416
+ md << '# LLM Instructions'
417
+ md << ''
418
+ md << "Generated: #{@data[:meta][:generated_at]}"
419
+ md << "Total Instructions: #{@data[:meta][:total_instructions]}"
420
+ md << ''
421
+
422
+ @data[:instructions].each do |instruction|
423
+ md << "## #{instruction[:method]} #{instruction[:path]}"
424
+ md << ''
425
+ md << "**Purpose:** #{instruction[:purpose]}"
426
+ md << ''
427
+ md << "**Summary:** #{instruction[:metadata][:summary]}" if instruction[:metadata][:summary]
428
+ md << ''
429
+ md << '### Instruction'
430
+ md << ''
431
+ md << '```'
432
+ md << instruction[:instruction]
433
+ md << '```'
434
+ md << ''
435
+ end
436
+
437
+ md.join("\n")
438
+ end
439
+
440
+ def to_prompt_files(output_dir)
441
+ require 'fileutils'
442
+ FileUtils.mkdir_p(output_dir)
443
+
444
+ @data[:instructions].each do |instruction|
445
+ filename = "#{instruction[:endpoint_id]}_#{instruction[:purpose]}.txt"
446
+ filepath = File.join(output_dir, filename)
447
+
448
+ File.write(filepath, instruction[:instruction])
449
+ end
450
+
451
+ "Exported #{@data[:instructions].size} prompt files to #{output_dir}"
452
+ end
453
+ end
454
+ end
455
+ end
456
+ end