1use crate::{CodeGenerator, GeneratorError, Result, SchemaAnalysis, analysis::SchemaType};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet};
6use syn::{Attribute, Item, Meta, Path, Token, parse::Parser, punctuated::Punctuated};
7
8#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
15#[serde(default, deny_unknown_fields)]
16pub struct DerivesConfig {
17 pub string_enums: Vec<String>,
20 pub types: BTreeMap<String, Vec<String>>,
24}
25
26#[derive(Clone, Copy)]
27pub(crate) enum DeriveTarget {
28 StringEnum,
29 Model,
30 Alias,
31}
32
33fn validation(message: impl Into<String>) -> GeneratorError {
34 GeneratorError::ValidationError(message.into())
35}
36
37fn parse_path(value: &str) -> Result<Path> {
38 Path::parse_mod_style
39 .parse_str(value)
40 .map_err(|error| validation(format!("Invalid derive macro path `{value}`: {error}")))
41}
42
43fn identity(path: &Path) -> String {
46 let name = path
47 .segments
48 .iter()
49 .map(|segment| segment.ident.to_string())
50 .collect::<Vec<_>>()
51 .join("::");
52 match name.as_str() {
53 "Debug" | "std::fmt::Debug" | "core::fmt::Debug" => "Debug",
54 "Display" | "std::fmt::Display" | "core::fmt::Display" => "Display",
55 "Clone" | "std::clone::Clone" | "core::clone::Clone" => "Clone",
56 "Copy" | "std::marker::Copy" | "core::marker::Copy" => "Copy",
57 "Default" | "std::default::Default" | "core::default::Default" => "Default",
58 "PartialEq" | "std::cmp::PartialEq" | "core::cmp::PartialEq" => "PartialEq",
59 "Eq" | "std::cmp::Eq" | "core::cmp::Eq" => "Eq",
60 "PartialOrd" | "std::cmp::PartialOrd" | "core::cmp::PartialOrd" => "PartialOrd",
61 "Ord" | "std::cmp::Ord" | "core::cmp::Ord" => "Ord",
62 "Hash" | "std::hash::Hash" | "core::hash::Hash" => "Hash",
63 "Serialize" | "serde::Serialize" => "Serialize",
64 "Deserialize" | "serde::Deserialize" => "Deserialize",
65 "specta::Type" => "specta::Type",
66 _ if path.leading_colon.is_some() => return format!("::{name}"),
67 _ => return name,
68 }
69 .to_string()
70}
71
72fn collect_derives(meta: &Meta, existing: &mut BTreeSet<String>) -> syn::Result<()> {
73 if let Meta::List(list) = meta {
74 if list.path.is_ident("derive") {
75 let paths = list.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated)?;
76 existing.extend(paths.iter().map(identity));
77 } else if list.path.is_ident("cfg_attr") {
78 let nested = list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
79 for meta in nested.iter().skip(1) {
82 collect_derives(meta, existing)?;
83 }
84 }
85 }
86 Ok(())
87}
88
89impl DerivesConfig {
90 pub(crate) fn is_empty(&self) -> bool {
91 self.string_enums.is_empty() && self.types.is_empty()
92 }
93
94 pub(crate) fn validate_paths(&self) -> Result<()> {
95 for (scope, values) in std::iter::once(("string_enums".to_string(), &self.string_enums))
96 .chain(
97 self.types
98 .iter()
99 .map(|(name, values)| (format!("types.{name}"), values)),
100 )
101 {
102 for value in values {
103 parse_path(value)
104 .map_err(|error| validation(format!("generator.derives.{scope}: {error}")))?;
105 }
106 }
107 Ok(())
108 }
109
110 pub(crate) fn apply(
111 &self,
112 file: &mut syn::File,
113 targets: &BTreeMap<String, DeriveTarget>,
114 ) -> Result<()> {
115 if self.is_empty() {
116 return Ok(());
117 }
118 let mut manual: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
120 for item in &file.items {
121 if let Item::Impl(item) = item {
122 if let (Some((_, trait_path, _)), syn::Type::Path(self_type)) =
123 (&item.trait_, item.self_ty.as_ref())
124 {
125 if let Some(segment) = self_type.path.segments.last() {
126 manual
127 .entry(segment.ident.to_string())
128 .or_default()
129 .insert(identity(trait_path));
130 }
131 }
132 }
133 }
134 for item in &mut file.items {
135 let (name, attrs): (String, &mut Vec<Attribute>) = match item {
136 Item::Struct(item) => (item.ident.to_string(), &mut item.attrs),
137 Item::Enum(item) => (item.ident.to_string(), &mut item.attrs),
138 _ => continue,
139 };
140 let Some(target) = targets.get(&name) else {
141 continue;
142 };
143 let requested = self.types.get(&name).into_iter().flatten();
144 let grouped = self
145 .string_enums
146 .iter()
147 .filter(|_| matches!(target, DeriveTarget::StringEnum));
148 let mut existing = BTreeSet::new();
149 for attr in attrs.iter() {
150 collect_derives(&attr.meta, &mut existing).map_err(|error| {
151 GeneratorError::CodeGenError(format!(
152 "Cannot inspect derives on `{name}`: {error}"
153 ))
154 })?;
155 }
156 let mut additions = Vec::new();
157 for value in grouped.chain(requested) {
158 let path = parse_path(value)?;
159 let key = identity(&path);
160 if manual
161 .get(&name)
162 .is_some_and(|traits| traits.contains(&key))
163 {
164 return Err(validation(format!(
165 "generator.derives: cannot derive `{value}` on `{name}` because the generator already implements that trait manually"
166 )));
167 }
168 if existing.insert(key) {
169 additions.push(path);
170 }
171 }
172 if !additions.is_empty() {
173 attrs.push(syn::parse_quote!(#[derive(#(#additions),*)]));
174 }
175 }
176 Ok(())
177 }
178}
179
180impl CodeGenerator {
181 pub(crate) fn model_derive_targets(
182 &self,
183 analysis: &SchemaAnalysis,
184 ) -> BTreeMap<String, DeriveTarget> {
185 analysis
186 .schemas
187 .values()
188 .filter_map(|schema| {
189 let target = match &schema.schema_type {
190 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
191 DeriveTarget::StringEnum
192 }
193 SchemaType::Object { .. }
194 | SchemaType::Composition { .. }
195 | SchemaType::Union { .. }
196 | SchemaType::DiscriminatedUnion { .. } => DeriveTarget::Model,
197 SchemaType::Reference { target } if *target == schema.name => return None,
198 _ => DeriveTarget::Alias,
199 };
200 Some((self.to_rust_type_name(&schema.name), target))
201 })
202 .collect()
203 }
204
205 pub(crate) fn parameter_derive_targets(
206 &self,
207 operations: &[&crate::analysis::OperationInfo],
208 ) -> BTreeMap<String, DeriveTarget> {
209 operations
210 .iter()
211 .flat_map(|operation| &operation.parameters)
212 .filter(|parameter| parameter.enum_values.is_some())
213 .map(|parameter| (parameter.rust_type.clone(), DeriveTarget::StringEnum))
214 .collect()
215 }
216
217 pub(crate) fn validate_derives(&self, analysis: &SchemaAnalysis) -> Result<()> {
218 let config = &self.config().derives;
219 if config.is_empty() {
220 return Ok(());
221 }
222 config.validate_paths()?;
223 let models = self.model_derive_targets(analysis);
226 let operations: Vec<_> = analysis.operations.values().collect();
227 let parameters = self.parameter_derive_targets(&operations);
228 for name in config.types.keys() {
229 if analysis
230 .schemas
231 .values()
232 .filter(|schema| self.to_rust_type_name(&schema.name) == *name)
233 .count()
234 > 1
235 {
236 return Err(validation(format!(
237 "generator.derives.types: `{name}` is ambiguous between multiple model schemas"
238 )));
239 }
240 match (models.get(name), parameters.get(name)) {
241 (Some(_), Some(_)) => {
242 return Err(validation(format!(
243 "generator.derives.types: `{name}` is ambiguous between a model and an HTTP parameter enum"
244 )));
245 }
246 (Some(DeriveTarget::Alias), _) => {
247 return Err(validation(format!(
248 "generator.derives.types: `{name}` is a type alias; derives require a struct or enum"
249 )));
250 }
251 (None, None) => {
252 return Err(validation(format!(
253 "generator.derives.types: unknown generated type `{name}`; use the emitted Rust type name"
254 )));
255 }
256 _ => {}
257 }
258 }
259 Ok(())
260 }
261}