1use std::fmt::Write;
11
12use crate::path::ParsedPath;
13use crate::schema::Schema;
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum TypeStructure {
18 Plain,
20 Optional(Box<TypeStructure>),
22 Vector(Box<TypeStructure>),
24}
25
26pub fn analyze_type(ty: &syn::Type) -> TypeStructure {
33 if let Some(inner) = unwrap_option(ty) {
34 TypeStructure::Optional(Box::new(analyze_type(inner)))
35 } else if let Some(inner) = unwrap_vec(ty) {
36 TypeStructure::Vector(Box::new(analyze_type(inner)))
37 } else {
38 TypeStructure::Plain
39 }
40}
41
42fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
44 unwrap_type(ty, "Option")
45}
46
47fn unwrap_vec(ty: &syn::Type) -> Option<&syn::Type> {
49 unwrap_type(ty, "Vec")
50}
51
52fn unwrap_type<'a>(ty: &'a syn::Type, type_name: &str) -> Option<&'a syn::Type> {
54 let syn::Type::Path(type_path) = ungroup(ty) else {
55 return None;
56 };
57 let seg = type_path.path.segments.last()?;
58 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
59 return None;
60 };
61
62 if seg.ident == type_name
63 && args.args.len() == 1
64 && let syn::GenericArgument::Type(inner) = &args.args[0]
65 {
66 return Some(inner);
67 }
68 None
69}
70
71fn ungroup(mut ty: &syn::Type) -> &syn::Type {
79 while let syn::Type::Group(group) = ty {
80 ty = &group.elem;
81 }
82 ty
83}
84
85pub fn count_vec_depth(ts: &TypeStructure) -> usize {
87 match ts {
88 TypeStructure::Plain => 0,
89 TypeStructure::Optional(inner) => count_vec_depth(inner),
90 TypeStructure::Vector(inner) => 1 + count_vec_depth(inner),
91 }
92}
93
94const OBJECT_LIKE_SCALARS: &[&str] = &[
99 "JSON",
100 "MoveTypeLayout",
101 "MoveTypeSignature",
102 "OpenMoveTypeSignature",
103];
104
105pub fn validate_path_against_schema<'a>(
112 schema: &'a Schema,
113 root_type: &'a str,
114 path: &ParsedPath,
115 span: proc_macro2::Span,
116) -> Result<&'a str, syn::Error> {
117 let mut current_type: &str = root_type;
118
119 for segment in &path.segments {
120 let field = schema
121 .field(current_type, segment.field)
122 .ok_or_else(|| field_not_found_error(schema, current_type, segment.field, span))?;
123
124 if segment.is_list() && !field.is_list {
125 return Err(syn::Error::new(
126 span,
127 format!(
128 "Cannot use '[]' on non-list field '{}' (type '{}')",
129 segment.field, field.type_name
130 ),
131 ));
132 }
133
134 if !segment.is_list() && field.is_list {
135 return Err(syn::Error::new(
136 span,
137 format!(
138 "Field '{}' is a list type, use '{}[]' to iterate over it",
139 segment.field, segment.field
140 ),
141 ));
142 }
143
144 current_type = &field.type_name;
145 }
146
147 Ok(current_type)
148}
149
150pub fn validate_union_member(
152 schema: &Schema,
153 union_type: &str,
154 member_name: &str,
155 span: proc_macro2::Span,
156) -> Result<(), syn::Error> {
157 let union_types = schema.union_types(union_type);
158 if !union_types.contains(&member_name) {
159 let suggestion = find_similar(&union_types, member_name);
160
161 let mut msg = format!(
162 "'{}' is not a member of union '{}'. Members: {}",
163 member_name,
164 union_type,
165 union_types.join(", ")
166 );
167 if let Some(s) = suggestion {
168 msg.push_str(&format!(". Did you mean '{}'?", s));
169 }
170 return Err(syn::Error::new(span, msg));
171 }
172 Ok(())
173}
174
175pub fn is_object_like_scalar(type_name: &str) -> bool {
177 OBJECT_LIKE_SCALARS.contains(&type_name)
178}
179
180pub fn validate_type_matches_path(
192 path: &ParsedPath<'_>,
193 ty: &syn::Type,
194 skip_vec_excess_check: bool,
195) -> Result<(), syn::Error> {
196 let analyzed = analyze_type(ty);
197 let mut type_structure = &analyzed;
198
199 let mut peeled_optional_in_group = false;
200
201 for segment in &path.segments {
202 if segment.is_nullable && !peeled_optional_in_group {
204 let TypeStructure::Optional(inner) = type_structure else {
205 return Err(syn::Error::new_spanned(
206 ty,
207 format!(
208 "'{}' is marked nullable with '?' but type is not wrapped in Option<...>",
209 segment.field
210 ),
211 ));
212 };
213 type_structure = inner.as_ref();
214 peeled_optional_in_group = true;
215 }
216
217 if let Some(list) = &segment.list {
219 if !peeled_optional_in_group && matches!(type_structure, TypeStructure::Optional(_)) {
221 return Err(syn::Error::new_spanned(
222 ty,
223 format!(
224 "type is Option but no segment before '{}[]' has a '?' marker; \
225 add '?' to mark which segment is nullable",
226 segment.field
227 ),
228 ));
229 }
230
231 let TypeStructure::Vector(element_type) = type_structure else {
233 return Err(syn::Error::new_spanned(
234 ty,
235 format!(
236 "field '{}' is a list but type has no Vec wrapper for it",
237 segment.field
238 ),
239 ));
240 };
241 type_structure = element_type.as_ref();
242
243 if list.elements_nullable {
245 let TypeStructure::Optional(inner) = type_structure else {
246 return Err(syn::Error::new_spanned(
247 ty,
248 format!(
249 "'{}' has '[]?' but element type is not wrapped in Option<...>",
250 segment.field
251 ),
252 ));
253 };
254 type_structure = inner.as_ref();
255 }
256
257 peeled_optional_in_group = list.elements_nullable;
260 }
261 }
262
263 if !peeled_optional_in_group && matches!(type_structure, TypeStructure::Optional(_)) {
265 let last_field = path.segments.last().map(|s| s.field).unwrap_or(path.raw);
266 return Err(syn::Error::new_spanned(
267 ty,
268 format!(
269 "type is Option but no '?' found at or before '{}'; \
270 add '?' to mark which segments are nullable",
271 last_field
272 ),
273 ));
274 }
275
276 if !skip_vec_excess_check && count_vec_depth(type_structure) > 0 {
278 return Err(syn::Error::new_spanned(
279 ty,
280 format!(
281 "type has {} excess Vec wrapper(s) but path '{}' has no matching list field(s)",
282 count_vec_depth(type_structure),
283 path.raw
284 ),
285 ));
286 }
287
288 Ok(())
289}
290
291fn field_not_found_error(
293 schema: &Schema,
294 type_name: &str,
295 field_name: &str,
296 span: proc_macro2::Span,
297) -> syn::Error {
298 let available = schema.field_names(type_name);
299 let suggestion = find_similar(&available, field_name);
300
301 let mut msg = format!("Field '{field_name}' not found on type '{type_name}'");
302
303 if let Some(suggested) = suggestion {
304 write!(msg, ". Did you mean '{suggested}'?").unwrap();
305 } else if !available.is_empty() {
306 let mut fields: Vec<_> = available;
308 fields.sort();
309 write!(msg, ". Available fields: {}", fields.join(", ")).unwrap();
310 }
311
312 syn::Error::new(span, msg)
313}
314
315pub fn find_similar<'a>(candidates: &[&'a str], target: &str) -> Option<&'a str> {
317 candidates
318 .iter()
319 .filter_map(|&candidate| {
320 let distance = edit_distance::edit_distance(candidate, target);
321 if distance <= 3 {
323 Some((candidate, distance))
324 } else {
325 None
326 }
327 })
328 .min_by_key(|(_, d)| *d)
329 .map(|(c, _)| c)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_find_similar() {
338 let candidates = vec!["address", "version", "digest", "owner"];
339 assert_eq!(find_similar(&candidates, "addrss"), Some("address"));
340 assert_eq!(find_similar(&candidates, "vesion"), Some("version"));
341 assert_eq!(find_similar(&candidates, "xyz"), None);
342 }
343}