krabka_protocol_codegen/
resolve.rs1use std::collections::{HashMap, HashSet};
7
8use crate::{
9 ir::{FieldSpec, MessageSpec},
10 name_conv,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum StructKind {
15 Nested,
18 Common,
21}
22
23#[derive(Debug, Clone)]
24pub struct Resolution {
25 pub kind: StructKind,
26 pub rust_path: String,
28 pub needs_lifetime: bool,
32}
33
34#[derive(Debug, thiserror::Error)]
35pub enum ResolveError {
36 #[error("unresolved type reference `{type_name}` in message `{message}`")]
37 Unknown { message: String, type_name: String },
38}
39
40fn base_type(t: &str) -> &str {
41 t.strip_prefix("[]").unwrap_or(t)
42}
43
44fn is_struct_type(t: &str) -> bool {
45 t.chars().next().is_some_and(char::is_uppercase)
46}
47
48fn fields_need_lifetime(fields: &[FieldSpec]) -> bool {
56 fields.iter().any(|f| {
57 let base = base_type(&f.field_type);
58 matches!(base, "string" | "bytes" | "records")
59 || (!f.fields.is_empty() && fields_need_lifetime(&f.fields))
60 })
61}
62
63fn walk(fields: &[FieldSpec], map: &mut HashMap<String, Resolution>) {
65 for f in fields {
66 if !f.fields.is_empty() {
67 let type_name = base_type(&f.field_type);
68 let nl = fields_need_lifetime(&f.fields);
69 map.insert(
70 type_name.to_owned(),
71 Resolution {
72 kind: StructKind::Nested,
73 rust_path: type_name.to_owned(),
74 needs_lifetime: nl,
75 },
76 );
77 walk(&f.fields, map);
78 }
79 }
80}
81
82fn check(
84 fields: &[FieldSpec],
85 map: &HashMap<String, Resolution>,
86 message: &str,
87) -> Result<(), ResolveError> {
88 for f in fields {
89 let base = base_type(&f.field_type);
90 if is_struct_type(base) && !map.contains_key(base) {
91 return Err(ResolveError::Unknown {
92 message: message.to_owned(),
93 type_name: base.to_owned(),
94 });
95 }
96 check(&f.fields, map, message)?;
97 }
98 Ok(())
99}
100
101fn common_struct_needs_lifetime(
105 cs_fields: &[FieldSpec],
106 common_names_needing_lifetime: &HashSet<String>,
107) -> bool {
108 cs_fields.iter().any(|f| {
109 let base = base_type(&f.field_type);
110 matches!(base, "string" | "bytes" | "records")
111 || (!f.fields.is_empty()
112 && common_struct_needs_lifetime(&f.fields, common_names_needing_lifetime))
113 || (is_struct_type(base)
114 && f.fields.is_empty()
115 && common_names_needing_lifetime.contains(base))
116 })
117}
118
119pub fn resolve_message(spec: &MessageSpec) -> Result<HashMap<String, Resolution>, ResolveError> {
124 let mut map = HashMap::new();
125
126 let mut cs_needing_lt = HashSet::new();
131 let mut changed = true;
132 while changed {
133 changed = false;
134 for cs in &spec.common_structs {
135 if cs_needing_lt.contains(&cs.name) {
136 continue; }
138 if common_struct_needs_lifetime(&cs.fields, &cs_needing_lt) {
139 cs_needing_lt.insert(cs.name.clone());
140 changed = true;
141 }
142 }
143 }
144
145 let message_snake = name_conv::module_name(&spec.name);
156 for cs in &spec.common_structs {
157 let snake = name_conv::module_name(&cs.name);
158 map.insert(
159 cs.name.clone(),
160 Resolution {
161 kind: StructKind::Common,
162 rust_path: format!("super::common::{message_snake}::{snake}::{}", cs.name),
163 needs_lifetime: cs_needing_lt.contains(&cs.name),
164 },
165 );
166 }
167
168 walk(&spec.fields, &mut map);
169 check(&spec.fields, &map, &spec.name)?;
170
171 Ok(map)
172}
173
174#[cfg(test)]
175mod tests {
176 use std::path::PathBuf;
177
178 use assert2::assert;
179
180 use super::*;
181 use crate::ir;
182
183 fn load(name: &str) -> MessageSpec {
184 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
185 .parent()
186 .unwrap()
187 .join("protocol")
188 .join("schemas");
189 let specs = ir::load_dir(&dir).unwrap();
190 specs.into_iter().find(|s| s.name == name).unwrap()
191 }
192
193 #[test]
194 fn api_versions_request_has_no_nested_structs() {
195 let spec = load("ApiVersionsRequest");
196 let map = resolve_message(&spec).unwrap();
197 assert!(map.is_empty(), "found unexpected struct refs: {map:?}");
198 }
199
200 #[test]
201 fn metadata_request_resolves_topics() {
202 let spec = load("MetadataRequest");
203 let map = resolve_message(&spec).unwrap();
204 assert!(
206 map.contains_key("MetadataRequestTopic"),
207 "did not resolve MetadataRequestTopic: {map:?}"
208 );
209 }
210}