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}
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}