Skip to main content

ranvier_openapi/
lib.rs

1use std::collections::{BTreeMap, HashMap};
2
3use http::Method;
4use ranvier_core::Schematic;
5use ranvier_http::{FromRequest, HttpIngress, HttpRouteDescriptor, IntoResponse};
6use schemars::{JsonSchema, schema_for};
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct OpenApiDocument {
12    pub openapi: String,
13    pub info: OpenApiInfo,
14    pub paths: BTreeMap<String, OpenApiPathItem>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub components: Option<OpenApiComponents>,
17}
18
19/// OpenAPI components object (schemas, securitySchemes, etc.).
20#[derive(Clone, Debug, Serialize, Deserialize, Default)]
21pub struct OpenApiComponents {
22    #[serde(rename = "securitySchemes", skip_serializing_if = "BTreeMap::is_empty", default)]
23    pub security_schemes: BTreeMap<String, SecurityScheme>,
24    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
25    pub schemas: BTreeMap<String, Value>,
26}
27
28/// OpenAPI Security Scheme object.
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub struct SecurityScheme {
31    #[serde(rename = "type")]
32    pub scheme_type: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub scheme: Option<String>,
35    #[serde(rename = "bearerFormat", skip_serializing_if = "Option::is_none")]
36    pub bearer_format: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub description: Option<String>,
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct OpenApiInfo {
43    pub title: String,
44    pub version: String,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub description: Option<String>,
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize, Default)]
50pub struct OpenApiPathItem {
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub get: Option<OpenApiOperation>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub post: Option<OpenApiOperation>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub put: Option<OpenApiOperation>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub delete: Option<OpenApiOperation>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub patch: Option<OpenApiOperation>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub options: Option<OpenApiOperation>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub head: Option<OpenApiOperation>,
65}
66
67impl OpenApiPathItem {
68    fn set_operation(&mut self, method: &Method, operation: OpenApiOperation) {
69        match *method {
70            Method::GET => self.get = Some(operation),
71            Method::POST => self.post = Some(operation),
72            Method::PUT => self.put = Some(operation),
73            Method::DELETE => self.delete = Some(operation),
74            Method::PATCH => self.patch = Some(operation),
75            Method::OPTIONS => self.options = Some(operation),
76            Method::HEAD => self.head = Some(operation),
77            _ => {}
78        }
79    }
80}
81
82#[derive(Clone, Debug, Serialize, Deserialize)]
83pub struct OpenApiOperation {
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub summary: Option<String>,
86    #[serde(rename = "operationId", skip_serializing_if = "Option::is_none")]
87    pub operation_id: Option<String>,
88    #[serde(skip_serializing_if = "Vec::is_empty", default)]
89    pub parameters: Vec<OpenApiParameter>,
90    #[serde(rename = "requestBody", skip_serializing_if = "Option::is_none")]
91    pub request_body: Option<OpenApiRequestBody>,
92    pub responses: BTreeMap<String, OpenApiResponse>,
93    #[serde(rename = "x-ranvier", skip_serializing_if = "Option::is_none")]
94    pub x_ranvier: Option<Value>,
95}
96
97#[derive(Clone, Debug, Serialize, Deserialize)]
98pub struct OpenApiParameter {
99    pub name: String,
100    #[serde(rename = "in")]
101    pub location: String,
102    pub required: bool,
103    pub schema: Value,
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
107pub struct OpenApiRequestBody {
108    pub required: bool,
109    pub content: BTreeMap<String, OpenApiMediaType>,
110}
111
112#[derive(Clone, Debug, Serialize, Deserialize)]
113pub struct OpenApiResponse {
114    pub description: String,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub content: Option<BTreeMap<String, OpenApiMediaType>>,
117}
118
119#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct OpenApiMediaType {
121    pub schema: Value,
122}
123
124#[derive(Clone, Debug)]
125struct OperationPatch {
126    summary: Option<String>,
127    request_schema: Option<Value>,
128    response_schema: Option<Value>,
129}
130
131impl OperationPatch {
132    fn apply(self, operation: &mut OpenApiOperation) {
133        if let Some(summary) = self.summary {
134            operation.summary = Some(summary);
135        }
136        if let Some(schema) = self.request_schema {
137            let mut content = BTreeMap::new();
138            content.insert("application/json".to_string(), OpenApiMediaType { schema });
139            operation.request_body = Some(OpenApiRequestBody {
140                required: true,
141                content,
142            });
143        }
144        if let Some(schema) = self.response_schema {
145            let mut content = BTreeMap::new();
146            content.insert("application/json".to_string(), OpenApiMediaType { schema });
147
148            let response =
149                operation
150                    .responses
151                    .entry("200".to_string())
152                    .or_insert(OpenApiResponse {
153                        description: "Successful response".to_string(),
154                        content: None,
155                    });
156            response.content = Some(content);
157        }
158    }
159}
160
161#[derive(Clone, Debug)]
162struct SchematicMetadata {
163    id: String,
164    name: String,
165    node_count: usize,
166    edge_count: usize,
167}
168
169impl From<&Schematic> for SchematicMetadata {
170    fn from(value: &Schematic) -> Self {
171        Self {
172            id: value.id.clone(),
173            name: value.name.clone(),
174            node_count: value.nodes.len(),
175            edge_count: value.edges.len(),
176        }
177    }
178}
179
180/// OpenAPI generator bound to a set of ingress route descriptors.
181#[derive(Clone, Debug)]
182pub struct OpenApiGenerator {
183    routes: Vec<HttpRouteDescriptor>,
184    title: String,
185    version: String,
186    description: Option<String>,
187    patches: HashMap<String, OperationPatch>,
188    schematic: Option<SchematicMetadata>,
189    bearer_auth: bool,
190    problem_detail_errors: bool,
191}
192
193impl OpenApiGenerator {
194    pub fn from_descriptors(routes: Vec<HttpRouteDescriptor>) -> Self {
195        Self {
196            routes,
197            title: "Ranvier API".to_string(),
198            version: "0.1.0".to_string(),
199            description: None,
200            patches: HashMap::new(),
201            schematic: None,
202            bearer_auth: false,
203            problem_detail_errors: false,
204        }
205    }
206
207    pub fn from_ingress<R>(ingress: &HttpIngress<R>) -> Self
208    where
209        R: ranvier_core::transition::ResourceRequirement + Clone + Send + Sync + 'static,
210    {
211        Self::from_descriptors(ingress.route_descriptors())
212    }
213
214    pub fn title(mut self, title: impl Into<String>) -> Self {
215        self.title = title.into();
216        self
217    }
218
219    pub fn version(mut self, version: impl Into<String>) -> Self {
220        self.version = version.into();
221        self
222    }
223
224    pub fn description(mut self, description: impl Into<String>) -> Self {
225        self.description = Some(description.into());
226        self
227    }
228
229    pub fn with_schematic(mut self, schematic: &Schematic) -> Self {
230        self.schematic = Some(SchematicMetadata::from(schematic));
231        self
232    }
233
234    pub fn summary(
235        mut self,
236        method: Method,
237        path_pattern: impl AsRef<str>,
238        summary: impl Into<String>,
239    ) -> Self {
240        let key = operation_key(&method, path_pattern.as_ref());
241        let patch = self.patches.entry(key).or_insert(OperationPatch {
242            summary: None,
243            request_schema: None,
244            response_schema: None,
245        });
246        patch.summary = Some(summary.into());
247        self
248    }
249
250    pub fn json_request_schema<T>(mut self, method: Method, path_pattern: impl AsRef<str>) -> Self
251    where
252        T: JsonSchema,
253    {
254        let key = operation_key(&method, path_pattern.as_ref());
255        let patch = self.patches.entry(key).or_insert(OperationPatch {
256            summary: None,
257            request_schema: None,
258            response_schema: None,
259        });
260        patch.request_schema = Some(schema_value::<T>());
261        self
262    }
263
264    /// Register JSON request schema using a `FromRequest` implementor type.
265    pub fn json_request_schema_from_extractor<T>(
266        self,
267        method: Method,
268        path_pattern: impl AsRef<str>,
269    ) -> Self
270    where
271        T: FromRequest + JsonSchema,
272    {
273        self.json_request_schema::<T>(method, path_pattern)
274    }
275
276    pub fn json_response_schema<T>(mut self, method: Method, path_pattern: impl AsRef<str>) -> Self
277    where
278        T: JsonSchema,
279    {
280        let key = operation_key(&method, path_pattern.as_ref());
281        let patch = self.patches.entry(key).or_insert(OperationPatch {
282            summary: None,
283            request_schema: None,
284            response_schema: None,
285        });
286        patch.response_schema = Some(schema_value::<T>());
287        self
288    }
289
290    /// Register JSON response schema using an `IntoResponse` implementor type.
291    pub fn json_response_schema_from_into_response<T>(
292        self,
293        method: Method,
294        path_pattern: impl AsRef<str>,
295    ) -> Self
296    where
297        T: IntoResponse + JsonSchema,
298    {
299        self.json_response_schema::<T>(method, path_pattern)
300    }
301
302    /// Add a Bearer token SecurityScheme (`bearerAuth`) to the spec.
303    ///
304    /// When enabled, each operation will reference the `bearerAuth` security scheme,
305    /// and the Swagger UI will show a token input.
306    pub fn with_bearer_auth(mut self) -> Self {
307        self.bearer_auth = true;
308        self
309    }
310
311    /// Add RFC 7807 ProblemDetail error schemas for 4xx/5xx responses.
312    ///
313    /// When enabled, every operation gets `400`, `404`, and `500` response entries
314    /// with a `ProblemDetail` JSON schema.
315    pub fn with_problem_detail_errors(mut self) -> Self {
316        self.problem_detail_errors = true;
317        self
318    }
319
320    pub fn build(self) -> OpenApiDocument {
321        let mut paths = BTreeMap::new();
322
323        for route in self.routes {
324            let (openapi_path, parameters) = normalize_path(route.path_pattern());
325            let default_summary = format!("{} {}", route.method(), openapi_path);
326            let operation_id = format!(
327                "{}_{}",
328                route.method().as_str().to_ascii_lowercase(),
329                route
330                    .path_pattern()
331                    .trim_matches('/')
332                    .replace(['/', ':', '*'], "_")
333                    .trim_matches('_')
334            );
335
336            let mut operation = OpenApiOperation {
337                summary: Some(default_summary),
338                operation_id: if operation_id.is_empty() {
339                    None
340                } else {
341                    Some(operation_id)
342                },
343                parameters,
344                request_body: None,
345                responses: BTreeMap::from([(
346                    "200".to_string(),
347                    OpenApiResponse {
348                        description: "Successful response".to_string(),
349                        content: None,
350                    },
351                )]),
352                x_ranvier: self.schematic.as_ref().map(|metadata| {
353                    json!({
354                        "schematic_id": metadata.id,
355                        "schematic_name": metadata.name,
356                        "node_count": metadata.node_count,
357                        "edge_count": metadata.edge_count,
358                        "route_pattern": route.path_pattern(),
359                    })
360                }),
361            };
362
363            if let Some(patch) = self
364                .patches
365                .get(&operation_key(route.method(), route.path_pattern()))
366            {
367                patch.clone().apply(&mut operation);
368            }
369
370            // Add ProblemDetail error responses
371            if self.problem_detail_errors {
372                let problem_ref = json!({"$ref": "#/components/schemas/ProblemDetail"});
373                let mut problem_content = BTreeMap::new();
374                problem_content.insert(
375                    "application/problem+json".to_string(),
376                    OpenApiMediaType {
377                        schema: problem_ref,
378                    },
379                );
380
381                for (code, desc) in [
382                    ("400", "Bad Request"),
383                    ("404", "Not Found"),
384                    ("500", "Internal Server Error"),
385                ] {
386                    operation.responses.entry(code.to_string()).or_insert(
387                        OpenApiResponse {
388                            description: desc.to_string(),
389                            content: Some(problem_content.clone()),
390                        },
391                    );
392                }
393            }
394
395            paths
396                .entry(openapi_path)
397                .or_insert_with(OpenApiPathItem::default)
398                .set_operation(route.method(), operation);
399        }
400
401        // Build components
402        let mut components = OpenApiComponents::default();
403
404        if self.bearer_auth {
405            components.security_schemes.insert(
406                "bearerAuth".to_string(),
407                SecurityScheme {
408                    scheme_type: "http".to_string(),
409                    scheme: Some("bearer".to_string()),
410                    bearer_format: Some("JWT".to_string()),
411                    description: Some("Bearer token authentication".to_string()),
412                },
413            );
414        }
415
416        if self.problem_detail_errors {
417            components.schemas.insert(
418                "ProblemDetail".to_string(),
419                json!({
420                    "type": "object",
421                    "description": "RFC 7807 Problem Detail",
422                    "properties": {
423                        "type": { "type": "string", "description": "URI reference identifying the problem type" },
424                        "title": { "type": "string", "description": "Short human-readable summary" },
425                        "status": { "type": "integer", "description": "HTTP status code" },
426                        "detail": { "type": "string", "description": "Human-readable explanation" },
427                        "instance": { "type": "string", "description": "URI reference identifying the specific occurrence" }
428                    },
429                    "required": ["type", "title", "status"]
430                }),
431            );
432        }
433
434        let has_components = !components.security_schemes.is_empty()
435            || !components.schemas.is_empty();
436
437        OpenApiDocument {
438            openapi: "3.0.3".to_string(),
439            info: OpenApiInfo {
440                title: self.title,
441                version: self.version,
442                description: self.description,
443            },
444            paths,
445            components: if has_components {
446                Some(components)
447            } else {
448                None
449            },
450        }
451    }
452
453    pub fn build_json(self) -> Value {
454        serde_json::to_value(self.build()).expect("openapi document should serialize")
455    }
456
457    pub fn build_pretty_json(self) -> String {
458        serde_json::to_string_pretty(&self.build()).expect("openapi document should serialize")
459    }
460}
461
462pub fn swagger_ui_html(spec_url: &str, title: &str) -> String {
463    format!(
464        r#"<!doctype html>
465<html lang="en">
466<head>
467  <meta charset="utf-8" />
468  <meta name="viewport" content="width=device-width,initial-scale=1" />
469  <title>{title}</title>
470  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
471</head>
472<body>
473  <div id="swagger-ui"></div>
474  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
475  <script>
476    window.ui = SwaggerUIBundle({{
477      url: '{spec_url}',
478      dom_id: '#swagger-ui',
479      deepLinking: true,
480      presets: [SwaggerUIBundle.presets.apis]
481    }});
482  </script>
483</body>
484</html>"#
485    )
486}
487
488fn operation_key(method: &Method, path_pattern: &str) -> String {
489    format!("{} {}", method.as_str(), path_pattern)
490}
491
492fn normalize_path(path_pattern: &str) -> (String, Vec<OpenApiParameter>) {
493    if path_pattern == "/" {
494        return ("/".to_string(), Vec::new());
495    }
496
497    let mut params = Vec::new();
498    let mut segments = Vec::new();
499
500    for segment in path_pattern
501        .trim_matches('/')
502        .split('/')
503        .filter(|segment| !segment.is_empty())
504    {
505        if let Some(name) = segment
506            .strip_prefix(':')
507            .or_else(|| segment.strip_prefix('*'))
508        {
509            let normalized_name = if name.is_empty() { "path" } else { name };
510            segments.push(format!("{{{normalized_name}}}"));
511            params.push(OpenApiParameter {
512                name: normalized_name.to_string(),
513                location: "path".to_string(),
514                required: true,
515                schema: json!({"type": "string"}),
516            });
517            continue;
518        }
519
520        segments.push(segment.to_string());
521    }
522
523    (format!("/{}", segments.join("/")), params)
524}
525
526fn schema_value<T>() -> Value
527where
528    T: JsonSchema,
529{
530    serde_json::to_value(schema_for!(T)).expect("json schema should serialize")
531}
532
533pub mod prelude {
534    pub use crate::{
535        OpenApiComponents, OpenApiDocument, OpenApiGenerator, SecurityScheme, swagger_ui_html,
536    };
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use schemars::JsonSchema;
543
544    #[derive(JsonSchema)]
545    #[allow(dead_code)]
546    struct CreateUserRequest {
547        email: String,
548    }
549
550    #[derive(JsonSchema)]
551    #[allow(dead_code)]
552    struct CreateUserResponse {
553        id: String,
554    }
555
556    #[test]
557    fn normalize_path_converts_param_and_wildcard_segments() {
558        let (path, params) = normalize_path("/users/:id/files/*path");
559        assert_eq!(path, "/users/{id}/files/{path}");
560        assert_eq!(params.len(), 2);
561        assert_eq!(params[0].name, "id");
562        assert_eq!(params[1].name, "path");
563    }
564
565    #[test]
566    fn generator_builds_paths_from_route_descriptors() {
567        let doc = OpenApiGenerator::from_descriptors(vec![
568            HttpRouteDescriptor::new(Method::GET, "/users/:id"),
569            HttpRouteDescriptor::new(Method::POST, "/users"),
570        ])
571        .title("Users API")
572        .version("0.7.0")
573        .build();
574
575        assert_eq!(doc.info.title, "Users API");
576        assert!(doc.paths.contains_key("/users/{id}"));
577        assert!(doc.paths.contains_key("/users"));
578        assert!(doc.paths["/users/{id}"].get.is_some());
579        assert!(doc.paths["/users"].post.is_some());
580    }
581
582    #[test]
583    fn generator_applies_json_request_response_schema_overrides() {
584        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
585            Method::POST,
586            "/users",
587        )])
588        .json_request_schema::<CreateUserRequest>(Method::POST, "/users")
589        .json_response_schema::<CreateUserResponse>(Method::POST, "/users")
590        .summary(Method::POST, "/users", "Create a user")
591        .build();
592
593        let operation = doc.paths["/users"].post.as_ref().expect("post operation");
594        assert_eq!(operation.summary.as_deref(), Some("Create a user"));
595        assert!(operation.request_body.is_some());
596        assert!(
597            operation.responses["200"]
598                .content
599                .as_ref()
600                .expect("response content")
601                .contains_key("application/json")
602        );
603    }
604
605    #[test]
606    fn swagger_html_contains_spec_url() {
607        let html = swagger_ui_html("/openapi.json", "API Docs");
608        assert!(html.contains("/openapi.json"));
609        assert!(html.contains("SwaggerUIBundle"));
610    }
611
612    // --- M241: SecurityScheme + ProblemDetail tests ---
613
614    #[test]
615    fn bearer_auth_adds_security_scheme() {
616        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
617            Method::GET,
618            "/users",
619        )])
620        .with_bearer_auth()
621        .build();
622
623        let components = doc.components.expect("should have components");
624        let scheme = components
625            .security_schemes
626            .get("bearerAuth")
627            .expect("should have bearerAuth");
628        assert_eq!(scheme.scheme_type, "http");
629        assert_eq!(scheme.scheme.as_deref(), Some("bearer"));
630        assert_eq!(scheme.bearer_format.as_deref(), Some("JWT"));
631    }
632
633    #[test]
634    fn no_bearer_auth_means_no_components() {
635        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
636            Method::GET,
637            "/users",
638        )])
639        .build();
640
641        assert!(doc.components.is_none());
642    }
643
644    #[test]
645    fn problem_detail_adds_error_responses() {
646        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
647            Method::GET,
648            "/users",
649        )])
650        .with_problem_detail_errors()
651        .build();
652
653        let op = doc.paths["/users"].get.as_ref().unwrap();
654        assert!(op.responses.contains_key("400"));
655        assert!(op.responses.contains_key("404"));
656        assert!(op.responses.contains_key("500"));
657
658        let r404 = &op.responses["404"];
659        assert_eq!(r404.description, "Not Found");
660        let content = r404.content.as_ref().unwrap();
661        assert!(content.contains_key("application/problem+json"));
662    }
663
664    #[test]
665    fn problem_detail_schema_in_components() {
666        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
667            Method::GET,
668            "/users",
669        )])
670        .with_problem_detail_errors()
671        .build();
672
673        let components = doc.components.expect("should have components");
674        let schema = components
675            .schemas
676            .get("ProblemDetail")
677            .expect("should have ProblemDetail schema");
678        assert_eq!(schema["type"], "object");
679        assert!(schema["properties"]["type"].is_object());
680        assert!(schema["properties"]["title"].is_object());
681        assert!(schema["properties"]["status"].is_object());
682    }
683
684    #[test]
685    fn problem_detail_references_schema() {
686        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
687            Method::POST,
688            "/orders",
689        )])
690        .with_problem_detail_errors()
691        .build();
692
693        let op = doc.paths["/orders"].post.as_ref().unwrap();
694        let content_500 = op.responses["500"].content.as_ref().unwrap();
695        let schema = &content_500["application/problem+json"].schema;
696        assert_eq!(schema["$ref"], "#/components/schemas/ProblemDetail");
697    }
698
699    #[test]
700    fn multiple_routes_all_get_error_responses() {
701        let doc = OpenApiGenerator::from_descriptors(vec![
702            HttpRouteDescriptor::new(Method::GET, "/users"),
703            HttpRouteDescriptor::new(Method::POST, "/users"),
704            HttpRouteDescriptor::new(Method::DELETE, "/users/:id"),
705        ])
706        .with_problem_detail_errors()
707        .build();
708
709        // All operations should have error responses
710        let get_op = doc.paths["/users"].get.as_ref().unwrap();
711        let post_op = doc.paths["/users"].post.as_ref().unwrap();
712        let delete_op = doc.paths["/users/{id}"].delete.as_ref().unwrap();
713
714        assert!(get_op.responses.contains_key("400"));
715        assert!(post_op.responses.contains_key("404"));
716        assert!(delete_op.responses.contains_key("500"));
717    }
718
719    #[test]
720    fn bearer_auth_and_problem_detail_combined() {
721        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
722            Method::GET,
723            "/protected",
724        )])
725        .with_bearer_auth()
726        .with_problem_detail_errors()
727        .build();
728
729        let components = doc.components.expect("should have components");
730        assert!(components.security_schemes.contains_key("bearerAuth"));
731        assert!(components.schemas.contains_key("ProblemDetail"));
732    }
733
734    #[test]
735    fn bearer_auth_serializes_in_json() {
736        let doc = OpenApiGenerator::from_descriptors(vec![HttpRouteDescriptor::new(
737            Method::GET,
738            "/api",
739        )])
740        .with_bearer_auth()
741        .build_json();
742
743        let schemes = &doc["components"]["securitySchemes"];
744        assert_eq!(schemes["bearerAuth"]["type"], "http");
745        assert_eq!(schemes["bearerAuth"]["scheme"], "bearer");
746        assert_eq!(schemes["bearerAuth"]["bearerFormat"], "JWT");
747    }
748}