Skip to main content

lenso_openapi_plugin/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Immutable `OpenAPI` document policy selected by App Composition.
4#[derive(Clone, Debug, Deserialize)]
5#[serde(default, deny_unknown_fields)]
6pub struct OpenApiConfig {
7    title: String,
8    version: String,
9    description: Option<String>,
10    document_path: String,
11    servers: Vec<OpenApiServer>,
12    components: Option<serde_json::Map<String, serde_json::Value>>,
13}
14
15impl OpenApiConfig {
16    pub(crate) fn validate(&self) -> Result<(), String> {
17        if self.title.trim().is_empty() {
18            return Err("title must not be empty".to_owned());
19        }
20        if self.version.trim().is_empty() {
21            return Err("version must not be empty".to_owned());
22        }
23        if !self.document_path.starts_with('/') || self.document_path.contains(['?', '#', '{', '}'])
24        {
25            return Err("document_path must be one static absolute path".to_owned());
26        }
27        if self
28            .servers
29            .iter()
30            .any(|server| server.url.trim().is_empty())
31        {
32            return Err("server URLs must not be empty".to_owned());
33        }
34        Ok(())
35    }
36
37    pub(crate) fn title(&self) -> &str {
38        &self.title
39    }
40
41    pub(crate) fn version(&self) -> &str {
42        &self.version
43    }
44
45    pub(crate) fn description(&self) -> Option<&str> {
46        self.description.as_deref()
47    }
48
49    pub(crate) fn document_path(&self) -> &str {
50        &self.document_path
51    }
52
53    pub(crate) fn servers(&self) -> &[OpenApiServer] {
54        &self.servers
55    }
56
57    pub(crate) fn components(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
58        self.components.as_ref()
59    }
60}
61
62impl Default for OpenApiConfig {
63    fn default() -> Self {
64        Self {
65            title: "Lenso App".to_owned(),
66            version: "0.0.0".to_owned(),
67            description: None,
68            document_path: "/openapi.json".to_owned(),
69            servers: Vec::new(),
70            components: None,
71        }
72    }
73}
74
75/// One explicitly configured `OpenAPI` server entry. No address is inferred from Ingress.
76#[derive(Clone, Debug, Deserialize, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct OpenApiServer {
79    url: String,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    description: Option<String>,
82}
83
84#[cfg(test)]
85mod tests {
86    use super::OpenApiConfig;
87
88    #[test]
89    fn defaults_are_valid_and_unknown_configuration_is_rejected() {
90        OpenApiConfig::default().validate().unwrap();
91        let error = serde_json::from_str::<OpenApiConfig>(r#"{"enabled":true}"#).unwrap_err();
92        assert!(error.to_string().contains("unknown field"));
93    }
94
95    #[test]
96    fn document_path_must_be_one_static_absolute_path() {
97        for path in [
98            "openapi.json",
99            "/openapi/{version}.json",
100            "/openapi.json?x=1",
101        ] {
102            let config = serde_json::from_value::<OpenApiConfig>(serde_json::json!({
103                "document_path": path
104            }))
105            .unwrap();
106            assert!(config.validate().is_err(), "accepted {path}");
107        }
108    }
109}