use std::fmt::Write;
use crate::path::ParsedPath;
use crate::schema::Schema;
#[derive(Debug, Clone, PartialEq)]
pub enum TypeStructure {
Plain,
Optional(Box<TypeStructure>),
Vector(Box<TypeStructure>),
}
pub fn analyze_type(ty: &syn::Type) -> TypeStructure {
if let Some(inner) = unwrap_option(ty) {
TypeStructure::Optional(Box::new(analyze_type(inner)))
} else if let Some(inner) = unwrap_vec(ty) {
TypeStructure::Vector(Box::new(analyze_type(inner)))
} else {
TypeStructure::Plain
}
}
fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
unwrap_type(ty, "Option")
}
fn unwrap_vec(ty: &syn::Type) -> Option<&syn::Type> {
unwrap_type(ty, "Vec")
}
fn unwrap_type<'a>(ty: &'a syn::Type, type_name: &str) -> Option<&'a syn::Type> {
let syn::Type::Path(type_path) = ungroup(ty) else {
return None;
};
let seg = type_path.path.segments.last()?;
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
if seg.ident == type_name
&& args.args.len() == 1
&& let syn::GenericArgument::Type(inner) = &args.args[0]
{
return Some(inner);
}
None
}
fn ungroup(mut ty: &syn::Type) -> &syn::Type {
while let syn::Type::Group(group) = ty {
ty = &group.elem;
}
ty
}
pub fn count_vec_depth(ts: &TypeStructure) -> usize {
match ts {
TypeStructure::Plain => 0,
TypeStructure::Optional(inner) => count_vec_depth(inner),
TypeStructure::Vector(inner) => 1 + count_vec_depth(inner),
}
}
const OBJECT_LIKE_SCALARS: &[&str] = &[
"JSON",
"MoveTypeLayout",
"MoveTypeSignature",
"OpenMoveTypeSignature",
];
pub fn validate_path_against_schema<'a>(
schema: &'a Schema,
root_type: &'a str,
path: &ParsedPath,
span: proc_macro2::Span,
) -> Result<&'a str, syn::Error> {
let mut current_type: &str = root_type;
for segment in &path.segments {
let field = schema
.field(current_type, segment.field)
.ok_or_else(|| field_not_found_error(schema, current_type, segment.field, span))?;
if segment.is_list() && !field.is_list {
return Err(syn::Error::new(
span,
format!(
"Cannot use '[]' on non-list field '{}' (type '{}')",
segment.field, field.type_name
),
));
}
if !segment.is_list() && field.is_list {
return Err(syn::Error::new(
span,
format!(
"Field '{}' is a list type, use '{}[]' to iterate over it",
segment.field, segment.field
),
));
}
current_type = &field.type_name;
}
Ok(current_type)
}
pub fn validate_union_member(
schema: &Schema,
union_type: &str,
member_name: &str,
span: proc_macro2::Span,
) -> Result<(), syn::Error> {
let union_types = schema.union_types(union_type);
if !union_types.contains(&member_name) {
let suggestion = find_similar(&union_types, member_name);
let mut msg = format!(
"'{}' is not a member of union '{}'. Members: {}",
member_name,
union_type,
union_types.join(", ")
);
if let Some(s) = suggestion {
msg.push_str(&format!(". Did you mean '{}'?", s));
}
return Err(syn::Error::new(span, msg));
}
Ok(())
}
pub fn is_object_like_scalar(type_name: &str) -> bool {
OBJECT_LIKE_SCALARS.contains(&type_name)
}
pub fn validate_type_matches_path(
path: &ParsedPath<'_>,
ty: &syn::Type,
skip_vec_excess_check: bool,
) -> Result<(), syn::Error> {
let analyzed = analyze_type(ty);
let mut type_structure = &analyzed;
let mut peeled_optional_in_group = false;
for segment in &path.segments {
if segment.is_nullable && !peeled_optional_in_group {
let TypeStructure::Optional(inner) = type_structure else {
return Err(syn::Error::new_spanned(
ty,
format!(
"'{}' is marked nullable with '?' but type is not wrapped in Option<...>",
segment.field
),
));
};
type_structure = inner.as_ref();
peeled_optional_in_group = true;
}
if let Some(list) = &segment.list {
if !peeled_optional_in_group && matches!(type_structure, TypeStructure::Optional(_)) {
return Err(syn::Error::new_spanned(
ty,
format!(
"type is Option but no segment before '{}[]' has a '?' marker; \
add '?' to mark which segment is nullable",
segment.field
),
));
}
let TypeStructure::Vector(element_type) = type_structure else {
return Err(syn::Error::new_spanned(
ty,
format!(
"field '{}' is a list but type has no Vec wrapper for it",
segment.field
),
));
};
type_structure = element_type.as_ref();
if list.elements_nullable {
let TypeStructure::Optional(inner) = type_structure else {
return Err(syn::Error::new_spanned(
ty,
format!(
"'{}' has '[]?' but element type is not wrapped in Option<...>",
segment.field
),
));
};
type_structure = inner.as_ref();
}
peeled_optional_in_group = list.elements_nullable;
}
}
if !peeled_optional_in_group && matches!(type_structure, TypeStructure::Optional(_)) {
let last_field = path.segments.last().map(|s| s.field).unwrap_or(path.raw);
return Err(syn::Error::new_spanned(
ty,
format!(
"type is Option but no '?' found at or before '{}'; \
add '?' to mark which segments are nullable",
last_field
),
));
}
if !skip_vec_excess_check && count_vec_depth(type_structure) > 0 {
return Err(syn::Error::new_spanned(
ty,
format!(
"type has {} excess Vec wrapper(s) but path '{}' has no matching list field(s)",
count_vec_depth(type_structure),
path.raw
),
));
}
Ok(())
}
fn field_not_found_error(
schema: &Schema,
type_name: &str,
field_name: &str,
span: proc_macro2::Span,
) -> syn::Error {
let available = schema.field_names(type_name);
let suggestion = find_similar(&available, field_name);
let mut msg = format!("Field '{field_name}' not found on type '{type_name}'");
if let Some(suggested) = suggestion {
write!(msg, ". Did you mean '{suggested}'?").unwrap();
} else if !available.is_empty() {
let mut fields: Vec<_> = available;
fields.sort();
write!(msg, ". Available fields: {}", fields.join(", ")).unwrap();
}
syn::Error::new(span, msg)
}
pub fn find_similar<'a>(candidates: &[&'a str], target: &str) -> Option<&'a str> {
candidates
.iter()
.filter_map(|&candidate| {
let distance = edit_distance::edit_distance(candidate, target);
if distance <= 3 {
Some((candidate, distance))
} else {
None
}
})
.min_by_key(|(_, d)| *d)
.map(|(c, _)| c)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_similar() {
let candidates = vec!["address", "version", "digest", "owner"];
assert_eq!(find_similar(&candidates, "addrss"), Some("address"));
assert_eq!(find_similar(&candidates, "vesion"), Some("version"));
assert_eq!(find_similar(&candidates, "xyz"), None);
}
}