use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
use syn::{GenericArgument, PathArguments, Type, TypePath};
pub fn generate_imports() -> TokenStream2 {
quote! {
use mf_model::node_definition::NodeSpec;
use mf_model::schema::AttributeSpec;
use std::collections::HashMap;
use serde_json::Value as JsonValue;
}
}
pub fn is_option_type(ty: &Type) -> bool {
match ty {
Type::Path(TypePath { path, .. }) => {
if let Some(segment) = path.segments.last() {
segment.ident == "Option"
} else {
false
}
},
_ => false,
}
}
pub fn extract_option_inner_type(ty: &Type) -> Option<&Type> {
match ty {
Type::Path(TypePath { path, .. }) => {
let last_segment = path.segments.last()?;
if last_segment.ident != "Option" {
return None;
}
match &last_segment.arguments {
PathArguments::AngleBracketed(args) => {
args.args.first().and_then(|arg| match arg {
GenericArgument::Type(ty) => Some(ty),
_ => None,
})
},
_ => None,
}
},
_ => None,
}
}
pub fn generate_field_conversion(
field_name: &Ident,
field_type: &Type,
) -> TokenStream2 {
if is_option_type(field_type) {
quote! {
self.#field_name.as_ref()
.map(|v| serde_json::to_value(v).unwrap_or(JsonValue::Null))
.unwrap_or(JsonValue::Null)
}
} else {
quote! {
serde_json::to_value(&self.#field_name).unwrap_or(JsonValue::Null)
}
}
}
pub fn is_supported_basic_type(ty: &Type) -> bool {
const SUPPORTED_TYPES: &[&str] = &[
"String",
"str",
"&str",
"i32",
"i64",
"u32",
"u64",
"i8",
"i16",
"u8",
"u16",
"i128",
"u128",
"f32",
"f64",
"bool",
"usize",
"isize",
"serde_json::Value",
"Value",
"uuid::Uuid",
"Uuid",
"Vec<u8>",
"Vec<String>",
];
let type_str = quote! { #ty }.to_string().replace(" ", "");
SUPPORTED_TYPES.iter().any(|&supported| type_str == supported)
}
pub fn is_supported_type(ty: &Type) -> bool {
if is_option_type(ty) {
if let Some(inner_type) = extract_option_inner_type(ty) {
is_supported_basic_type(inner_type)
} else {
false
}
} else {
is_supported_basic_type(ty)
}
}
pub fn extract_type_name(ty: &Type) -> String {
match ty {
Type::Path(type_path) => {
let segments: Vec<String> = type_path
.path
.segments
.iter()
.map(|segment| {
let ident = &segment.ident;
match &segment.arguments {
PathArguments::AngleBracketed(args) => {
let args_str: Vec<String> = args
.args
.iter()
.map(|arg| match arg {
GenericArgument::Type(ty) => {
extract_type_name(ty)
},
_ => "?".to_string(),
})
.collect();
format!("{}<{}>", ident, args_str.join(", "))
},
_ => ident.to_string(),
}
})
.collect();
segments.last().cloned().unwrap_or_else(|| "Unknown".to_string())
},
_ => {
quote! { #ty }.to_string()
},
}
}
pub fn generate_attr_setter_code(
field_name: &Ident,
field_type: &Type,
target: &str,
) -> TokenStream2 {
let conversion = generate_field_conversion(field_name, field_type);
let field_name_str = field_name.to_string();
let target_ident = syn::parse_str::<Ident>(target)
.unwrap_or_else(|_| syn::parse_str("target").unwrap());
quote! {
#target_ident.set_attr(#field_name_str, Some(#conversion));
}
}
pub fn is_valid_identifier(identifier: &str) -> bool {
if identifier.is_empty() {
return false;
}
let first_char = identifier.chars().next().unwrap();
if !first_char.is_alphabetic() && first_char != '_' {
return false;
}
identifier.chars().all(|c| c.is_alphanumeric() || c == '_')
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_is_option_type() {
let option_string: Type = parse_quote! { Option<String> };
assert!(is_option_type(&option_string));
let string: Type = parse_quote! { String };
assert!(!is_option_type(&string));
let option_int: Type = parse_quote! { Option<i32> };
assert!(is_option_type(&option_int));
}
#[test]
fn test_extract_option_inner_type() {
let option_string: Type = parse_quote! { Option<String> };
let inner = extract_option_inner_type(&option_string);
assert!(inner.is_some());
let string: Type = parse_quote! { String };
let inner = extract_option_inner_type(&string);
assert!(inner.is_none());
}
#[test]
fn test_is_supported_type() {
let string: Type = parse_quote! { String };
assert!(is_supported_type(&string));
let option_string: Type = parse_quote! { Option<String> };
assert!(is_supported_type(&option_string));
let vec_string: Type = parse_quote! { Vec<String> };
assert!(!is_supported_type(&vec_string));
let i32_type: Type = parse_quote! { i32 };
assert!(is_supported_type(&i32_type));
let option_i32: Type = parse_quote! { Option<i32> };
assert!(is_supported_type(&option_i32));
}
#[test]
fn test_extract_type_name() {
let string: Type = parse_quote! { String };
assert_eq!(extract_type_name(&string), "String");
let option_string: Type = parse_quote! { Option<String> };
assert_eq!(extract_type_name(&option_string), "Option<String>");
let option_i32: Type = parse_quote! { Option<i32> };
assert_eq!(extract_type_name(&option_i32), "Option<i32>");
}
#[test]
fn test_is_valid_identifier() {
assert!(is_valid_identifier("valid_name"));
assert!(is_valid_identifier("ValidName"));
assert!(is_valid_identifier("valid123"));
assert!(is_valid_identifier("_private"));
assert!(!is_valid_identifier(""));
assert!(!is_valid_identifier("123invalid"));
assert!(!is_valid_identifier("invalid-name"));
assert!(!is_valid_identifier("invalid name"));
}
#[test]
fn test_generate_imports() {
let imports = generate_imports();
let imports_str = imports.to_string();
assert!(imports_str.contains("mf_model :: node_type :: NodeSpec"));
assert!(imports_str.contains("mf_model :: schema :: AttributeSpec"));
assert!(imports_str.contains("serde_json :: Value"));
assert!(imports_str.contains("std :: collections :: HashMap"));
}
#[test]
fn test_generate_field_conversion() {
let field_name = syn::parse_str::<Ident>("test_field").unwrap();
let string_type: Type = parse_quote! { String };
let conversion = generate_field_conversion(&field_name, &string_type);
let conversion_str = conversion.to_string();
assert!(conversion_str.contains("serde_json :: to_value"));
assert!(conversion_str.contains("test_field"));
let option_type: Type = parse_quote! { Option<String> };
let conversion = generate_field_conversion(&field_name, &option_type);
let conversion_str = conversion.to_string();
assert!(conversion_str.contains("as_ref"));
assert!(conversion_str.contains("unwrap_or"));
}
}