Skip to main content

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}