Skip to main content

openapi_nexus/generators/rust/common/
config.rs

1//! Base configuration shared across all Rust generators.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use tracing::error;
7
8/// Extra derives for a single type-kind (structs, enums, unions, or response structs).
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct ExtraDeriveConfig {
11    #[serde(default)]
12    pub derives: Vec<String>,
13    #[serde(default)]
14    pub dependencies: BTreeMap<String, String>,
15}
16
17/// Per-type-kind extra derive configuration.
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
19pub struct ExtraDerivesConfig {
20    #[serde(default)]
21    pub structs: Option<ExtraDeriveConfig>,
22    #[serde(default)]
23    pub enums: Option<ExtraDeriveConfig>,
24    #[serde(default)]
25    pub unions: Option<ExtraDeriveConfig>,
26    #[serde(default)]
27    pub response_structs: Option<ExtraDeriveConfig>,
28}
29
30impl ExtraDerivesConfig {
31    /// Collect all unique crate dependencies across every type-kind.
32    pub fn all_dependencies(&self) -> BTreeMap<String, String> {
33        let mut deps = BTreeMap::new();
34        for cfg in [
35            &self.structs,
36            &self.enums,
37            &self.unions,
38            &self.response_structs,
39        ]
40        .into_iter()
41        .flatten()
42        {
43            for (k, v) in &cfg.dependencies {
44                deps.entry(k.clone()).or_insert_with(|| v.clone());
45            }
46        }
47        deps
48    }
49}
50
51/// Base configuration for Rust generators.
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53pub struct RustGeneratorConfig {
54    #[serde(default)]
55    pub crate_name: Option<String>,
56    #[serde(default)]
57    pub extra_derives: Option<ExtraDerivesConfig>,
58}
59
60impl From<toml::value::Table> for RustGeneratorConfig {
61    fn from(value: toml::value::Table) -> Self {
62        use serde::Deserialize;
63        match RustGeneratorConfig::deserialize(value) {
64            Ok(config) => config,
65            Err(e) => {
66                error!(
67                    "Failed to parse Rust generator config: {}. Using default configuration.",
68                    e
69                );
70                Self::default()
71            }
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn table(toml_str: &str) -> toml::value::Table {
81        toml::from_str::<toml::value::Table>(toml_str).unwrap()
82    }
83
84    #[test]
85    fn default_config_has_no_extra_derives() {
86        let config = RustGeneratorConfig::default();
87        assert!(config.crate_name.is_none());
88        assert!(config.extra_derives.is_none());
89    }
90
91    #[test]
92    fn deserialize_empty_table() {
93        let config = RustGeneratorConfig::from(toml::value::Table::new());
94        assert!(config.crate_name.is_none());
95        assert!(config.extra_derives.is_none());
96    }
97
98    #[test]
99    fn deserialize_crate_name_only() {
100        let config = RustGeneratorConfig::from(table(r#"crate_name = "my-sdk""#));
101        assert_eq!(config.crate_name.as_deref(), Some("my-sdk"));
102        assert!(config.extra_derives.is_none());
103    }
104
105    #[test]
106    fn deserialize_extra_derives_structs() {
107        let config = RustGeneratorConfig::from(table(
108            r#"
109            [extra_derives.structs]
110            derives = ["PartialEq", "Hash"]
111            "#,
112        ));
113        let extra = config.extra_derives.unwrap();
114        let structs = extra.structs.unwrap();
115        assert_eq!(structs.derives, vec!["PartialEq", "Hash"]);
116        assert!(structs.dependencies.is_empty());
117        assert!(extra.enums.is_none());
118        assert!(extra.unions.is_none());
119        assert!(extra.response_structs.is_none());
120    }
121
122    #[test]
123    fn deserialize_extra_derives_all_kinds() {
124        let config = RustGeneratorConfig::from(table(
125            r#"
126            [extra_derives.structs]
127            derives = ["PartialEq"]
128            [extra_derives.enums]
129            derives = ["Hash"]
130            [extra_derives.unions]
131            derives = ["Clone"]
132            [extra_derives.response_structs]
133            derives = ["PartialEq", "Eq"]
134            "#,
135        ));
136        let extra = config.extra_derives.unwrap();
137        assert_eq!(extra.structs.unwrap().derives, vec!["PartialEq"]);
138        assert_eq!(extra.enums.unwrap().derives, vec!["Hash"]);
139        assert_eq!(extra.unions.unwrap().derives, vec!["Clone"]);
140        assert_eq!(
141            extra.response_structs.unwrap().derives,
142            vec!["PartialEq", "Eq"]
143        );
144    }
145
146    #[test]
147    fn deserialize_extra_derives_with_dependencies() {
148        let config = RustGeneratorConfig::from(table(
149            r#"
150            [extra_derives.structs]
151            derives = ["utoipa::ToSchema"]
152            [extra_derives.structs.dependencies]
153            utoipa = '{ version = "5", features = ["openapi_extensions"] }'
154            "#,
155        ));
156        let structs = config.extra_derives.unwrap().structs.unwrap();
157        assert_eq!(structs.derives, vec!["utoipa::ToSchema"]);
158        assert_eq!(
159            structs.dependencies.get("utoipa").unwrap(),
160            r#"{ version = "5", features = ["openapi_extensions"] }"#
161        );
162    }
163
164    #[test]
165    fn all_dependencies_merges_across_kinds() {
166        let extra = ExtraDerivesConfig {
167            structs: Some(ExtraDeriveConfig {
168                derives: vec!["utoipa::ToSchema".into()],
169                dependencies: BTreeMap::from([
170                    ("utoipa".into(), r#"{ version = "5" }"#.into()),
171                    ("shared".into(), "\"1.0\"".into()),
172                ]),
173            }),
174            enums: Some(ExtraDeriveConfig {
175                derives: vec!["strum::Display".into()],
176                dependencies: BTreeMap::from([
177                    ("strum".into(), r#"{ version = "0.26" }"#.into()),
178                    ("shared".into(), "\"2.0\"".into()),
179                ]),
180            }),
181            unions: None,
182            response_structs: None,
183        };
184        let deps = extra.all_dependencies();
185        assert_eq!(deps.len(), 3);
186        assert_eq!(deps["shared"], "\"1.0\"");
187        assert!(deps.contains_key("utoipa"));
188        assert!(deps.contains_key("strum"));
189    }
190
191    #[test]
192    fn all_dependencies_empty_when_no_extra() {
193        let extra = ExtraDerivesConfig::default();
194        assert!(extra.all_dependencies().is_empty());
195    }
196
197    #[test]
198    fn invalid_config_falls_back_to_default() {
199        let mut t = toml::value::Table::new();
200        t.insert(
201            "extra_derives".into(),
202            toml::Value::String("invalid".into()),
203        );
204        let config = RustGeneratorConfig::from(t);
205        assert!(config.crate_name.is_none());
206        assert!(config.extra_derives.is_none());
207    }
208}