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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -7
- data/.rubocop_todo.yml +83 -0
- data/README.md +1319 -235
- data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
- data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
- data/docs/SINATRA_EXTENSION.md +399 -348
- data/docs/STRICT_VALIDATION.md +229 -0
- data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
- data/docs/ai-integration-plan.md +112 -0
- data/docs/auto-derivation.md +505 -92
- data/docs/endpoint-definition.md +536 -129
- data/docs/n8n-integration.md +212 -0
- data/docs/observability.md +810 -500
- data/docs/using-mcp.md +93 -0
- data/examples/ai/knowledge_base_rag.rb +83 -0
- data/examples/ai/user_management_mcp.rb +92 -0
- data/examples/ai/user_validation_llm.rb +187 -0
- data/examples/rails/RAILS_8_GUIDE.md +165 -0
- data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
- data/examples/rails/README.md +497 -0
- data/examples/rails/comprehensive_test.rb +91 -0
- data/examples/rails/config/routes.rb +48 -0
- data/examples/rails/debug_controller.rb +63 -0
- data/examples/rails/detailed_test.rb +46 -0
- data/examples/rails/enhanced_users_controller.rb +278 -0
- data/examples/rails/final_server_test.rb +50 -0
- data/examples/rails/hello_world_app.rb +116 -0
- data/examples/rails/hello_world_controller.rb +186 -0
- data/examples/rails/hello_world_routes.rb +28 -0
- data/examples/rails/rails8_minimal_demo.rb +132 -0
- data/examples/rails/rails8_simple_demo.rb +140 -0
- data/examples/rails/rails8_working_demo.rb +255 -0
- data/examples/rails/real_world_blog_api.rb +510 -0
- data/examples/rails/server_test.rb +46 -0
- data/examples/rails/test_direct_processing.rb +41 -0
- data/examples/rails/test_hello_world.rb +80 -0
- data/examples/rails/test_rails_integration.rb +54 -0
- data/examples/rails/traditional_app/Gemfile +37 -0
- data/examples/rails/traditional_app/README.md +265 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
- data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
- data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
- data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
- data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
- data/examples/rails/traditional_app/config/routes.rb +25 -0
- data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
- data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
- data/examples/rails/traditional_app_runnable.rb +406 -0
- data/examples/rails/users_controller.rb +4 -1
- data/examples/serverless/Gemfile +43 -0
- data/examples/serverless/QUICKSTART.md +331 -0
- data/examples/serverless/README.md +520 -0
- data/examples/serverless/aws_lambda_example.rb +307 -0
- data/examples/serverless/aws_sam_template.yaml +215 -0
- data/examples/serverless/azure_functions_example.rb +407 -0
- data/examples/serverless/deploy.rb +204 -0
- data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
- data/examples/serverless/gcp_function.yaml +23 -0
- data/examples/serverless/host.json +24 -0
- data/examples/serverless/package.json +32 -0
- data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
- data/examples/serverless/spec/spec_helper.rb +89 -0
- data/examples/serverless/vercel.json +31 -0
- data/examples/serverless/vercel_example.rb +404 -0
- data/examples/strict_validation_examples.rb +104 -0
- data/examples/validation_error_examples.rb +173 -0
- data/lib/rapitapir/ai/llm_instruction.rb +456 -0
- data/lib/rapitapir/ai/mcp.rb +134 -0
- data/lib/rapitapir/ai/rag.rb +287 -0
- data/lib/rapitapir/ai/rag_middleware.rb +147 -0
- data/lib/rapitapir/auth/oauth2.rb +43 -57
- data/lib/rapitapir/cli/command.rb +362 -2
- data/lib/rapitapir/cli/mcp_export.rb +18 -0
- data/lib/rapitapir/cli/validator.rb +2 -6
- data/lib/rapitapir/core/endpoint.rb +59 -6
- data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
- data/lib/rapitapir/endpoint_registry.rb +47 -0
- data/lib/rapitapir/observability/health_check.rb +4 -4
- data/lib/rapitapir/observability/logging.rb +10 -10
- data/lib/rapitapir/schema.rb +2 -2
- data/lib/rapitapir/server/rack_adapter.rb +1 -3
- data/lib/rapitapir/server/rails/configuration.rb +77 -0
- data/lib/rapitapir/server/rails/controller_base.rb +185 -0
- data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
- data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
- data/lib/rapitapir/server/rails/routes.rb +114 -0
- data/lib/rapitapir/server/rails_adapter.rb +10 -3
- data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
- data/lib/rapitapir/server/rails_controller.rb +1 -3
- data/lib/rapitapir/server/rails_integration.rb +67 -0
- data/lib/rapitapir/server/rails_response_handler.rb +16 -3
- data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
- data/lib/rapitapir/server/sinatra_integration.rb +4 -4
- data/lib/rapitapir/sinatra/extension.rb +2 -2
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
- data/lib/rapitapir/types/array.rb +4 -0
- data/lib/rapitapir/types/auto_derivation.rb +4 -18
- data/lib/rapitapir/types/datetime.rb +1 -3
- data/lib/rapitapir/types/float.rb +2 -6
- data/lib/rapitapir/types/hash.rb +40 -2
- data/lib/rapitapir/types/integer.rb +4 -12
- data/lib/rapitapir/types/object.rb +6 -2
- data/lib/rapitapir/types.rb +6 -2
- data/lib/rapitapir/version.rb +1 -1
- data/lib/rapitapir.rb +5 -3
- 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
|