···11-#!/usr/bin/env cargo
22-//! Example: Discover schemas across the workspace without link-time discovery
33-//!
44-//! Run with: cargo run --example workspace_discovery
55-66-use jacquard_lexgen::schema_discovery::WorkspaceDiscovery;
77-88-fn main() -> miette::Result<()> {
99- println!("Workspace Schema Discovery Example\n");
1010-1111- // Create workspace discovery
1212- let discovery = WorkspaceDiscovery::new().verbose(true);
1313-1414- // Scan workspace
1515- let schemas = discovery.scan()?;
1616-1717- println!("\n━━━ Results ━━━");
1818- println!("Discovered {} schema types:\n", schemas.len());
1919-2020- // Group by crate
2121- use std::collections::HashMap;
2222- let mut by_crate: HashMap<String, Vec<_>> = HashMap::new();
2323-2424- for schema in &schemas {
2525- let crate_name = schema
2626- .source_path
2727- .components()
2828- .find_map(|c| {
2929- let s = c.as_os_str().to_str()?;
3030- if s.starts_with("jacquard-") || s == "jacquard" {
3131- Some(s.to_string())
3232- } else {
3333- None
3434- }
3535- })
3636- .unwrap_or_else(|| "unknown".to_string());
3737-3838- by_crate.entry(crate_name).or_default().push(schema);
3939- }
4040-4141- for (crate_name, crate_schemas) in by_crate {
4242- println!("📦 {} ({} schemas)", crate_name, crate_schemas.len());
4343- for schema in crate_schemas {
4444- println!(" • {} ({})", schema.nsid, schema.type_name);
4545- }
4646- println!();
4747- }
4848-4949- Ok(())
5050-}
+16-47
crates/jacquard-lexgen/src/bin/extract_schemas.rs
···11-//! Extract AT Protocol lexicon schemas from compiled Rust types
11+//! Extract AT Protocol lexicon schemas via workspace discovery
22//!
33-//! This binary discovers types with `#[derive(LexiconSchema)]` via inventory
44-//! and generates lexicon JSON files. See the `schema_extraction` module docs
55-//! for usage patterns and integration examples.
33+//! This binary scans the workspace for types with `#[derive(LexiconSchema)]`
44+//! and generates lexicon JSON files. Unlike inventory-based extraction, this
55+//! discovers schemas across the entire workspace without requiring linking.
6677use clap::Parser;
88-use jacquard_lexgen::schema_extraction::{self, ExtractOptions, SchemaExtractor};
88+use jacquard_lexgen::schema_discovery::WorkspaceDiscovery;
99use miette::Result;
10101111-/// Extract lexicon schemas from compiled Rust types
1111+/// Extract lexicon schemas from workspace source files
1212#[derive(Parser, Debug)]
1313#[command(name = "extract-schemas")]
1414-#[command(about = "Extract AT Protocol lexicon schemas from Rust types")]
1414+#[command(about = "Extract AT Protocol lexicon schemas from workspace")]
1515#[command(long_about = r#"
1616-Discovers types implementing LexiconSchema via inventory and generates
1717-lexicon JSON files. The binary only discovers types that are linked,
1818-so you need to import your schema types in this binary or a custom one.
1616+Scans workspace source files for types with #[derive(LexiconSchema)] and
1717+generates lexicon JSON files. This discovers all schemas in the workspace
1818+without requiring types to be linked into the binary.
19192020-See: https://docs.rs/jacquard-lexgen/latest/jacquard_lexgen/schema_extraction/
2020+For inventory-based extraction (link-time discovery), see the extract_inventory example.
2121+2222+See: https://docs.rs/jacquard-lexgen/latest/jacquard_lexgen/schema_discovery/
2123"#)]
2224struct Args {
2325 /// Output directory for generated schema files
···2729 /// Verbose output
2830 #[arg(short, long)]
2931 verbose: bool,
3030-3131- /// Filter by NSID prefix (e.g., "app.bsky")
3232- #[arg(short, long)]
3333- filter: Option<String>,
3434-3535- /// Validate schemas before writing
3636- #[arg(short = 'V', long, default_value = "true")]
3737- validate: bool,
3838-3939- /// Pretty-print JSON output
4040- #[arg(short, long, default_value = "true")]
4141- pretty: bool,
4242-4343- /// Watch mode - regenerate on changes
4444- #[arg(short, long)]
4545- watch: bool,
4632}
47334834fn main() -> Result<()> {
4935 let args = Args::parse();
50365151- // Simple case: use convenience function
5252- if !args.watch && args.filter.is_none() && args.validate && args.pretty {
5353- return schema_extraction::run(&args.output, args.verbose);
5454- }
3737+ let discovery = WorkspaceDiscovery::new()
3838+ .verbose(args.verbose);
55395656- // Advanced case: use full options
5757- let options = ExtractOptions {
5858- output_dir: args.output.into(),
5959- verbose: args.verbose,
6060- filter: args.filter,
6161- validate: args.validate,
6262- pretty: args.pretty,
6363- };
6464-6565- let extractor = SchemaExtractor::new(options);
6666-6767- if args.watch {
6868- extractor.watch()?;
6969- } else {
7070- extractor.extract_all()?;
7171- }
4040+ discovery.generate_and_write(args.output)?;
72417342 Ok(())
7443}
-2
crates/jacquard-lexgen/src/lib.rs
···3636pub mod fetch;
3737pub mod schema_discovery;
3838pub mod schema_extraction;
3939-#[cfg(any(test, debug_assertions))]
4040-pub mod test_schemas;
41394240pub use fetch::{Config, Fetcher};
+11-8
crates/jacquard-lexgen/src/schema_discovery.rs
···155155156156 // Use schema builder based on kind
157157 let built = match schema_info.kind {
158158- SchemaKind::Struct => {
159159- jacquard_lexicon::schema::from_ast::build_struct_schema(&ast)?
160160- }
158158+ SchemaKind::Struct => jacquard_lexicon::schema::from_ast::build_struct_schema(&ast)
159159+ .into_diagnostic()?,
161160 SchemaKind::Enum => {
162162- jacquard_lexicon::schema::from_ast::build_enum_schema(&ast)?
161161+ jacquard_lexicon::schema::from_ast::build_enum_schema(&ast).into_diagnostic()?
163162 }
164163 };
165164···210209 }
211210212211 /// Group schemas by base NSID (strip fragment suffix)
213213- fn group_by_base_nsid(&self, schemas: &[GeneratedSchema]) -> BTreeMap<String, Vec<&GeneratedSchema>> {
214214- let mut groups: BTreeMap<String, Vec<&GeneratedSchema>> = BTreeMap::new();
212212+ fn group_by_base_nsid<'a>(
213213+ &self,
214214+ schemas: &'a [GeneratedSchema],
215215+ ) -> BTreeMap<String, Vec<&'a GeneratedSchema>> {
216216+ let mut groups: BTreeMap<String, Vec<&'a GeneratedSchema>> = BTreeMap::new();
215217216218 for schema in schemas {
217219 // Split on # to get base NSID
···297299298300 /// Serialize a lexicon doc with "main" def first
299301 fn serialize_with_main_first(&self, doc: &LexiconDoc) -> Result<String> {
300300- use serde_json::{json, Map, Value};
302302+ use serde_json::{Map, Value, json};
301303302304 // Build defs map with main first
303305 let mut defs_map = Map::new();
···533535 lex_attrs.key = Some(lit.value());
534536 }
535537 Ok(())
536536- }).into_diagnostic()?;
538538+ })
539539+ .into_diagnostic()?;
537540 }
538541 }
539542