Skip to main content

owlauth_types/
export.rs

1use std::{fmt, str::FromStr};
2
3/// Public HTTP plane whose `OpenAPI` document should be exported.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub enum OpenApiPlane {
6    /// Project Auth Runtime API.
7    Runtime,
8    /// Project-scoped customer backend Server API.
9    Server,
10    /// Deployment Control API.
11    Control,
12}
13
14impl fmt::Display for OpenApiPlane {
15    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
16        formatter.write_str(match self {
17            Self::Runtime => "runtime",
18            Self::Server => "server",
19            Self::Control => "control",
20        })
21    }
22}
23
24impl FromStr for OpenApiPlane {
25    type Err = ParseOpenApiPlaneError;
26
27    fn from_str(value: &str) -> Result<Self, Self::Err> {
28        match value {
29            "runtime" => Ok(Self::Runtime),
30            "server" => Ok(Self::Server),
31            "control" => Ok(Self::Control),
32            _ => Err(ParseOpenApiPlaneError),
33        }
34    }
35}
36
37/// Error returned for an unsupported `OpenAPI` plane name.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct ParseOpenApiPlaneError;
40
41impl fmt::Display for ParseOpenApiPlaneError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str("plane must be `runtime`, `server`, or `control`")
44    }
45}
46
47impl std::error::Error for ParseOpenApiPlaneError {}
48
49/// Generates one complete plane-specific `OpenAPI` document as stable pretty JSON.
50///
51/// # Errors
52///
53/// Returns a serialization error if the generated document cannot be encoded.
54pub fn to_pretty_json(plane: OpenApiPlane) -> Result<String, serde_json::Error> {
55    match plane {
56        OpenApiPlane::Runtime => {
57            let mut document = serde_json::to_value(crate::runtime::openapi())?;
58            require_contract_headers(&mut document);
59            serde_json::to_string_pretty(&document)
60        }
61        OpenApiPlane::Server => {
62            let mut document = serde_json::to_value(crate::server::openapi())?;
63            require_contract_headers(&mut document);
64            require_server_literal_discriminator(&mut document);
65            serde_json::to_string_pretty(&document)
66        }
67        OpenApiPlane::Control => crate::control::openapi().to_pretty_json(),
68    }
69}
70
71fn require_contract_headers(document: &mut serde_json::Value) {
72    // utoipa 5.5 models response Header Objects without the OpenAPI `required` field.
73    // Preserve the typed Rust declarations, then add that standard field at the one
74    // serialization boundary consumed by checked generated clients.
75    let Some(paths) = document
76        .get_mut("paths")
77        .and_then(serde_json::Value::as_object_mut)
78    else {
79        return;
80    };
81    for path in paths.values_mut() {
82        let Some(operations) = path.as_object_mut() else {
83            continue;
84        };
85        for operation in operations.values_mut() {
86            for (status, header) in [("401", "WWW-Authenticate")] {
87                let Some(value) = operation
88                    .get_mut("responses")
89                    .and_then(|responses| responses.get_mut(status))
90                    .and_then(|response| response.get_mut("headers"))
91                    .and_then(|headers| headers.get_mut(header))
92                    .and_then(serde_json::Value::as_object_mut)
93                else {
94                    continue;
95                };
96                value.insert("required".to_owned(), serde_json::Value::Bool(true));
97            }
98        }
99    }
100}
101
102fn require_server_literal_discriminator(document: &mut serde_json::Value) {
103    for (schema, expected) in [
104        ("InactiveProjectToken", false),
105        ("ActiveProjectToken", true),
106    ] {
107        let Some(active) = document
108            .get_mut("components")
109            .and_then(|components| components.get_mut("schemas"))
110            .and_then(|schemas| schemas.get_mut(schema))
111            .and_then(|schema| schema.get_mut("properties"))
112            .and_then(|properties| properties.get_mut("active"))
113            .and_then(serde_json::Value::as_object_mut)
114        else {
115            continue;
116        };
117        active.insert("const".to_owned(), serde_json::Value::Bool(expected));
118    }
119}