openapi_nexus/generators/rust/common/
config.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use tracing::error;
7
8#[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#[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 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#[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}