Something went wrong. Try again.
atproto made easy crates.io/crates/jacquard
atproto rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192use heck::ToPascalCase;use jacquard_common::CowStr;use proc_macro2::TokenStream;use quote::quote;
/// Rust keywords that need escaping with r# prefix in module pathsconst RUST_KEYWORDS: &[&str] = &[ "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", // Reserved keywords "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield", // 2018+ edition keywords "async", "await", "dyn",];
/// Check if a string is a Rust keyword#[inline]fn is_rust_keyword(s: &str) -> bool { RUST_KEYWORDS.contains(&s)}/// Convert a value string to a valid Rust variant namepub(super) fn value_to_variant_name(value: &str) -> String { // Remove leading special chars and convert to pascal case let clean = value.trim_start_matches(|c: char| !c.is_alphanumeric()); let variant = clean.replace('-', "_").to_pascal_case();
// Prefix with underscore if starts with digit if variant.chars().next().map_or(false, |c| c.is_ascii_digit()) { format!("_{}", variant) } else if variant.is_empty() { "Unknown".to_string() } else { variant }}
/// Convert a knownValues entry to a valid Rust variant name./// For NSID#fragment values (e.g., "tools.ozone.team.defs#roleAdmin"),/// extracts just the fragment part for the variant name./// For plain values (e.g., "create"), uses the whole value.pub(super) fn known_value_to_variant_name(value: &str) -> String { // If contains #, use just the fragment part for the variant name let name_part = if let Some(idx) = value.rfind('#') { &value[idx + 1..] } else { value }; value_to_variant_name(name_part)}
/// Check if a string is already a valid identifier (alphanumeric + underscore, not starting with digit)#[inline]fn is_valid_identifier(s: &str) -> bool { if s.is_empty() { return false; }
let mut chars = s.chars(); let first = chars.next().unwrap();
// Must start with letter or underscore if !first.is_ascii_alphabetic() && first != '_' { return false; }
// Rest must be alphanumeric or underscore chars.all(|c| c.is_ascii_alphanumeric() || c == '_')}
/// Sanitize a string to be safe for identifiers and filenames, returning CowStr./// Borrows if already valid, allocates if modifications needed.pub(super) fn sanitize_name_cow(s: &str) -> CowStr<'_> { if is_valid_identifier(s) { return CowStr::Borrowed(s); }
if s.is_empty() { return CowStr::Owned(jacquard_common::deps::smol_str::SmolStr::new_static( "unknown", )); }
// Replace invalid characters with underscores let mut sanitized: String = s .chars() .map(|c| { if c.is_alphanumeric() || c == '_' { c } else { '_' } }) .collect();
// Ensure it doesn't start with a digit if sanitized .chars() .next() .map_or(false, |c| c.is_ascii_digit()) { sanitized = format!("_{}", sanitized); }
CowStr::Owned(sanitized.into())}
/// Sanitize a string to be safe for identifiers and filenames, always returning String./// Convenience wrapper around sanitize_name_cow for existing callsites.pub(super) fn sanitize_name(s: &str) -> String { sanitize_name_cow(s).to_string()}
/// Build namespace prefix from first two NSID segments (e.g., "com", "atproto" → "com_atproto")pub(super) fn namespace_prefix(first: &str, second: &str) -> String { format!("{}_{}", sanitize_name_cow(first), sanitize_name_cow(second))}
/// Escape a Rust keyword with r# prefix for use in pathsfn escape_keyword_for_path(s: &str) -> std::borrow::Cow<'_, str> { // crate, self, super, and Self are valid in path contexts if is_rust_keyword(s) && !matches!(s, "crate" | "self" | "super" | "Self") { std::borrow::Cow::Owned(format!("r#{}", s)) } else { std::borrow::Cow::Borrowed(s) }}
/// Join NSID segments into a module path (e.g., ["repo", "admin"] → "repo::admin")pub(super) fn join_module_path(segments: &[&str]) -> String { segments .iter() .map(|s| { let sanitized = sanitize_name_cow(s); escape_keyword_for_path(&sanitized).into_owned() }) .collect::<Vec<_>>() .join("::")}
/// Join already-processed strings into a Rust module path (e.g., ["crate", "foo", "Bar"] → "crate::foo::Bar")pub(super) fn join_path_parts(parts: &[impl AsRef<str>]) -> String { parts .iter() .map(|p| escape_keyword_for_path(p.as_ref()).into_owned()) .collect::<Vec<_>>() .join("::")}
/// Create an identifier, using raw identifier if necessary for keywordspub fn make_ident(s: &str) -> syn::Ident { if s.is_empty() { eprintln!("Warning: Empty identifier encountered, using 'unknown' as fallback"); return syn::Ident::new("unknown", proc_macro2::Span::call_site()); }
let sanitized = sanitize_name(s);
// Try to parse as ident, fall back to raw ident if needed syn::parse_str::<syn::Ident>(&sanitized).unwrap_or_else(|_| { // only print if the sanitization actually changed the name // for types where the name is a keyword, will prepend 'r#' if s != sanitized { eprintln!( "Warning: Invalid identifier '{}' sanitized to '{}'", s, sanitized ); syn::Ident::new(&sanitized, proc_macro2::Span::call_site()) } else { syn::Ident::new_raw(&sanitized, proc_macro2::Span::call_site()) } })}
/// Generate doc comment from optional description.////// Multi-line descriptions are split into separate `#[doc]` attributes so that/// pretty-printers render them as `///` line comments rather than `/** ... */`/// block comments. Block comments with indented continuation lines cause rustdoc/// to interpret the indented text as a Markdown code block, which then gets/// compiled as a (broken) doctest.pub(super) fn generate_doc_comment(desc: Option<&CowStr>) -> TokenStream { if let Some(description) = desc { let lines: Vec<String> = description.lines().map(|line| format!(" {line}")).collect(); quote! { #(#[doc = #lines])* } } else { quote! {} }}