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    #[serde(default)]
59    pub workspace_mode: Option<bool>,
60}
61
62impl From<toml::value::Table> for RustGeneratorConfig {
63    fn from(value: toml::value::Table) -> Self {
64        use serde::Deserialize;
65        match RustGeneratorConfig::deserialize(value) {
66            Ok(config) => config,
67            Err(e) => {
68                error!(
69                    "Failed to parse Rust generator config: {}. Using default configuration.",
70                    e
71                );
72                Self::default()
73            }
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn table(toml_str: &str) -> toml::value::Table {
83        toml::from_str::<toml::value::Table>(toml_str).unwrap()
84    }
85
86    #[test]
87    fn default_config_has_no_extra_derives() {
88        let config = RustGeneratorConfig::default();
89        assert!(config.crate_name.is_none());
90        assert!(config.extra_derives.is_none());
91    }
92
93    #[test]
94    fn deserialize_empty_table() {
95        let config = RustGeneratorConfig::from(toml::value::Table::new());
96        assert!(config.crate_name.is_none());
97        assert!(config.extra_derives.is_none());
98    }
99
100    #[test]
101    fn deserialize_crate_name_only() {
102        let config = RustGeneratorConfig::from(table(r#"crate_name = "my-sdk""#));
103        assert_eq!(config.crate_name.as_deref(), Some("my-sdk"));
104        assert!(config.extra_derives.is_none());
105    }
106
107    #[test]
108    fn deserialize_extra_derives_structs() {
109        let config = RustGeneratorConfig::from(table(
110            r#"
111            [extra_derives.structs]
112            derives = ["PartialEq", "Hash"]
113            "#,
114        ));
115        let extra = config.extra_derives.unwrap();
116        let structs = extra.structs.unwrap();
117        assert_eq!(structs.derives, vec!["PartialEq", "Hash"]);
118        assert!(structs.dependencies.is_empty());
119        assert!(extra.enums.is_none());
120        assert!(extra.unions.is_none());
121        assert!(extra.response_structs.is_none());
122    }
123
124    #[test]
125    fn deserialize_extra_derives_all_kinds() {
126        let config = RustGeneratorConfig::from(table(
127            r#"
128            [extra_derives.structs]
129            derives = ["PartialEq"]
130            [extra_derives.enums]
131            derives = ["Hash"]
132            [extra_derives.unions]
133            derives = ["Clone"]
134            [extra_derives.response_structs]
135            derives = ["PartialEq", "Eq"]
136            "#,
137        ));
138        let extra = config.extra_derives.unwrap();
139        assert_eq!(extra.structs.unwrap().derives, vec!["PartialEq"]);
140        assert_eq!(extra.enums.unwrap().derives, vec!["Hash"]);
141        assert_eq!(extra.unions.unwrap().derives, vec!["Clone"]);
142        assert_eq!(
143            extra.response_structs.unwrap().derives,
144            vec!["PartialEq", "Eq"]
145        );
146    }
147
148    #[test]
149    fn deserialize_extra_derives_with_dependencies() {
150        let config = RustGeneratorConfig::from(table(
151            r#"
152            [extra_derives.structs]
153            derives = ["utoipa::ToSchema"]
154            [extra_derives.structs.dependencies]
155            utoipa = '{ version = "5", features = ["openapi_extensions"] }'
156            "#,
157        ));
158        let structs = config.extra_derives.unwrap().structs.unwrap();
159        assert_eq!(structs.derives, vec!["utoipa::ToSchema"]);
160        assert_eq!(
161            structs.dependencies.get("utoipa").unwrap(),
162            r#"{ version = "5", features = ["openapi_extensions"] }"#
163        );
164    }
165
166    #[test]
167    fn all_dependencies_merges_across_kinds() {
168        let extra = ExtraDerivesConfig {
169            structs: Some(ExtraDeriveConfig {
170                derives: vec!["utoipa::ToSchema".into()],
171                dependencies: BTreeMap::from([
172                    ("utoipa".into(), r#"{ version = "5" }"#.into()),
173                    ("shared".into(), "\"1.0\"".into()),
174                ]),
175            }),
176            enums: Some(ExtraDeriveConfig {
177                derives: vec!["strum::Display".into()],
178                dependencies: BTreeMap::from([
179                    ("strum".into(), r#"{ version = "0.26" }"#.into()),
180                    ("shared".into(), "\"2.0\"".into()),
181                ]),
182            }),
183            unions: None,
184            response_structs: None,
185        };
186        let deps = extra.all_dependencies();
187        assert_eq!(deps.len(), 3);
188        assert_eq!(deps["shared"], "\"1.0\"");
189        assert!(deps.contains_key("utoipa"));
190        assert!(deps.contains_key("strum"));
191    }
192
193    #[test]
194    fn all_dependencies_empty_when_no_extra() {
195        let extra = ExtraDerivesConfig::default();
196        assert!(extra.all_dependencies().is_empty());
197    }
198
199    #[test]
200    fn invalid_config_falls_back_to_default() {
201        let mut t = toml::value::Table::new();
202        t.insert(
203            "extra_derives".into(),
204            toml::Value::String("invalid".into()),
205        );
206        let config = RustGeneratorConfig::from(t);
207        assert!(config.crate_name.is_none());
208        assert!(config.extra_derives.is_none());
209    }
210}