Something went wrong. Try again.
atproto made easy crates.io/crates/jacquard
atproto rust
Something went wrong. Try again.
38 kB · 915 lines
Rust
at bos-beta
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916use crate::corpus::LexiconCorpus;use crate::error::{CodegenError, Result};use crate::lexicon::{LexArrayItem, LexUserType};use proc_macro2::TokenStream;use quote::quote;
pub(crate) mod builder_gen;pub(crate) mod builder_heuristics;pub(crate) mod collect;pub(crate) mod names;pub(crate) mod output;pub(crate) mod prettify;pub(crate) mod schema_impl;pub(crate) mod structs;pub(crate) mod type_param;pub(crate) mod types;pub(crate) mod union_codegen;pub(crate) mod utils;pub(crate) mod xrpc;
// Re-export types that external consumers need (binaries, test crates).pub use prettify::{CodegenMode, FileOutput, GeneratedCode};
/// Code generator for lexicon typespub struct CodeGenerator<'c> { corpus: &'c LexiconCorpus, root_module: String, mode: prettify::CodegenMode, /// Track namespace dependencies (namespace -> set of namespaces it depends on) namespace_deps: std::cell::RefCell<std::collections::HashMap<String, std::collections::HashSet<String>>>, /// Track which file paths contain subscription endpoints subscription_files: std::cell::RefCell<std::collections::HashSet<std::path::PathBuf>>, /// Track which NSIDs have already generated their shared lexicon_doc function generated_shared_docs: std::cell::RefCell<std::collections::HashSet<String>>,}
impl<'c> CodeGenerator<'c> { /// Create a new code generator pub fn new(corpus: &'c LexiconCorpus, root_module: impl Into<String>) -> Self { Self { corpus, root_module: root_module.into(), mode: prettify::CodegenMode::Macro, namespace_deps: std::cell::RefCell::new(std::collections::HashMap::new()), subscription_files: std::cell::RefCell::new(std::collections::HashSet::new()), generated_shared_docs: std::cell::RefCell::new(std::collections::HashSet::new()), } }
/// Create a new code generator with a specific codegen mode pub fn with_mode( corpus: &'c LexiconCorpus, root_module: impl Into<String>, mode: prettify::CodegenMode, ) -> Self { Self { corpus, root_module: root_module.into(), mode, namespace_deps: std::cell::RefCell::new(std::collections::HashMap::new()), subscription_files: std::cell::RefCell::new(std::collections::HashSet::new()), generated_shared_docs: std::cell::RefCell::new(std::collections::HashSet::new()), } }
/// Generate doc comment from optional description (wrapper for utils function) fn generate_doc_comment(&self, desc: Option<&jacquard_common::CowStr>) -> TokenStream { utils::generate_doc_comment(desc) }
/// Create a ResolvedImports instance for this generator's mode with no collisions. /// Used in tests and as a fallback when per-file ResolvedImports isn't available. /// In Macro mode, this produces fully-qualified paths for all types. /// In Pretty mode, this produces short names (but should normally use per-file ResolvedImports from Phase 2). #[cfg(test)] pub(crate) fn default_resolved_imports(&self) -> prettify::ResolvedImports { prettify::ResolvedImports::resolve( &prettify::ImportSet::default(), &std::collections::HashSet::new(), &std::collections::HashSet::new(), self.mode, &std::collections::BTreeMap::new(), ) }
/// Track namespace dependency when a ref crosses namespace boundaries pub(crate) fn track_ref_namespace_dep(&self, current_nsid: &str, ref_str: &str) { use crate::ref_utils::{NsidPath, RefPath};
let current_path = NsidPath::parse(current_nsid); let ref_path = RefPath::parse(ref_str, None); let ref_nsid_path = NsidPath::parse(ref_path.nsid());
let current_ns = current_path.namespace(); let ref_ns = ref_nsid_path.namespace();
// Only track if crossing namespace boundaries if current_ns != ref_ns { self.namespace_deps .borrow_mut() .entry(current_ns) .or_default() .insert(ref_ns); } }
/// Generate or reference the shared lexicon_doc function for this NSID. /// Returns (optional shared function, trait impl tokens). pub(crate) fn generate_schema_impl_with_shared( &self, type_name: &str, nsid: &str, def_name: &str, has_lifetime: bool, resolved: &prettify::ResolvedImports, ) -> (Option<TokenStream>, TokenStream) { let lex_doc = self.corpus.get(nsid).expect("nsid exists in corpus");
// Generate shared function name from NSID (use sanitize_name for proper handling) let shared_fn_name = format!("lexicon_doc_{}", utils::sanitize_name(nsid)); let shared_fn_ident = syn::Ident::new(&shared_fn_name, proc_macro2::Span::call_site());
// Check if we need to generate the shared function let mut generated = self.generated_shared_docs.borrow_mut(); let shared_fn = if !generated.contains(nsid) { generated.insert(nsid.to_string()); // Codegen from JSON doesn't have union_fields (those are for Rust -> lexicon derive). use crate::derive_impl::doc_to_tokens::{DocPaths, doc_to_tokens_with_paths}; let doc_paths = match self.mode { prettify::CodegenMode::Pretty => DocPaths::short(), prettify::CodegenMode::Macro => DocPaths::qualified(), }; let scoped_imports = doc_paths.scoped_imports(); let doc_literal = doc_to_tokens_with_paths(lex_doc, &std::collections::BTreeMap::new(), &doc_paths); let lexicon_doc_path = resolved.external_type_tokens(&prettify::ExternalImport::LexiconDoc); Some(quote! { fn #shared_fn_ident() -> #lexicon_doc_path<'static> { #scoped_imports #doc_literal } }) } else { None };
// Generate lightweight trait impl that calls shared function. let type_ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); let bosstr_tok = resolved.external_type_tokens(&prettify::ExternalImport::BosStr); let (impl_generics, type_generics) = if has_lifetime { (quote! { <S: #bosstr_tok> }, quote! { <S> }) } else { (quote! {}, quote! {}) };
// Extract validation checks for this specific def. let validation_checks = schema_impl::extract_validation_checks(lex_doc, def_name); let validation_code = crate::derive_impl::doc_to_tokens::validations_to_tokens_resolved( &validation_checks, Some(resolved), );
let constraint_error_type = resolved.external_type_tokens(&prettify::ExternalImport::ConstraintError); let schema_path = resolved.external_type_tokens(&prettify::ExternalImport::LexiconSchema); let lexicon_doc_path = resolved.external_type_tokens(&prettify::ExternalImport::LexiconDoc);
let trait_impl = quote! { impl #impl_generics #schema_path for #type_ident #type_generics { fn nsid() -> &'static str { #nsid }
fn def_name() -> &'static str { #def_name }
fn lexicon_doc() -> #lexicon_doc_path<'static> { #shared_fn_ident() }
fn validate(&self) -> Result<(), #constraint_error_type> { #validation_code } } };
(shared_fn, trait_impl) }
/// Generate code for a lexicon def pub fn generate_def( &self, nsid: &str, def_name: &str, def: &LexUserType<'static>, resolved: &prettify::ResolvedImports, ) -> Result<GeneratedCode> { match def { LexUserType::Record(record) => self.generate_record(nsid, def_name, record, resolved), LexUserType::Object(obj) => self.generate_object(nsid, def_name, obj, resolved), LexUserType::XrpcQuery(query) => self.generate_query(nsid, def_name, query, resolved), LexUserType::XrpcProcedure(proc) => { self.generate_procedure(nsid, def_name, proc, resolved) } LexUserType::Token(token) => { // Token types are marker structs that can be used as union refs let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); let doc = self.generate_doc_comment(token.description.as_ref());
// Token name for Display impl (just the def name, not the full ref) let token_name = def_name;
let derive_attr = resolved.derive_standard_with(quote! { Hash }); let tokens = quote! { #doc #derive_attr pub struct #ident;
impl core::fmt::Display for #ident { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, #token_name) } } }; Ok(GeneratedCode::type_only(tokens)) } LexUserType::String(s) if s.known_values.is_some() => { self.generate_known_values_enum(nsid, def_name, s, resolved) } LexUserType::String(s) => { // Plain string type alias let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); let rust_type = self.string_to_rust_type(s, resolved); let doc = self.generate_doc_comment(s.description.as_ref()); let needs_param = self.string_needs_type_param(s); let default_str_path = resolved.external_type_tokens(&prettify::ExternalImport::DefaultStr); let tokens = if needs_param { quote! { #doc pub type #ident<S = #default_str_path> = #rust_type; } } else { quote! { #doc pub type #ident = #rust_type; } }; Ok(GeneratedCode::type_only(tokens)) } LexUserType::Integer(i) if i.r#enum.is_some() => { self.generate_integer_enum(nsid, def_name, i) } LexUserType::Array(array) => { // Top-level array becomes type alias to Vec<ItemType> let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); let doc = self.generate_doc_comment(array.description.as_ref()); let needs_lifetime = self.array_item_needs_type_param(&array.items);
// Check if items are a union - if so, generate the union enum first if let LexArrayItem::Union(union) = &array.items { let union_name = format!("{}Item", type_name); let refs: Vec<_> = union.refs.iter().cloned().collect(); let union_generated = self.generate_union( nsid, &union_name, &refs, None, union.closed, resolved, )?;
let union_ident = syn::Ident::new(&union_name, proc_macro2::Span::call_site()); let union_tokens = union_generated.into_tokens(); let default_str_path = resolved.external_type_tokens(&prettify::ExternalImport::DefaultStr); let type_alias = if needs_lifetime { quote! { #doc pub type #ident<S = #default_str_path> = Vec<#union_ident<S>>; } } else { quote! { #doc pub type #ident = Vec<#union_ident>; } };
let type_defs = quote! { #union_tokens
#type_alias }; Ok(GeneratedCode::type_only(type_defs)) } else { // Regular array item type let item_type = self.array_item_to_rust_type(nsid, &array.items, resolved)?; let default_str_path = resolved.external_type_tokens(&prettify::ExternalImport::DefaultStr); let tokens = if needs_lifetime { quote! { #doc pub type #ident<S = #default_str_path> = Vec<#item_type>; } } else { quote! { #doc pub type #ident = Vec<#item_type>; } }; Ok(GeneratedCode::type_only(tokens)) } } LexUserType::Boolean(_) | LexUserType::Integer(_) | LexUserType::Bytes(_) | LexUserType::CidLink(_) | LexUserType::Unknown(_) => { // These are rarely top-level defs, but if they are, make type aliases let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); let (rust_type, needs_lifetime) = match def { LexUserType::Boolean(_) => (quote! { bool }, false), LexUserType::Integer(_) => (quote! { i64 }, false), LexUserType::Bytes(_) => ( resolved.external_type_tokens(&prettify::ExternalImport::Bytes), false, ), LexUserType::CidLink(_) => { (resolved.type_tokens(&prettify::CommonType::CidLink), true) } LexUserType::Unknown(_) => { (resolved.type_tokens(&prettify::CommonType::Data), true) } _ => unreachable!(), }; let default_str_path = resolved.external_type_tokens(&prettify::ExternalImport::DefaultStr); let tokens = if needs_lifetime { quote! { pub type #ident<S = #default_str_path> = #rust_type; } } else { quote! { pub type #ident = #rust_type; } }; Ok(GeneratedCode::type_only(tokens)) } LexUserType::Blob(_) => Err(CodegenError::unsupported( format!("top-level def type {:?}", def), nsid, None::<String>, )), LexUserType::XrpcSubscription(sub) => { // Track this file as containing a subscription let file_path = self.nsid_to_file_path(nsid); self.subscription_files.borrow_mut().insert(file_path); self.generate_subscription(nsid, def_name, sub, resolved) } LexUserType::Union(union) => { // Top-level union generates an enum let type_name = self.def_to_type_name(nsid, def_name); let refs: Vec<_> = union.refs.iter().cloned().collect(); // Union already returns GeneratedCode, so just forward it self.generate_union( nsid, &type_name, &refs, union.description.as_ref().map(|d| d.as_ref()), union.closed, resolved, ) } LexUserType::PermissionSet(_perm_set) => { // Permission sets are consumed at runtime by the permission set // resolver (Phase 5), not generated as code. Ok(GeneratedCode::default()) } } }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_generate_record() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus.get("app.bsky.feed.post").expect("get post"); let def = doc.defs.get("main").expect("get main def");
let resolved = codegen.default_resolved_imports(); let generated = codegen .generate_def("app.bsky.feed.post", "main", def, &resolved) .expect("generate"); let tokens = generated.into_tokens();
// Format and print for inspection let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check basic structure assert!(formatted.contains("struct Post")); assert!(formatted.contains("pub text")); // String fields use the bare type parameter S (CowStr becomes S in BOS mode). assert!( formatted.contains(": S,"), "expected bare S for string fields, got:\n{}", formatted ); }
#[test] fn test_generate_union() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
// Create a union with embed types let refs = vec![ "app.bsky.embed.images".into(), "app.bsky.embed.video".into(), "app.bsky.embed.external".into(), ];
let resolved = codegen.default_resolved_imports(); let generated = codegen .generate_union( "app.bsky.feed.post", "RecordEmbed", &refs, Some("Post embed union"), None, &resolved, ) .expect("generate union"); let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check structure assert!(formatted.contains("enum RecordEmbed")); assert!(formatted.contains("Images")); assert!(formatted.contains("Video")); assert!(formatted.contains("External")); assert!(formatted.contains("tag = \"$type\"")); assert!(formatted.contains("#[jacquard_derive::open_union]")); }
#[test] fn test_generate_query() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("app.bsky.feed.getAuthorFeed") .expect("get getAuthorFeed"); let def = doc.defs.get("main").expect("get main def");
let resolved = codegen.default_resolved_imports(); let generated = codegen .generate_def("app.bsky.feed.getAuthorFeed", "main", def, &resolved) .expect("generate"); let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check structure assert!(formatted.contains("struct GetAuthorFeed")); assert!(formatted.contains("struct GetAuthorFeedOutput")); assert!(formatted.contains("enum GetAuthorFeedError")); assert!(formatted.contains("pub actor")); assert!(formatted.contains("pub limit")); assert!(formatted.contains("pub cursor")); assert!(formatted.contains("pub feed")); assert!(formatted.contains("BlockedActor")); assert!(formatted.contains("BlockedByActor")); }
#[test] fn test_generate_known_values_enum() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("com.atproto.label.defs") .expect("get label defs"); let def = doc.defs.get("labelValue").expect("get labelValue def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("com.atproto.label.defs", "labelValue", def, &resolved) .expect("generate"); let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check structure assert!(formatted.contains("enum LabelValue")); assert!(formatted.contains("Hide")); assert!(formatted.contains("NoPromote")); assert!(formatted.contains("Warn")); assert!(formatted.contains("DmcaViolation")); // The catch-all variant uses the bare S type parameter. assert!( formatted.contains("Other(S)"), "expected Other(S) variant, got:\n{}", formatted ); // from_value replaces the old From<&str> impl. assert!( formatted.contains("fn from_value"), "expected from_value method, got:\n{}", formatted ); assert!(formatted.contains("fn as_str(&self)")); }
#[test] fn test_nsid_to_file_path() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
// Regular paths assert_eq!( codegen.nsid_to_file_path("app.bsky.feed.post"), std::path::PathBuf::from("app_bsky/feed/post.rs") );
assert_eq!( codegen.nsid_to_file_path("app.bsky.feed.getAuthorFeed"), std::path::PathBuf::from("app_bsky/feed/get_author_feed.rs") );
// Defs paths - should go in parent assert_eq!( codegen.nsid_to_file_path("com.atproto.label.defs"), std::path::PathBuf::from("com_atproto/label.rs") ); }
#[test] fn test_write_to_disk() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "test_generated");
let tmp_dir = tempfile::tempdir().expect("should be able to create temp directory for output"); let output_dir = std::path::PathBuf::from(tmp_dir.path());
// Clean up any previous test output let _ = std::fs::remove_dir_all(&output_dir);
// Generate and write codegen.write_to_disk(&output_dir).expect("write to disk");
// Verify some files were created assert!(output_dir.join("app_bsky/feed/post.rs").exists()); assert!(output_dir.join("app_bsky/feed/get_author_feed.rs").exists()); assert!(output_dir.join("com_atproto/label.rs").exists());
// Verify module files were created assert!(output_dir.join("lib.rs").exists()); assert!(output_dir.join("app_bsky.rs").exists());
// Read and verify post.rs contains expected content let post_content = std::fs::read_to_string(output_dir.join("app_bsky/feed/post.rs")) .expect("read post.rs"); assert!(post_content.contains("pub struct Post")); assert!(post_content.contains("jacquard_common")); }
#[test] fn test_generate_procedure() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("com.atproto.repo.createRecord") .expect("get createRecord"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("com.atproto.repo.createRecord", "main", def, &resolved) .expect("generate"); let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check structure - procedures have input, output, and error types assert!(formatted.contains("struct CreateRecord")); assert!(formatted.contains("struct CreateRecordOutput")); assert!(formatted.contains("enum CreateRecordError")); // Check input fields assert!(formatted.contains("pub repo")); assert!(formatted.contains("pub collection")); assert!(formatted.contains("pub record")); // Check output fields assert!(formatted.contains("pub uri")); assert!(formatted.contains("pub cid")); // Check error variants assert!(formatted.contains("InvalidSwap")); assert!(formatted.contains("InvalidRecord")); }
#[test] fn test_generate_subscription() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("com.atproto.sync.subscribeRepos") .expect("get subscribeRepos"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("com.atproto.sync.subscribeRepos", "main", def, &resolved) .expect("generate"); let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check subscription structure assert!(formatted.contains("struct SubscribeRepos")); assert!(formatted.contains("enum SubscribeReposMessage")); // Check message union variants assert!(formatted.contains("Commit")); assert!(formatted.contains("Identity")); assert!(formatted.contains("Account")); }
// #[test] // fn test_generate_token_type() { // let corpus = // LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); // let codegen = CodeGenerator::new(&corpus, "jacquard_api");
// let doc = corpus.get("app.bsky.embed.images").expect("get images"); // let def = doc.defs.get("viewImage").expect("get viewImage def");
// let tokens = codegen // .generate_def("app.bsky.embed.images", "viewImage", def, &resolved) // .expect("generate");
// let file: syn::File = syn::parse2(tokens).expect("parse tokens"); // let formatted = prettyplease::unparse(&file); // println!("\n{}\n", formatted);
// // Token types are unit structs // assert!(formatted.contains("struct ViewImage")); // // Should have Display implementation // assert!(formatted.contains("impl std::fmt::Display")); // }
#[test] fn test_generate_array_types() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus.get("test.array.types").expect("get array types"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("test.array.types", "main", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check different array item types assert!(formatted.contains("simple_strings")); assert!(formatted.contains("Vec<")); // Union array items should generate enum assert!(formatted.contains("union_items")); // Ref array items assert!(formatted.contains("ref_items")); // CID link arrays assert!(formatted.contains("cid_links")); }
#[test] fn test_generate_binary_types() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus.get("test.binary.types").expect("get binary types"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("test.binary.types", "main", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check binary field types assert!(formatted.contains("pub cid")); assert!(formatted.contains("CidLink") || formatted.contains("types::cid")); assert!(formatted.contains("pub data")); assert!(formatted.contains("Bytes")); assert!(formatted.contains("pub avatar")); assert!(formatted.contains("BlobRef") || formatted.contains("types::blob")); }
#[test] fn test_generate_empty_object() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus.get("test.empty.object").expect("get empty object"); let def = doc.defs.get("emptyDef").expect("get emptyDef"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("test.empty.object", "emptyDef", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Empty objects should generate type alias to Data<S>. assert!(formatted.contains("type EmptyDef") || formatted.contains("Data<S>")); }
#[test] fn test_generate_multi_def_lexicon() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("pub.leaflet.poll.definition") .expect("get poll definition");
// Test main def let main_def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports(); let main_generated = codegen .generate_def("pub.leaflet.poll.definition", "main", main_def, &resolved) .expect("generate main"); let main_tokens = main_generated.into_tokens(); let main_file: syn::File = syn::parse2(main_tokens).expect("parse main tokens"); let main_formatted = prettyplease::unparse(&main_file); println!("\nMain:\n{}\n", main_formatted); assert!(main_formatted.contains("struct Definition")); assert!(main_formatted.contains("pub question")); assert!(main_formatted.contains("pub options"));
// Test option fragment let option_def = doc.defs.get("option").expect("get option def"); let option_generated = codegen .generate_def( "pub.leaflet.poll.definition", "option", option_def, &resolved, ) .expect("generate option"); let option_tokens = option_generated.into_tokens(); let option_file: syn::File = syn::parse2(option_tokens).expect("parse option tokens"); let option_formatted = prettyplease::unparse(&option_file); println!("\nOption:\n{}\n", option_formatted); assert!(option_formatted.contains("struct DefinitionOption")); assert!(option_formatted.contains("pub text"));
// Test vote fragment let vote_def = doc.defs.get("vote").expect("get vote def"); let vote_generated = codegen .generate_def("pub.leaflet.poll.definition", "vote", vote_def, &resolved) .expect("generate vote"); let vote_tokens = vote_generated.into_tokens(); let vote_file: syn::File = syn::parse2(vote_tokens).expect("parse vote tokens"); let vote_formatted = prettyplease::unparse(&vote_file); println!("\nVote:\n{}\n", vote_formatted); assert!( vote_formatted.contains("struct DefinitionVote") || vote_formatted.contains("struct Vote") ); assert!(vote_formatted.contains("pub poll_ref")); assert!(vote_formatted.contains("pub option_index")); }
#[test] fn test_generate_with_constraints_and_defaults() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("test.constraints.validation") .expect("get constraints"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("test.constraints.validation", "main", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Check fields with constraints are generated assert!(formatted.contains("pub username")); assert!(formatted.contains("pub bio")); assert!(formatted.contains("pub age")); assert!(formatted.contains("pub enabled")); assert!(formatted.contains("pub tags")); assert!(formatted.contains("pub role"));
// Constraints should be in docs or validation metadata // (exact format depends on codegen implementation) }
#[test] fn test_local_refs_in_definitions() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus .get("pub.leaflet.poll.definition") .expect("get poll definition"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("pub.leaflet.poll.definition", "main", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Local ref #option should resolve to DefinitionOption type (fully qualified or local) assert!( formatted.contains("Vec<DefinitionOption") || formatted .contains("Vec<jacquard_api::pub_leaflet::poll::definition::DefinitionOption") ); }
#[test] fn test_nullable_optional_properties() { let corpus = LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); let codegen = CodeGenerator::new(&corpus, "jacquard_api");
let doc = corpus.get("test.binary.types").expect("get binary types"); let def = doc.defs.get("main").expect("get main def"); let resolved = codegen.default_resolved_imports();
let generated = codegen .generate_def("test.binary.types", "main", def, &resolved) .expect("generate");
let tokens = generated.into_tokens();
let file: syn::File = syn::parse2(tokens).expect("parse tokens"); let formatted = prettyplease::unparse(&file); println!("\n{}\n", formatted);
// Optional fields should use Option<T> assert!(formatted.contains("optional_cid")); assert!(formatted.contains("Option<")); }}