fraiseql_server/routes/api/openapi.rs
1//! `OpenAPI` specification for FraiseQL REST APIs.
2//!
3//! Provides a static `OpenAPI` 3.0.0 specification documenting all API endpoints,
4//! request/response schemas, and authentication requirements.
5
6/// Get complete `OpenAPI` 3.0.0 specification as JSON string.
7pub fn get_openapi_spec() -> String {
8 serde_json::json!({
9 "openapi": "3.0.0",
10 "info": {
11 "title": "FraiseQL Agent APIs",
12 "description": "GraphQL query intelligence, federation discovery, and administration APIs for FraiseQL",
13 "version": "1.0.0",
14 "contact": {
15 "name": "FraiseQL Support",
16 "url": "https://github.com/fraiseql/fraiseql"
17 },
18 "license": {
19 "name": "MIT OR Apache-2.0"
20 }
21 },
22 "servers": [
23 {
24 "url": "http://localhost:8080",
25 "description": "Local development server"
26 },
27 {
28 "url": "https://api.fraiseql.example.com",
29 "description": "Production server"
30 }
31 ],
32 "paths": {
33 "/api/v1/query/explain": {
34 "post": {
35 "summary": "Analyze GraphQL query complexity",
36 "description": "Analyzes a GraphQL query for depth, field count, and estimated execution cost. Returns complexity metrics and optimization recommendations.",
37 "tags": ["Query Intelligence"],
38 "requestBody": {
39 "required": true,
40 "content": {
41 "application/json": {
42 "schema": {
43 "$ref": "#/components/schemas/ExplainRequest"
44 }
45 }
46 }
47 },
48 "responses": {
49 "200": {
50 "description": "Query analysis successful",
51 "content": {
52 "application/json": {
53 "schema": {
54 "$ref": "#/components/schemas/ApiResponseExplain"
55 }
56 }
57 }
58 },
59 "400": {
60 "description": "Invalid query or validation error"
61 }
62 }
63 }
64 },
65 "/api/v1/query/validate": {
66 "post": {
67 "summary": "Validate GraphQL query syntax",
68 "description": "Validates GraphQL query syntax without executing analysis. Fast validation for batch operations.",
69 "tags": ["Query Intelligence"],
70 "requestBody": {
71 "required": true,
72 "content": {
73 "application/json": {
74 "schema": {
75 "$ref": "#/components/schemas/ValidateRequest"
76 }
77 }
78 }
79 },
80 "responses": {
81 "200": {
82 "description": "Query validation result",
83 "content": {
84 "application/json": {
85 "schema": {
86 "$ref": "#/components/schemas/ApiResponseValidate"
87 }
88 }
89 }
90 }
91 }
92 }
93 },
94 "/api/v1/query/stats": {
95 "get": {
96 "summary": "Get query performance statistics",
97 "description": "Retrieves historical performance metrics for queries. Requires metrics collection to be enabled.",
98 "tags": ["Query Intelligence"],
99 "responses": {
100 "200": {
101 "description": "Query statistics",
102 "content": {
103 "application/json": {
104 "schema": {
105 "$ref": "#/components/schemas/ApiResponseStats"
106 }
107 }
108 }
109 }
110 }
111 }
112 },
113 "/api/v1/federation/subgraphs": {
114 "get": {
115 "summary": "List federation subgraphs",
116 "description": "Returns all federated subgraphs with their URLs, managed entities, and health status.",
117 "tags": ["Federation"],
118 "responses": {
119 "200": {
120 "description": "List of subgraphs",
121 "content": {
122 "application/json": {
123 "schema": {
124 "$ref": "#/components/schemas/ApiResponseSubgraphs"
125 }
126 }
127 }
128 }
129 }
130 }
131 },
132 "/api/v1/federation/graph": {
133 "get": {
134 "summary": "Export federation dependency graph",
135 "description": "Exports the federation structure showing subgraph relationships and entity resolution paths. Supports multiple output formats.",
136 "tags": ["Federation"],
137 "parameters": [
138 {
139 "name": "format",
140 "in": "query",
141 "description": "Output format: json (default), dot (Graphviz), or mermaid",
142 "schema": {
143 "type": "string",
144 "enum": ["json", "dot", "mermaid"],
145 "default": "json"
146 }
147 }
148 ],
149 "responses": {
150 "200": {
151 "description": "Federation graph in requested format",
152 "content": {
153 "application/json": {
154 "schema": {
155 "$ref": "#/components/schemas/ApiResponseGraph"
156 }
157 }
158 }
159 },
160 "400": {
161 "description": "Invalid format parameter"
162 }
163 }
164 }
165 },
166 "/api/v1/schema.graphql": {
167 "get": {
168 "summary": "Export schema as GraphQL SDL",
169 "description": "Exports the compiled schema in GraphQL Schema Definition Language (SDL) format. Returns text/plain response.",
170 "tags": ["Schema"],
171 "responses": {
172 "200": {
173 "description": "Schema in SDL format",
174 "content": {
175 "text/plain": {
176 "schema": {
177 "type": "string",
178 "example": "type Query { users: [User!]! }\ntype User { id: ID! name: String! }"
179 }
180 }
181 }
182 }
183 }
184 }
185 },
186 "/api/v1/schema.json": {
187 "get": {
188 "summary": "Export schema as JSON",
189 "description": "Exports the full compiled schema in JSON format with type information and metadata.",
190 "tags": ["Schema"],
191 "responses": {
192 "200": {
193 "description": "Schema as JSON",
194 "content": {
195 "application/json": {
196 "schema": {
197 "$ref": "#/components/schemas/ApiResponseSchemaJson"
198 }
199 }
200 }
201 }
202 }
203 }
204 },
205 "/api/v1/admin/reload-schema": {
206 "post": {
207 "summary": "Hot reload schema",
208 "description": "Reload schema from file without restarting the server. Supports validation-only mode.",
209 "tags": ["Admin"],
210 "security": [
211 {
212 "BearerAuth": []
213 }
214 ],
215 "requestBody": {
216 "required": true,
217 "content": {
218 "application/json": {
219 "schema": {
220 "$ref": "#/components/schemas/ReloadSchemaRequest"
221 }
222 }
223 }
224 },
225 "responses": {
226 "200": {
227 "description": "Schema reload result",
228 "content": {
229 "application/json": {
230 "schema": {
231 "$ref": "#/components/schemas/ApiResponseReloadSchema"
232 }
233 }
234 }
235 },
236 "401": {
237 "description": "Unauthorized - admin token required"
238 },
239 "400": {
240 "description": "Invalid schema or validation error"
241 }
242 }
243 }
244 },
245 "/api/v1/admin/cache/clear": {
246 "post": {
247 "summary": "Clear cache entries",
248 "description": "Invalidate cache by scope: all (clear everything), entity (by type), or pattern (by glob).",
249 "tags": ["Admin"],
250 "security": [
251 {
252 "BearerAuth": []
253 }
254 ],
255 "requestBody": {
256 "required": true,
257 "content": {
258 "application/json": {
259 "schema": {
260 "$ref": "#/components/schemas/CacheClearRequest"
261 }
262 }
263 }
264 },
265 "responses": {
266 "200": {
267 "description": "Cache clear result",
268 "content": {
269 "application/json": {
270 "schema": {
271 "$ref": "#/components/schemas/ApiResponseCacheClear"
272 }
273 }
274 }
275 },
276 "401": {
277 "description": "Unauthorized - admin token required"
278 }
279 }
280 }
281 },
282 "/api/v1/admin/config": {
283 "get": {
284 "summary": "Get runtime configuration",
285 "description": "Returns sanitized runtime configuration (secrets excluded). Requires admin token.",
286 "tags": ["Admin"],
287 "security": [
288 {
289 "BearerAuth": []
290 }
291 ],
292 "responses": {
293 "200": {
294 "description": "Runtime configuration",
295 "content": {
296 "application/json": {
297 "schema": {
298 "$ref": "#/components/schemas/ApiResponseConfig"
299 }
300 }
301 }
302 },
303 "401": {
304 "description": "Unauthorized - admin token required"
305 }
306 }
307 }
308 }
309 },
310 "components": {
311 "securitySchemes": {
312 "BearerAuth": {
313 "type": "http",
314 "scheme": "bearer",
315 "description": "Bearer token for admin endpoints"
316 }
317 },
318 "schemas": {
319 "ExplainRequest": {
320 "type": "object",
321 "required": ["query"],
322 "properties": {
323 "query": {
324 "type": "string",
325 "description": "GraphQL query to analyze",
326 "example": "query { users { id name } }"
327 }
328 }
329 },
330 "ComplexityInfo": {
331 "type": "object",
332 "properties": {
333 "depth": {
334 "type": "integer",
335 "description": "Query nesting depth",
336 "example": 2
337 },
338 "field_count": {
339 "type": "integer",
340 "description": "Total fields requested",
341 "example": 10
342 },
343 "score": {
344 "type": "integer",
345 "description": "Complexity score (depth × field_count)",
346 "example": 45
347 }
348 }
349 },
350 "ExplainResponse": {
351 "type": "object",
352 "properties": {
353 "query": {
354 "type": "string"
355 },
356 "sql": {
357 "type": "string",
358 "nullable": true,
359 "description": "Generated SQL execution plan"
360 },
361 "estimated_cost": {
362 "type": "integer"
363 },
364 "complexity": {
365 "$ref": "#/components/schemas/ComplexityInfo"
366 },
367 "warnings": {
368 "type": "array",
369 "items": {
370 "type": "string"
371 }
372 }
373 }
374 },
375 "ValidateRequest": {
376 "type": "object",
377 "required": ["query"],
378 "properties": {
379 "query": {
380 "type": "string",
381 "description": "GraphQL query to validate"
382 }
383 }
384 },
385 "ValidateResponse": {
386 "type": "object",
387 "properties": {
388 "valid": {
389 "type": "boolean"
390 },
391 "errors": {
392 "type": "array",
393 "items": {
394 "type": "string"
395 }
396 }
397 }
398 },
399 "StatsResponse": {
400 "type": "object",
401 "properties": {
402 "query_count": {
403 "type": "integer"
404 },
405 "avg_latency_ms": {
406 "type": "number"
407 }
408 }
409 },
410 "SubgraphInfo": {
411 "type": "object",
412 "properties": {
413 "name": {
414 "type": "string",
415 "example": "users"
416 },
417 "url": {
418 "type": "string",
419 "example": "http://users.local/graphql"
420 },
421 "entities": {
422 "type": "array",
423 "items": {
424 "type": "string"
425 }
426 },
427 "healthy": {
428 "type": "boolean"
429 }
430 }
431 },
432 "SubgraphsResponse": {
433 "type": "object",
434 "properties": {
435 "subgraphs": {
436 "type": "array",
437 "items": {
438 "$ref": "#/components/schemas/SubgraphInfo"
439 }
440 }
441 }
442 },
443 "GraphResponse": {
444 "type": "object",
445 "properties": {
446 "format": {
447 "type": "string",
448 "enum": ["json", "dot", "mermaid"]
449 },
450 "content": {
451 "type": "string",
452 "description": "Graph in requested format"
453 }
454 }
455 },
456 "JsonSchemaResponse": {
457 "type": "object",
458 "properties": {
459 "schema": {
460 "type": "object",
461 "description": "Compiled schema as JSON"
462 }
463 }
464 },
465 "ReloadSchemaRequest": {
466 "type": "object",
467 "required": ["schema_path"],
468 "properties": {
469 "schema_path": {
470 "type": "string",
471 "description": "Path to compiled schema file",
472 "example": "/path/to/schema.compiled.json"
473 },
474 "validate_only": {
475 "type": "boolean",
476 "description": "If true, only validate without applying",
477 "default": false
478 }
479 }
480 },
481 "ReloadSchemaResponse": {
482 "type": "object",
483 "properties": {
484 "success": {
485 "type": "boolean"
486 },
487 "message": {
488 "type": "string"
489 }
490 }
491 },
492 "CacheClearRequest": {
493 "type": "object",
494 "required": ["scope"],
495 "properties": {
496 "scope": {
497 "type": "string",
498 "enum": ["all", "entity", "pattern"],
499 "description": "Scope for cache clearing"
500 },
501 "entity_type": {
502 "type": "string",
503 "nullable": true,
504 "description": "Required if scope is 'entity'"
505 },
506 "pattern": {
507 "type": "string",
508 "nullable": true,
509 "description": "Required if scope is 'pattern'"
510 }
511 }
512 },
513 "CacheClearResponse": {
514 "type": "object",
515 "properties": {
516 "success": {
517 "type": "boolean"
518 },
519 "entries_cleared": {
520 "type": "integer"
521 },
522 "message": {
523 "type": "string"
524 }
525 }
526 },
527 "AdminConfigResponse": {
528 "type": "object",
529 "properties": {
530 "version": {
531 "type": "string",
532 "example": "2.0.0-a1"
533 },
534 "config": {
535 "type": "object",
536 "description": "Sanitized configuration (no secrets)",
537 "additionalProperties": {
538 "type": "string"
539 }
540 }
541 }
542 },
543 "ApiResponse": {
544 "type": "object",
545 "properties": {
546 "status": {
547 "type": "string",
548 "example": "success"
549 },
550 "data": {
551 "type": "object"
552 }
553 }
554 },
555 "ApiResponseExplain": {
556 "allOf": [
557 {
558 "$ref": "#/components/schemas/ApiResponse"
559 },
560 {
561 "type": "object",
562 "properties": {
563 "data": {
564 "$ref": "#/components/schemas/ExplainResponse"
565 }
566 }
567 }
568 ]
569 },
570 "ApiResponseValidate": {
571 "allOf": [
572 {
573 "$ref": "#/components/schemas/ApiResponse"
574 },
575 {
576 "type": "object",
577 "properties": {
578 "data": {
579 "$ref": "#/components/schemas/ValidateResponse"
580 }
581 }
582 }
583 ]
584 },
585 "ApiResponseStats": {
586 "allOf": [
587 {
588 "$ref": "#/components/schemas/ApiResponse"
589 },
590 {
591 "type": "object",
592 "properties": {
593 "data": {
594 "$ref": "#/components/schemas/StatsResponse"
595 }
596 }
597 }
598 ]
599 },
600 "ApiResponseSubgraphs": {
601 "allOf": [
602 {
603 "$ref": "#/components/schemas/ApiResponse"
604 },
605 {
606 "type": "object",
607 "properties": {
608 "data": {
609 "$ref": "#/components/schemas/SubgraphsResponse"
610 }
611 }
612 }
613 ]
614 },
615 "ApiResponseGraph": {
616 "allOf": [
617 {
618 "$ref": "#/components/schemas/ApiResponse"
619 },
620 {
621 "type": "object",
622 "properties": {
623 "data": {
624 "$ref": "#/components/schemas/GraphResponse"
625 }
626 }
627 }
628 ]
629 },
630 "ApiResponseSchemaJson": {
631 "allOf": [
632 {
633 "$ref": "#/components/schemas/ApiResponse"
634 },
635 {
636 "type": "object",
637 "properties": {
638 "data": {
639 "$ref": "#/components/schemas/JsonSchemaResponse"
640 }
641 }
642 }
643 ]
644 },
645 "ApiResponseReloadSchema": {
646 "allOf": [
647 {
648 "$ref": "#/components/schemas/ApiResponse"
649 },
650 {
651 "type": "object",
652 "properties": {
653 "data": {
654 "$ref": "#/components/schemas/ReloadSchemaResponse"
655 }
656 }
657 }
658 ]
659 },
660 "ApiResponseCacheClear": {
661 "allOf": [
662 {
663 "$ref": "#/components/schemas/ApiResponse"
664 },
665 {
666 "type": "object",
667 "properties": {
668 "data": {
669 "$ref": "#/components/schemas/CacheClearResponse"
670 }
671 }
672 }
673 ]
674 },
675 "ApiResponseConfig": {
676 "allOf": [
677 {
678 "$ref": "#/components/schemas/ApiResponse"
679 },
680 {
681 "type": "object",
682 "properties": {
683 "data": {
684 "$ref": "#/components/schemas/AdminConfigResponse"
685 }
686 }
687 }
688 ]
689 }
690 }
691 }
692 }).to_string()
693}
694
695#[cfg(test)]
696mod tests {
697 #![allow(clippy::unwrap_used)] // Reason: test code, panics acceptable
698 #![allow(clippy::cast_precision_loss)] // Reason: test metrics reporting
699 #![allow(clippy::cast_sign_loss)] // Reason: test data uses small positive integers
700 #![allow(clippy::cast_possible_truncation)] // Reason: test data values are bounded
701 #![allow(clippy::cast_possible_wrap)] // Reason: test data values are bounded
702 #![allow(clippy::missing_panics_doc)] // Reason: test helpers
703 #![allow(clippy::missing_errors_doc)] // Reason: test helpers
704 #![allow(missing_docs)] // Reason: test code
705 #![allow(clippy::items_after_statements)] // Reason: test helpers defined near use site
706
707 use super::*;
708
709 #[test]
710 fn test_openapi_spec_parses_as_json() {
711 let spec = get_openapi_spec();
712 let parsed: serde_json::Value = serde_json::from_str(&spec)
713 .expect("OpenAPI spec is valid JSON: generated by crate at compile-time");
714 assert!(parsed.is_object());
715 }
716
717 #[test]
718 fn test_openapi_spec_has_all_required_fields() {
719 let spec = get_openapi_spec();
720 let parsed: serde_json::Value = serde_json::from_str(&spec).unwrap();
721
722 assert!(parsed.get("openapi").is_some(), "OpenAPI spec must contain 'openapi' key");
723 assert!(parsed.get("info").is_some(), "OpenAPI spec must contain 'info' key");
724 assert!(parsed.get("paths").is_some(), "OpenAPI spec must contain 'paths' key");
725 assert!(parsed.get("components").is_some(), "OpenAPI spec must contain 'components' key");
726 }
727
728 #[test]
729 fn test_openapi_spec_version() {
730 let spec = get_openapi_spec();
731 let parsed: serde_json::Value = serde_json::from_str(&spec).unwrap();
732
733 assert_eq!(parsed["openapi"].as_str(), Some("3.0.0"), "Should be OpenAPI 3.0.0");
734 }
735
736 #[test]
737 fn test_openapi_spec_documents_10_endpoints() {
738 let spec = get_openapi_spec();
739 let parsed: serde_json::Value = serde_json::from_str(&spec).unwrap();
740
741 let paths = &parsed["paths"];
742 let count = paths.as_object().map_or(0, |m| m.len());
743
744 assert_eq!(count, 10, "Should document all 10 API endpoint paths");
745 }
746
747 #[test]
748 fn test_openapi_has_security_schemes() {
749 let spec = get_openapi_spec();
750 let parsed: serde_json::Value = serde_json::from_str(&spec).unwrap();
751
752 let schemes = &parsed["components"]["securitySchemes"];
753 assert!(
754 schemes.get("BearerAuth").is_some(),
755 "security schemes must include 'BearerAuth'"
756 );
757 }
758
759 #[test]
760 fn test_openapi_has_component_schemas() {
761 let spec = get_openapi_spec();
762 let parsed: serde_json::Value = serde_json::from_str(&spec).unwrap();
763
764 let schemas = &parsed["components"]["schemas"];
765 assert!(
766 schemas.get("ExplainRequest").is_some(),
767 "component schemas must include 'ExplainRequest'"
768 );
769 assert!(
770 schemas.get("ExplainResponse").is_some(),
771 "component schemas must include 'ExplainResponse'"
772 );
773 assert!(
774 schemas.get("ReloadSchemaRequest").is_some(),
775 "component schemas must include 'ReloadSchemaRequest'"
776 );
777 }
778}