Skip to main content

nono_proxy/
diagnostic.rs

1//! Proxy startup diagnostics (credential load and OAuth exchange failures).
2
3use serde::{Deserialize, Serialize};
4
5/// Severity of a proxy diagnostic.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ProxyDiagnosticSeverity {
9    Info,
10    Warning,
11    Error,
12}
13
14/// Stable diagnostic code for proxy credential and route issues.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum ProxyDiagnosticCode {
19    CredentialNotFound,
20    CredentialUnavailable,
21    OAuthClientIdUnavailable,
22    OAuthClientSecretUnavailable,
23    OAuthTokenExchangeFailed,
24}
25
26impl ProxyDiagnosticCode {
27    /// Stable snake_case label matching JSON serialization.
28    #[must_use]
29    pub fn as_str(self) -> &'static str {
30        match self {
31            Self::CredentialNotFound => "credential_not_found",
32            Self::CredentialUnavailable => "credential_unavailable",
33            Self::OAuthClientIdUnavailable => "oauth_client_id_unavailable",
34            Self::OAuthClientSecretUnavailable => "oauth_client_secret_unavailable",
35            Self::OAuthTokenExchangeFailed => "oauth_token_exchange_failed",
36        }
37    }
38}
39
40/// One proxy startup diagnostic.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ProxyDiagnostic {
43    pub code: ProxyDiagnosticCode,
44    pub severity: ProxyDiagnosticSeverity,
45    pub route_prefix: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub credential_ref: Option<String>,
48    pub message: String,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub hint: Option<String>,
51}
52
53impl ProxyDiagnostic {
54    #[must_use]
55    pub fn warning(
56        code: ProxyDiagnosticCode,
57        route_prefix: impl Into<String>,
58        message: impl Into<String>,
59    ) -> Self {
60        Self {
61            code,
62            severity: ProxyDiagnosticSeverity::Warning,
63            route_prefix: route_prefix.into(),
64            credential_ref: None,
65            message: message.into(),
66            hint: None,
67        }
68    }
69
70    #[must_use]
71    pub fn with_credential_ref(mut self, credential_ref: impl Into<String>) -> Self {
72        self.credential_ref = Some(credential_ref.into());
73        self
74    }
75
76    #[must_use]
77    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
78        self.hint = Some(hint.into());
79        self
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn proxy_diagnostic_serializes_stable_code() {
89        let diagnostic = ProxyDiagnostic::warning(
90            ProxyDiagnosticCode::CredentialNotFound,
91            "openai",
92            "Credential not found",
93        )
94        .with_credential_ref("op://vault/item/secret");
95        let json = serde_json::to_string(&diagnostic).expect("json");
96        assert!(json.contains("\"credential_not_found\""));
97        assert!(json.contains("\"openai\""));
98    }
99}