rapitapir 0.1.1 → 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
- data/rapitapir.gemspec +7 -5
- metadata +116 -16
@@ -11,20 +11,20 @@ module RapiTapir
|
|
11
11
|
# Based on the Auth0 Sinatra integration patterns
|
12
12
|
class Auth0Scheme < Schemes::Base
|
13
13
|
attr_reader :domain, :audience, :algorithm, :jwks_cache_ttl
|
14
|
-
|
14
|
+
|
15
15
|
def jwks_url
|
16
16
|
"https://#{@domain}/.well-known/jwks.json"
|
17
17
|
end
|
18
18
|
|
19
19
|
def initialize(name = :oauth2_auth0, config = {})
|
20
|
-
super
|
21
|
-
|
20
|
+
super
|
21
|
+
|
22
22
|
@domain = config[:domain] || raise(ArgumentError, 'Auth0 domain is required')
|
23
23
|
@audience = config[:audience] || raise(ArgumentError, 'Auth0 audience is required')
|
24
24
|
@algorithm = config[:algorithm] || 'RS256'
|
25
25
|
@jwks_cache_ttl = config[:jwks_cache_ttl] || 300 # 5 minutes
|
26
26
|
@realm = config[:realm] || 'API'
|
27
|
-
|
27
|
+
|
28
28
|
# Cache for JWKS to avoid frequent requests
|
29
29
|
@jwks_cache = nil
|
30
30
|
@jwks_cache_time = nil
|
@@ -35,13 +35,9 @@ module RapiTapir
|
|
35
35
|
return nil unless auth_header
|
36
36
|
|
37
37
|
token = extract_bearer_token(auth_header)
|
38
|
-
if token.nil?
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
if token.strip.empty?
|
43
|
-
raise AuthenticationError, "Token cannot be empty"
|
44
|
-
end
|
38
|
+
raise AuthenticationError, 'Invalid authorization header format' if token.nil?
|
39
|
+
|
40
|
+
raise AuthenticationError, 'Token cannot be empty' if token.strip.empty?
|
45
41
|
|
46
42
|
begin
|
47
43
|
decoded_token = validate_auth0_token(token)
|
@@ -49,7 +45,7 @@ module RapiTapir
|
|
49
45
|
|
50
46
|
# Extract user info and scopes from the token
|
51
47
|
payload = decoded_token.first
|
52
|
-
|
48
|
+
|
53
49
|
create_context(
|
54
50
|
user: extract_user_from_token(payload),
|
55
51
|
scopes: extract_scopes_from_token(payload),
|
@@ -88,9 +84,9 @@ module RapiTapir
|
|
88
84
|
|
89
85
|
def validate_auth0_token(token)
|
90
86
|
require 'jwt' # Ensure JWT gem is available
|
91
|
-
|
87
|
+
|
92
88
|
jwks = fetch_jwks
|
93
|
-
|
89
|
+
|
94
90
|
JWT.decode(
|
95
91
|
token,
|
96
92
|
nil,
|
@@ -108,37 +104,33 @@ module RapiTapir
|
|
108
104
|
|
109
105
|
def fetch_jwks
|
110
106
|
# Return cached JWKS if still valid
|
111
|
-
if @jwks_cache && @jwks_cache_time && (Time.now - @jwks_cache_time) < @jwks_cache_ttl
|
112
|
-
return @jwks_cache
|
113
|
-
end
|
107
|
+
return @jwks_cache if @jwks_cache && @jwks_cache_time && (Time.now - @jwks_cache_time) < @jwks_cache_ttl
|
114
108
|
|
115
109
|
# Fetch fresh JWKS from Auth0
|
116
110
|
jwks_response = get_jwks_from_auth0
|
117
|
-
|
118
|
-
unless jwks_response.is_a?(Net::HTTPSuccess)
|
119
|
-
raise AuthenticationError, 'Unable to fetch JWKS from Auth0'
|
120
|
-
end
|
111
|
+
|
112
|
+
raise AuthenticationError, 'Unable to fetch JWKS from Auth0' unless jwks_response.is_a?(Net::HTTPSuccess)
|
121
113
|
|
122
114
|
jwks_data = JSON.parse(jwks_response.body)
|
123
|
-
|
115
|
+
|
124
116
|
# Cache the JWKS
|
125
117
|
@jwks_cache = { keys: jwks_data['keys'] }
|
126
118
|
@jwks_cache_time = Time.now
|
127
|
-
|
119
|
+
|
128
120
|
@jwks_cache
|
129
121
|
end
|
130
122
|
|
131
123
|
def get_jwks_from_auth0
|
132
124
|
jwks_uri = URI("#{base_domain_url}/.well-known/jwks.json")
|
133
|
-
|
125
|
+
|
134
126
|
http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
|
135
127
|
http.use_ssl = true
|
136
128
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
137
129
|
http.read_timeout = 10
|
138
|
-
|
130
|
+
|
139
131
|
request = Net::HTTP::Get.new(jwks_uri.request_uri)
|
140
132
|
request['User-Agent'] = 'RapiTapir OAuth2 Client'
|
141
|
-
|
133
|
+
|
142
134
|
http.request(request)
|
143
135
|
end
|
144
136
|
|
@@ -166,17 +158,17 @@ module RapiTapir
|
|
166
158
|
scope_string = payload['scope']
|
167
159
|
return [] unless scope_string
|
168
160
|
|
169
|
-
scope_string.split
|
161
|
+
scope_string.split
|
170
162
|
end
|
171
163
|
end
|
172
164
|
|
173
165
|
# Generic OAuth2 scheme with token introspection support
|
174
166
|
class GenericScheme < Schemes::Base
|
175
167
|
attr_reader :introspection_endpoint, :client_id, :client_secret, :token_cache_ttl
|
176
|
-
|
168
|
+
|
177
169
|
def initialize(name = :oauth2, config = {})
|
178
|
-
super
|
179
|
-
|
170
|
+
super
|
171
|
+
|
180
172
|
@introspection_endpoint = config[:introspection_endpoint]
|
181
173
|
@client_id = config[:client_id]
|
182
174
|
@client_secret = config[:client_secret]
|
@@ -184,7 +176,7 @@ module RapiTapir
|
|
184
176
|
@realm = config[:realm] || 'API'
|
185
177
|
@cache_tokens = config.fetch(:cache_tokens, true)
|
186
178
|
@token_cache_ttl = config[:token_cache_ttl] || 300 # 5 minutes
|
187
|
-
|
179
|
+
|
188
180
|
# Token cache to avoid repeated introspection calls
|
189
181
|
@token_cache = {} if @cache_tokens
|
190
182
|
end
|
@@ -198,11 +190,9 @@ module RapiTapir
|
|
198
190
|
|
199
191
|
begin
|
200
192
|
token_info = validate_oauth2_token(token)
|
201
|
-
|
193
|
+
|
202
194
|
# Check if token is active
|
203
|
-
unless token_info && token_info[:active]
|
204
|
-
raise AuthenticationError, "Token is not active"
|
205
|
-
end
|
195
|
+
raise AuthenticationError, 'Token is not active' unless token_info && token_info[:active]
|
206
196
|
|
207
197
|
create_context(
|
208
198
|
user: token_info[:user],
|
@@ -242,19 +232,17 @@ module RapiTapir
|
|
242
232
|
# Check cache first
|
243
233
|
if @cache_tokens && @token_cache
|
244
234
|
cached = @token_cache[token]
|
245
|
-
if cached && (Time.now - cached[:cached_at]) < @token_cache_ttl
|
246
|
-
return cached[:data]
|
247
|
-
end
|
235
|
+
return cached[:data] if cached && (Time.now - cached[:cached_at]) < @token_cache_ttl
|
248
236
|
end
|
249
237
|
|
250
238
|
# Validate token
|
251
239
|
token_info = if @introspection_endpoint
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
240
|
+
introspect_token_via_endpoint(token)
|
241
|
+
elsif @token_validator
|
242
|
+
@token_validator.call(token)
|
243
|
+
else
|
244
|
+
default_token_validation(token)
|
245
|
+
end
|
258
246
|
|
259
247
|
# Cache the result
|
260
248
|
if @cache_tokens && @token_cache && token_info
|
@@ -269,40 +257,38 @@ module RapiTapir
|
|
269
257
|
|
270
258
|
def introspect_token_via_endpoint(token)
|
271
259
|
uri = URI(@introspection_endpoint)
|
272
|
-
|
260
|
+
|
273
261
|
http = Net::HTTP.new(uri.host, uri.port)
|
274
262
|
http.use_ssl = (uri.scheme == 'https')
|
275
|
-
|
263
|
+
|
276
264
|
request = Net::HTTP::Post.new(uri.request_uri)
|
277
265
|
request['Content-Type'] = 'application/x-www-form-urlencoded'
|
278
266
|
request['Accept'] = 'application/json'
|
279
|
-
|
267
|
+
|
280
268
|
# Add client authentication
|
281
269
|
if @client_id && @client_secret
|
282
270
|
credentials = Base64.strict_encode64("#{@client_id}:#{@client_secret}")
|
283
271
|
request['Authorization'] = "Basic #{credentials}"
|
284
272
|
end
|
285
|
-
|
273
|
+
|
286
274
|
# Set form data
|
287
275
|
request.set_form_data(
|
288
276
|
'token' => token,
|
289
277
|
'token_type_hint' => 'access_token'
|
290
278
|
)
|
291
|
-
|
279
|
+
|
292
280
|
response = http.request(request)
|
293
|
-
|
294
|
-
unless response.is_a?(Net::HTTPSuccess)
|
295
|
-
|
296
|
-
end
|
297
|
-
|
281
|
+
|
282
|
+
raise AuthenticationError, "Token introspection failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
283
|
+
|
298
284
|
begin
|
299
285
|
introspection_data = JSON.parse(response.body)
|
300
286
|
rescue JSON::ParserError => e
|
301
287
|
raise AuthenticationError, "Invalid introspection response: #{e.message}"
|
302
288
|
end
|
303
|
-
|
289
|
+
|
304
290
|
return { active: false } unless introspection_data['active']
|
305
|
-
|
291
|
+
|
306
292
|
{
|
307
293
|
active: true,
|
308
294
|
user: extract_user_from_introspection(introspection_data),
|
@@ -324,7 +310,7 @@ module RapiTapir
|
|
324
310
|
|
325
311
|
def extract_scopes_from_introspection(data)
|
326
312
|
if data['scope'].is_a?(String)
|
327
|
-
data['scope'].split
|
313
|
+
data['scope'].split
|
328
314
|
elsif data['scope'].is_a?(Array)
|
329
315
|
data['scope']
|
330
316
|
else
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'fileutils'
|
5
|
+
require 'json'
|
5
6
|
|
6
7
|
module RapiTapir
|
7
8
|
module CLI
|
@@ -38,10 +39,14 @@ module RapiTapir
|
|
38
39
|
case command
|
39
40
|
when 'generate'
|
40
41
|
run_generate(@args)
|
42
|
+
when 'export'
|
43
|
+
run_export(@args)
|
41
44
|
when 'serve'
|
42
45
|
run_serve(@args)
|
43
46
|
when 'validate'
|
44
47
|
run_validate(@args)
|
48
|
+
when 'ai'
|
49
|
+
run_ai(@args)
|
45
50
|
when 'version'
|
46
51
|
puts "RapiTapir version #{RapiTapir::VERSION}"
|
47
52
|
when 'help', nil
|
@@ -153,7 +158,7 @@ module RapiTapir
|
|
153
158
|
type = args.shift
|
154
159
|
unless type
|
155
160
|
puts 'Error: Generate command requires a type'
|
156
|
-
puts 'Available types: openapi, client, docs'
|
161
|
+
puts 'Available types: openapi, client, docs, mcp'
|
157
162
|
exit 1
|
158
163
|
end
|
159
164
|
|
@@ -166,9 +171,11 @@ module RapiTapir
|
|
166
171
|
when 'docs'
|
167
172
|
docs_type = args.shift || 'html'
|
168
173
|
generate_docs(docs_type)
|
174
|
+
when 'mcp'
|
175
|
+
generate_mcp
|
169
176
|
else
|
170
177
|
puts "Error: Unknown generation type: #{type}"
|
171
|
-
puts 'Available types: openapi, client, docs'
|
178
|
+
puts 'Available types: openapi, client, docs, mcp'
|
172
179
|
exit 1
|
173
180
|
end
|
174
181
|
end
|
@@ -206,6 +213,24 @@ module RapiTapir
|
|
206
213
|
end
|
207
214
|
end
|
208
215
|
|
216
|
+
def run_export(args)
|
217
|
+
type = args.shift
|
218
|
+
unless type
|
219
|
+
puts 'Error: Export command requires a type'
|
220
|
+
puts 'Available types: mcp'
|
221
|
+
exit 1
|
222
|
+
end
|
223
|
+
|
224
|
+
case type
|
225
|
+
when 'mcp'
|
226
|
+
export_mcp
|
227
|
+
else
|
228
|
+
puts "Error: Unknown export type: #{type}"
|
229
|
+
puts 'Available types: mcp'
|
230
|
+
exit 1
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
209
234
|
def generate_openapi
|
210
235
|
validate_openapi_options
|
211
236
|
|
@@ -412,6 +437,33 @@ module RapiTapir
|
|
412
437
|
end
|
413
438
|
end
|
414
439
|
|
440
|
+
def generate_mcp
|
441
|
+
validate_mcp_options
|
442
|
+
|
443
|
+
endpoints = load_endpoints(@options[:input])
|
444
|
+
require_relative '../ai/mcp'
|
445
|
+
exporter = RapiTapir::AI::MCP::Exporter.new(endpoints)
|
446
|
+
content = JSON.pretty_generate(exporter.as_mcp_context)
|
447
|
+
save_mcp_output(content)
|
448
|
+
end
|
449
|
+
|
450
|
+
def export_mcp
|
451
|
+
generate_mcp
|
452
|
+
end
|
453
|
+
|
454
|
+
def validate_mcp_options
|
455
|
+
return if @options[:input]
|
456
|
+
|
457
|
+
puts 'Error: --endpoints is required'
|
458
|
+
exit 1
|
459
|
+
end
|
460
|
+
|
461
|
+
def save_mcp_output(content)
|
462
|
+
output_file = @options[:output] || 'mcp-context.json'
|
463
|
+
File.write(output_file, content)
|
464
|
+
puts "MCP context exported to #{output_file}"
|
465
|
+
end
|
466
|
+
|
415
467
|
def load_endpoints(file_path)
|
416
468
|
raise "Input file not found: #{file_path}" unless File.exist?(file_path)
|
417
469
|
|
@@ -530,6 +582,314 @@ module RapiTapir
|
|
530
582
|
|
531
583
|
endpoints
|
532
584
|
end
|
585
|
+
|
586
|
+
def run_ai(args)
|
587
|
+
subcommand = args.shift
|
588
|
+
|
589
|
+
case subcommand
|
590
|
+
when 'rag'
|
591
|
+
run_ai_rag(args)
|
592
|
+
when 'query'
|
593
|
+
run_ai_query(args)
|
594
|
+
when 'llm'
|
595
|
+
run_ai_llm(args)
|
596
|
+
when 'help', nil
|
597
|
+
puts ai_help
|
598
|
+
else
|
599
|
+
puts "Unknown AI subcommand: #{subcommand}"
|
600
|
+
puts ai_help
|
601
|
+
exit 1
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
def run_ai_rag(args)
|
606
|
+
command = args.shift
|
607
|
+
|
608
|
+
case command
|
609
|
+
when 'test'
|
610
|
+
run_rag_test(args)
|
611
|
+
when 'setup'
|
612
|
+
run_rag_setup(args)
|
613
|
+
else
|
614
|
+
puts 'RAG subcommands: test, setup'
|
615
|
+
exit 1
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
def run_rag_test(args)
|
620
|
+
unless @options[:input]
|
621
|
+
puts 'Error: --endpoints is required'
|
622
|
+
exit 1
|
623
|
+
end
|
624
|
+
|
625
|
+
endpoints = load_endpoints(@options[:input])
|
626
|
+
|
627
|
+
# Find RAG-enabled endpoints
|
628
|
+
rag_endpoints = endpoints.select(&:rag_inference?)
|
629
|
+
|
630
|
+
if rag_endpoints.empty?
|
631
|
+
puts "No RAG-enabled endpoints found in #{@options[:input]}"
|
632
|
+
puts 'Add .rag_inference to your endpoints to enable RAG capabilities'
|
633
|
+
exit 1
|
634
|
+
end
|
635
|
+
|
636
|
+
require_relative '../ai/rag'
|
637
|
+
|
638
|
+
puts 'Testing RAG functionality...'
|
639
|
+
puts "Found #{rag_endpoints.size} RAG-enabled endpoint(s):"
|
640
|
+
|
641
|
+
rag_endpoints.each do |endpoint|
|
642
|
+
puts " #{endpoint.method} #{endpoint.path}"
|
643
|
+
config = endpoint.rag_config
|
644
|
+
|
645
|
+
# Create pipeline and test
|
646
|
+
pipeline = RapiTapir::AI::RAG::Pipeline.new(
|
647
|
+
llm: config[:llm] || :openai,
|
648
|
+
retrieval: config[:retrieval] || :memory,
|
649
|
+
config: config[:config] || {}
|
650
|
+
)
|
651
|
+
|
652
|
+
test_query = args.first || 'What can this API do?'
|
653
|
+
result = pipeline.process(test_query)
|
654
|
+
|
655
|
+
puts " Query: #{test_query}"
|
656
|
+
puts " Answer: #{result[:answer][0..100]}#{'...' if result[:answer].length > 100}"
|
657
|
+
puts " Sources: #{result[:sources].size} document(s)"
|
658
|
+
puts
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
def run_rag_setup(_args)
|
663
|
+
puts 'Setting up RAG configuration...'
|
664
|
+
|
665
|
+
config_template = {
|
666
|
+
rag: {
|
667
|
+
llm: {
|
668
|
+
provider: 'openai',
|
669
|
+
api_key: 'your-openai-api-key'
|
670
|
+
},
|
671
|
+
retrieval: {
|
672
|
+
backend: 'memory',
|
673
|
+
documents_path: './docs/**/*.md'
|
674
|
+
}
|
675
|
+
}
|
676
|
+
}
|
677
|
+
|
678
|
+
config_file = 'rapitapir_ai.json'
|
679
|
+
|
680
|
+
if File.exist?(config_file)
|
681
|
+
puts "Configuration file #{config_file} already exists"
|
682
|
+
exit 1
|
683
|
+
end
|
684
|
+
|
685
|
+
File.write(config_file, JSON.pretty_generate(config_template))
|
686
|
+
puts "Created #{config_file}"
|
687
|
+
puts 'Please update the configuration with your API keys and document paths'
|
688
|
+
end
|
689
|
+
|
690
|
+
def run_ai_query(args)
|
691
|
+
query = args.join(' ')
|
692
|
+
|
693
|
+
if query.empty?
|
694
|
+
puts 'Usage: rapitapir ai query <your question>'
|
695
|
+
exit 1
|
696
|
+
end
|
697
|
+
|
698
|
+
unless @options[:input]
|
699
|
+
puts 'Error: --endpoints is required'
|
700
|
+
exit 1
|
701
|
+
end
|
702
|
+
|
703
|
+
endpoints = load_endpoints(@options[:input])
|
704
|
+
|
705
|
+
# Export MCP context for the query
|
706
|
+
require_relative '../ai/mcp'
|
707
|
+
exporter = RapiTapir::AI::MCP::Exporter.new(endpoints)
|
708
|
+
context = exporter.as_mcp_context
|
709
|
+
|
710
|
+
puts 'API Context for AI Agents:'
|
711
|
+
puts '========================='
|
712
|
+
puts
|
713
|
+
puts "Query: #{query}"
|
714
|
+
puts
|
715
|
+
puts 'Available Endpoints:'
|
716
|
+
if context && context[:endpoints]
|
717
|
+
context[:endpoints].each do |ep|
|
718
|
+
puts " #{ep[:method]} #{ep[:path]} - #{ep[:summary]}"
|
719
|
+
end
|
720
|
+
else
|
721
|
+
puts ' No endpoints found'
|
722
|
+
end
|
723
|
+
puts
|
724
|
+
puts 'This context can be used by AI agents to understand your API structure.'
|
725
|
+
end
|
726
|
+
|
727
|
+
def run_ai_llm(args)
|
728
|
+
command = args.shift
|
729
|
+
|
730
|
+
case command
|
731
|
+
when 'generate'
|
732
|
+
run_llm_generate(args)
|
733
|
+
when 'export'
|
734
|
+
run_llm_export(args)
|
735
|
+
when 'test'
|
736
|
+
run_llm_test(args)
|
737
|
+
else
|
738
|
+
puts 'LLM subcommands: generate, export, test'
|
739
|
+
exit 1
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
def run_llm_generate(_args)
|
744
|
+
unless @options[:input]
|
745
|
+
puts 'Error: --endpoints is required'
|
746
|
+
exit 1
|
747
|
+
end
|
748
|
+
|
749
|
+
endpoints = load_endpoints(@options[:input])
|
750
|
+
|
751
|
+
# Find LLM instruction-enabled endpoints
|
752
|
+
llm_endpoints = endpoints.select(&:llm_instruction?)
|
753
|
+
|
754
|
+
if llm_endpoints.empty?
|
755
|
+
puts "No LLM instruction-enabled endpoints found in #{@options[:input]}"
|
756
|
+
puts 'Add .llm_instruction(purpose: :validation) to your endpoints to enable LLM instruction generation'
|
757
|
+
exit 1
|
758
|
+
end
|
759
|
+
|
760
|
+
require_relative '../ai/llm_instruction'
|
761
|
+
|
762
|
+
puts 'Generating LLM instructions...'
|
763
|
+
puts "Found #{llm_endpoints.size} LLM instruction-enabled endpoint(s):"
|
764
|
+
|
765
|
+
generator = RapiTapir::AI::LLMInstruction::Generator.new(llm_endpoints)
|
766
|
+
instructions = generator.generate_all_instructions
|
767
|
+
|
768
|
+
instructions[:instructions].each do |instruction|
|
769
|
+
puts "\n#{'=' * 60}"
|
770
|
+
puts "#{instruction[:method]} #{instruction[:path]} (#{instruction[:purpose]})"
|
771
|
+
puts '=' * 60
|
772
|
+
puts instruction[:instruction]
|
773
|
+
end
|
774
|
+
|
775
|
+
puts "\n#{'=' * 60}"
|
776
|
+
puts "Generated #{instructions[:instructions].size} LLM instructions"
|
777
|
+
end
|
778
|
+
|
779
|
+
def run_llm_export(args)
|
780
|
+
format = args.shift || 'json'
|
781
|
+
output_file = @options[:output]
|
782
|
+
|
783
|
+
unless @options[:input]
|
784
|
+
puts 'Error: --endpoints is required'
|
785
|
+
exit 1
|
786
|
+
end
|
787
|
+
|
788
|
+
endpoints = load_endpoints(@options[:input])
|
789
|
+
llm_endpoints = endpoints.select(&:llm_instruction?)
|
790
|
+
|
791
|
+
if llm_endpoints.empty?
|
792
|
+
puts 'No LLM instruction-enabled endpoints found'
|
793
|
+
exit 1
|
794
|
+
end
|
795
|
+
|
796
|
+
require_relative '../ai/llm_instruction'
|
797
|
+
|
798
|
+
generator = RapiTapir::AI::LLMInstruction::Generator.new(llm_endpoints)
|
799
|
+
instructions = generator.generate_all_instructions
|
800
|
+
exporter = RapiTapir::AI::LLMInstruction::Exporter.new(instructions)
|
801
|
+
|
802
|
+
case format.downcase
|
803
|
+
when 'json'
|
804
|
+
output = exporter.to_json
|
805
|
+
extension = '.json'
|
806
|
+
when 'yaml', 'yml'
|
807
|
+
output = exporter.to_yaml
|
808
|
+
extension = '.yml'
|
809
|
+
when 'markdown', 'md'
|
810
|
+
output = exporter.to_markdown
|
811
|
+
extension = '.md'
|
812
|
+
when 'prompts'
|
813
|
+
if output_file
|
814
|
+
puts exporter.to_prompt_files(output_file)
|
815
|
+
return
|
816
|
+
else
|
817
|
+
puts 'Error: --output directory is required for prompts format'
|
818
|
+
exit 1
|
819
|
+
end
|
820
|
+
else
|
821
|
+
puts "Error: Unsupported format '#{format}'. Supported: json, yaml, markdown, prompts"
|
822
|
+
exit 1
|
823
|
+
end
|
824
|
+
|
825
|
+
if output_file
|
826
|
+
output_file += extension unless output_file.end_with?(extension)
|
827
|
+
File.write(output_file, output)
|
828
|
+
puts "LLM instructions exported to #{output_file}"
|
829
|
+
else
|
830
|
+
puts output
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
def run_llm_test(args)
|
835
|
+
purpose = args.shift
|
836
|
+
|
837
|
+
unless purpose
|
838
|
+
puts 'Usage: rapitapir ai llm test <purpose>'
|
839
|
+
puts 'Available purposes: validation, transformation, analysis, documentation, testing, completion'
|
840
|
+
exit 1
|
841
|
+
end
|
842
|
+
|
843
|
+
unless @options[:input]
|
844
|
+
puts 'Error: --endpoints is required'
|
845
|
+
exit 1
|
846
|
+
end
|
847
|
+
|
848
|
+
endpoints = load_endpoints(@options[:input])
|
849
|
+
llm_endpoints = endpoints.select(&:llm_instruction?)
|
850
|
+
|
851
|
+
if llm_endpoints.empty?
|
852
|
+
puts 'No LLM instruction-enabled endpoints found'
|
853
|
+
exit 1
|
854
|
+
end
|
855
|
+
|
856
|
+
require_relative '../ai/llm_instruction'
|
857
|
+
|
858
|
+
puts "Testing LLM instruction generation for purpose: #{purpose}"
|
859
|
+
puts
|
860
|
+
|
861
|
+
generator = RapiTapir::AI::LLMInstruction::Generator.new(llm_endpoints)
|
862
|
+
|
863
|
+
llm_endpoints.each do |endpoint|
|
864
|
+
config = endpoint.llm_instruction_config.dup
|
865
|
+
config[:purpose] = purpose.to_sym
|
866
|
+
|
867
|
+
begin
|
868
|
+
instruction = generator.generate_instruction(endpoint, config)
|
869
|
+
puts "✅ #{endpoint.method&.upcase} #{endpoint.path}"
|
870
|
+
puts " Purpose: #{instruction[:purpose]}"
|
871
|
+
puts " Length: #{instruction[:instruction].length} characters"
|
872
|
+
puts
|
873
|
+
rescue StandardError => e
|
874
|
+
puts "❌ #{endpoint.method&.upcase} #{endpoint.path}"
|
875
|
+
puts " Error: #{e.message}"
|
876
|
+
puts
|
877
|
+
end
|
878
|
+
end
|
879
|
+
end
|
880
|
+
|
881
|
+
def ai_help
|
882
|
+
<<~HELP
|
883
|
+
AI Commands:
|
884
|
+
rapitapir ai rag test [query] - Test RAG functionality on your API
|
885
|
+
rapitapir ai rag setup - Create RAG configuration template
|
886
|
+
rapitapir ai query <question> - Get AI-ready context for a query
|
887
|
+
rapitapir ai llm generate - Generate LLM instructions from endpoints
|
888
|
+
rapitapir ai llm export [format] - Export LLM instructions (json, yaml, markdown, prompts)
|
889
|
+
rapitapir ai llm test <purpose> - Test LLM instruction generation for a purpose
|
890
|
+
rapitapir ai help - Show this help
|
891
|
+
HELP
|
892
|
+
end
|
533
893
|
end
|
534
894
|
end
|
535
895
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../core/endpoint'
|
4
|
+
require_relative '../ai/mcp'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module RapiTapir
|
8
|
+
module CLI
|
9
|
+
class MCPExport
|
10
|
+
# Exports all endpoints marked for MCP as a JSON file
|
11
|
+
def self.run(endpoints, output_path = 'mcp-context.json')
|
12
|
+
exporter = RapiTapir::AI::MCP::Exporter.new(endpoints)
|
13
|
+
File.write(output_path, JSON.pretty_generate(exporter.as_mcp_context))
|
14
|
+
puts "MCP context exported to #{output_path}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -112,9 +112,7 @@ module RapiTapir
|
|
112
112
|
# Check path parameter format
|
113
113
|
path.scan(/:(\w+)/).each do |param_match|
|
114
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
|
115
|
+
@errors << "#{context}: Invalid path parameter name '#{param_name}'" unless param_name.match?(/^[a-zA-Z][a-zA-Z0-9_]*$/)
|
118
116
|
end
|
119
117
|
end
|
120
118
|
|
@@ -219,9 +217,7 @@ module RapiTapir
|
|
219
217
|
|
220
218
|
def validate_hash_schema(schema, context)
|
221
219
|
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
|
220
|
+
@errors << "#{context}: Hash key '#{key}' must be a symbol or string" unless key.is_a?(Symbol) || key.is_a?(String)
|
225
221
|
|
226
222
|
validate_type(value, "#{context}.#{key}")
|
227
223
|
end
|