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 #[serde(default)]
29 pub per_type: Option<BTreeMap<String, ExtraDeriveConfig>>,
30}
31
32impl ExtraDerivesConfig {
33 pub fn all_dependencies(&self) -> BTreeMap<String, String> {
35 let mut deps = BTreeMap::new();
36 for cfg in [
37 &self.structs,
38 &self.enums,
39 &self.unions,
40 &self.response_structs,
41 ]
42 .into_iter()
43 .flatten()
44 {
45 for (k, v) in &cfg.dependencies {
46 deps.entry(k.clone()).or_insert_with(|| v.clone());
47 }
48 }
49 if let Some(per_type) = &self.per_type {
50 for cfg in per_type.values() {
51 for (k, v) in &cfg.dependencies {
52 deps.entry(k.clone()).or_insert_with(|| v.clone());
53 }
54 }
55 }
56 deps
57 }
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "snake_case")]
63pub enum WorkspaceDepsMode {
64 #[default]
66 Explicit,
67 WorkspaceVersion,
69 Full,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct RustGeneratorConfig {
76 #[serde(default, alias = "package_name")]
77 pub crate_name: Option<String>,
78 #[serde(default)]
79 pub extra_derives: Option<ExtraDerivesConfig>,
80 #[serde(default)]
81 pub workspace_mode: Option<bool>,
82 #[serde(default)]
83 pub workspace_deps: Option<WorkspaceDepsMode>,
84}
85
86impl From<toml::value::Table> for RustGeneratorConfig {
87 fn from(value: toml::value::Table) -> Self {
88 use serde::Deserialize;
89 match RustGeneratorConfig::deserialize(value) {
90 Ok(config) => config,
91 Err(e) => {
92 error!(
93 "Failed to parse Rust generator config: {}. Using default configuration.",
94 e
95 );
96 Self::default()
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn table(toml_str: &str) -> toml::value::Table {
107 toml::from_str::<toml::value::Table>(toml_str).unwrap()
108 }
109
110 #[test]
111 fn default_config_has_no_extra_derives() {
112 let config = RustGeneratorConfig::default();
113 assert!(config.crate_name.is_none());
114 assert!(config.extra_derives.is_none());
115 }
116
117 #[test]
118 fn deserialize_empty_table() {
119 let config = RustGeneratorConfig::from(toml::value::Table::new());
120 assert!(config.crate_name.is_none());
121 assert!(config.extra_derives.is_none());
122 }
123
124 #[test]
125 fn deserialize_crate_name_only() {
126 let config = RustGeneratorConfig::from(table(r#"crate_name = "my-sdk""#));
127 assert_eq!(config.crate_name.as_deref(), Some("my-sdk"));
128 assert!(config.extra_derives.is_none());
129 }
130
131 #[test]
132 fn deserialize_extra_derives_structs() {
133 let config = RustGeneratorConfig::from(table(
134 r#"
135 [extra_derives.structs]
136 derives = ["PartialEq", "Hash"]
137 "#,
138 ));
139 let extra = config.extra_derives.unwrap();
140 let structs = extra.structs.unwrap();
141 assert_eq!(structs.derives, vec!["PartialEq", "Hash"]);
142 assert!(structs.dependencies.is_empty());
143 assert!(extra.enums.is_none());
144 assert!(extra.unions.is_none());
145 assert!(extra.response_structs.is_none());
146 }
147
148 #[test]
149 fn deserialize_extra_derives_all_kinds() {
150 let config = RustGeneratorConfig::from(table(
151 r#"
152 [extra_derives.structs]
153 derives = ["PartialEq"]
154 [extra_derives.enums]
155 derives = ["Hash"]
156 [extra_derives.unions]
157 derives = ["Clone"]
158 [extra_derives.response_structs]
159 derives = ["PartialEq", "Eq"]
160 "#,
161 ));
162 let extra = config.extra_derives.unwrap();
163 assert_eq!(extra.structs.unwrap().derives, vec!["PartialEq"]);
164 assert_eq!(extra.enums.unwrap().derives, vec!["Hash"]);
165 assert_eq!(extra.unions.unwrap().derives, vec!["Clone"]);
166 assert_eq!(
167 extra.response_structs.unwrap().derives,
168 vec!["PartialEq", "Eq"]
169 );
170 }
171
172 #[test]
173 fn deserialize_extra_derives_with_dependencies() {
174 let config = RustGeneratorConfig::from(table(
175 r#"
176 [extra_derives.structs]
177 derives = ["utoipa::ToSchema"]
178 [extra_derives.structs.dependencies]
179 utoipa = '{ version = "5", features = ["openapi_extensions"] }'
180 "#,
181 ));
182 let structs = config.extra_derives.unwrap().structs.unwrap();
183 assert_eq!(structs.derives, vec!["utoipa::ToSchema"]);
184 assert_eq!(
185 structs.dependencies.get("utoipa").unwrap(),
186 r#"{ version = "5", features = ["openapi_extensions"] }"#
187 );
188 }
189
190 #[test]
191 fn all_dependencies_merges_across_kinds() {
192 let extra = ExtraDerivesConfig {
193 structs: Some(ExtraDeriveConfig {
194 derives: vec!["utoipa::ToSchema".into()],
195 dependencies: BTreeMap::from([
196 ("utoipa".into(), r#"{ version = "5" }"#.into()),
197 ("shared".into(), "\"1.0\"".into()),
198 ]),
199 }),
200 enums: Some(ExtraDeriveConfig {
201 derives: vec!["strum::Display".into()],
202 dependencies: BTreeMap::from([
203 ("strum".into(), r#"{ version = "0.26" }"#.into()),
204 ("shared".into(), "\"2.0\"".into()),
205 ]),
206 }),
207 unions: None,
208 response_structs: None,
209 per_type: None,
210 };
211 let deps = extra.all_dependencies();
212 assert_eq!(deps.len(), 3);
213 assert_eq!(deps["shared"], "\"1.0\"");
214 assert!(deps.contains_key("utoipa"));
215 assert!(deps.contains_key("strum"));
216 }
217
218 #[test]
219 fn all_dependencies_empty_when_no_extra() {
220 let extra = ExtraDerivesConfig::default();
221 assert!(extra.all_dependencies().is_empty());
222 }
223
224 #[test]
225 fn invalid_config_falls_back_to_default() {
226 let mut t = toml::value::Table::new();
227 t.insert(
228 "extra_derives".into(),
229 toml::Value::String("invalid".into()),
230 );
231 let config = RustGeneratorConfig::from(t);
232 assert!(config.crate_name.is_none());
233 assert!(config.extra_derives.is_none());
234 }
235
236 #[test]
237 fn package_name_alias_resolves_to_crate_name() {
238 let config = RustGeneratorConfig::from(table(r#"package_name = "my-api""#));
239 assert_eq!(config.crate_name.as_deref(), Some("my-api"));
240 }
241
242 #[test]
243 fn workspace_deps_mode_explicit() {
244 let config = RustGeneratorConfig::from(table(r#"workspace_deps = "explicit""#));
245 assert_eq!(
246 config.workspace_deps,
247 Some(super::WorkspaceDepsMode::Explicit)
248 );
249 }
250
251 #[test]
252 fn workspace_deps_mode_workspace_version() {
253 let config = RustGeneratorConfig::from(table(r#"workspace_deps = "workspace_version""#));
254 assert_eq!(
255 config.workspace_deps,
256 Some(super::WorkspaceDepsMode::WorkspaceVersion)
257 );
258 }
259
260 #[test]
261 fn workspace_deps_mode_full() {
262 let config = RustGeneratorConfig::from(table(r#"workspace_deps = "full""#));
263 assert_eq!(config.workspace_deps, Some(super::WorkspaceDepsMode::Full));
264 }
265
266 #[test]
267 fn per_type_extra_derives() {
268 let config = RustGeneratorConfig::from(table(
269 r#"
270 [extra_derives.per_type.PaymentMethod]
271 derives = ["Hash", "PartialEq"]
272
273 [extra_derives.per_type.PaymentMethod.dependencies]
274 dummy = '"1.0"'
275 "#,
276 ));
277 let extra = config.extra_derives.unwrap();
278 let per_type = extra.per_type.unwrap();
279 let pm = per_type.get("PaymentMethod").unwrap();
280 assert_eq!(pm.derives, vec!["Hash", "PartialEq"]);
281 assert_eq!(pm.dependencies.get("dummy").unwrap(), "\"1.0\"");
282 }
283
284 #[test]
285 fn all_dependencies_includes_per_type() {
286 let extra = ExtraDerivesConfig {
287 structs: Some(ExtraDeriveConfig {
288 derives: vec!["utoipa::ToSchema".into()],
289 dependencies: BTreeMap::from([("utoipa".into(), r#"{ version = "5" }"#.into())]),
290 }),
291 enums: None,
292 unions: None,
293 response_structs: None,
294 per_type: Some(BTreeMap::from([(
295 "MyType".into(),
296 ExtraDeriveConfig {
297 derives: vec!["Hash".into()],
298 dependencies: BTreeMap::from([("custom".into(), "\"2.0\"".into())]),
299 },
300 )])),
301 };
302 let deps = extra.all_dependencies();
303 assert_eq!(deps.len(), 2);
304 assert!(deps.contains_key("utoipa"));
305 assert!(deps.contains_key("custom"));
306 }
307}