From 372f4ba48a7c894a154443c5cf7371c1311aa37c Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:32:47 +0000 Subject: [PATCH 1/2] embed code generators as stellar-xdr CLI subcommands --- Cargo.lock | 368 +++++++++++++++++- Cargo.toml | 4 +- src/cli/xfile/ast.rs | 42 ++ src/cli/xfile/generate_rust.rs | 53 +++ src/cli/xfile/mod.rs | 12 + xdr-generator-rust/Cargo.lock | 16 +- .../generator-definitions-json/src/lib.rs | 104 +++++ .../generator-definitions-json/src/main.rs | 84 +--- xdr-generator-rust/generator/src/lib.rs | 81 ++++ xdr-generator-rust/generator/src/main.rs | 57 +-- 10 files changed, 685 insertions(+), 136 deletions(-) create mode 100644 src/cli/xfile/ast.rs create mode 100644 src/cli/xfile/generate_rust.rs create mode 100644 xdr-generator-rust/generator-definitions-json/src/lib.rs create mode 100644 xdr-generator-rust/generator/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 641b9e9a..0bd7a7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -26,6 +69,48 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.101", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -38,6 +123,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bitflags" version = "1.3.2" @@ -130,9 +230,11 @@ version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" dependencies = [ + "anstream", "anstyle", "bitflags 1.3.2", "clap_lex", + "strsim", ] [[package]] @@ -141,7 +243,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", "quote", "syn 2.0.101", @@ -163,6 +265,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -345,6 +453,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generator-definitions-json" +version = "0.1.0" +dependencies = [ + "clap", + "serde", + "serde_json", + "xdr-parser", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -385,6 +503,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -446,11 +576,22 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -461,6 +602,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.172" @@ -485,6 +632,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "logos" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "rustc_version", + "syn 2.0.101", +] + +[[package]] +name = "logos-derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + [[package]] name = "num-bigint" version = "0.4.6" @@ -525,6 +712,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "powerfmt" version = "0.2.0" @@ -593,6 +786,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.11" @@ -629,6 +843,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.219" @@ -735,6 +955,7 @@ dependencies = [ "crate-git-revision", "escape-bytes", "ethnum", + "generator-definitions-json", "hex", "rand", "schemars", @@ -743,7 +964,8 @@ dependencies = [ "serde_with", "sha2", "stellar-strkey", - "thiserror", + "thiserror 1.0.37", + "xdr-generator-rust", ] [[package]] @@ -789,7 +1011,16 @@ version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.37", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -803,6 +1034,17 @@ dependencies = [ "syn 1.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "time" version = "0.3.41" @@ -852,6 +1094,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" @@ -952,6 +1200,96 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -961,6 +1299,26 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "xdr-generator-rust" +version = "0.1.0" +dependencies = [ + "askama", + "clap", + "heck 0.5.0", + "xdr-parser", +] + +[[package]] +name = "xdr-parser" +version = "0.1.0" +dependencies = [ + "heck 0.5.0", + "logos", + "sha2", + "thiserror 2.0.18", +] + [[package]] name = "zerocopy" version = "0.8.25" diff --git a/Cargo.toml b/Cargo.toml index e497f31a..1bbfb7a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ schemars = { version = "0.8.16", optional = true } ethnum = { version = "1.5.3", optional = true } rand = { version = "0.9.0", optional = true } sha2 = { version = "0.10.9", optional = true } +xdr-generator-rust = { version = "0.1.0", path = "xdr-generator-rust/generator", optional = true } +generator-definitions-json = { version = "0.1.0", path = "xdr-generator-rust/generator-definitions-json", optional = true } [dev-dependencies] serde_json = "1.0.89" @@ -61,7 +63,7 @@ hex = [] rand = ["dep:rand"] # Features for the CLI. -cli = ["std", "type_enum", "base64", "serde", "serde_json", "schemars", "arbitrary", "rand", "dep:clap", "dep:thiserror", "dep:ethnum"] +cli = ["std", "type_enum", "base64", "serde", "serde_json", "schemars", "arbitrary", "rand", "dep:clap", "dep:thiserror", "dep:ethnum", "dep:xdr-generator-rust", "dep:generator-definitions-json"] [package.metadata.docs.rs] all-features = true diff --git a/src/cli/xfile/ast.rs b/src/cli/xfile/ast.rs new file mode 100644 index 00000000..72a24444 --- /dev/null +++ b/src/cli/xfile/ast.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use clap::Args; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("error generating JSON AST: {0}")] + Generate(Box), +} + +/// Emit parsed XDR definitions as a JSON AST +#[derive(Args, Debug, Clone)] +#[command()] +pub struct Cmd { + /// Input XDR files + #[arg(long, required = true)] + pub input: Vec, + + /// Output JSON AST path + #[arg(long)] + pub output: PathBuf, + + /// Feature set for cfg resolution. Items whose cfg evaluates to true under + /// this feature set are kept; others are dropped. Accepts comma-separated + /// values or may be repeated (e.g. --feature curr,next or --feature curr + /// --feature next). + #[arg(long, value_delimiter = ',')] + pub feature: Vec, +} + +impl Cmd { + /// Run the CLIs xfile ast command. + /// + /// ## Errors + /// + /// If the input files cannot be read or parsed, or the JSON AST cannot be + /// written. + pub fn run(&self) -> Result<(), Error> { + generator_definitions_json::generate(&self.input, &self.output, &self.feature) + .map_err(Error::Generate) + } +} diff --git a/src/cli/xfile/generate_rust.rs b/src/cli/xfile/generate_rust.rs new file mode 100644 index 00000000..08dd406d --- /dev/null +++ b/src/cli/xfile/generate_rust.rs @@ -0,0 +1,53 @@ +use std::path::PathBuf; + +use clap::Args; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("error generating Rust code: {0}")] + Generate(Box), +} + +/// Generate Rust code from XDR .x files +#[derive(Args, Debug, Clone)] +#[command()] +pub struct Cmd { + /// Input XDR files + #[arg(long, required = true)] + pub input: Vec, + + /// Output module file (e.g. src/generated.rs) + #[arg(long)] + pub output: PathBuf, + + /// Types with a custom Default implementation (skip derive(Default)) + #[arg(long, value_delimiter = ',')] + pub custom_default: Vec, + + /// Types with a custom FromStr/Display implementation (use SerializeDisplay) + #[arg(long, value_delimiter = ',')] + pub custom_str: Vec, + + /// Types that should NOT have Display/FromStr/schemars generated + #[arg(long, value_delimiter = ',')] + pub no_display_fromstr: Vec, +} + +impl Cmd { + /// Run the CLIs xfile generate-rust command. + /// + /// ## Errors + /// + /// If the input files cannot be read or parsed, or the generated output + /// cannot be written. + pub fn run(&self) -> Result<(), Error> { + xdr_generator_rust::generate( + &self.input, + &self.output, + &self.custom_default, + &self.custom_str, + &self.no_display_fromstr, + ) + .map_err(Error::Generate) + } +} diff --git a/src/cli/xfile/mod.rs b/src/cli/xfile/mod.rs index c0d7ed9c..25b219a1 100644 --- a/src/cli/xfile/mod.rs +++ b/src/cli/xfile/mod.rs @@ -1,3 +1,5 @@ +pub mod ast; +pub mod generate_rust; pub mod preprocess; use clap::{Args, Subcommand}; @@ -6,6 +8,10 @@ use clap::{Args, Subcommand}; pub enum Error { #[error("{0}")] Preprocess(#[from] preprocess::Error), + #[error("{0}")] + GenerateRust(#[from] generate_rust::Error), + #[error("{0}")] + Ast(#[from] ast::Error), } #[derive(Args, Debug, Clone)] @@ -19,6 +25,10 @@ pub struct Cmd { pub enum Sub { /// Preprocess XDR .x files by evaluating #ifdef/#ifndef/#elif/#else/#endif directives Preprocess(preprocess::Cmd), + /// Generate Rust code from XDR .x files + GenerateRust(generate_rust::Cmd), + /// Emit parsed XDR definitions as a JSON AST + Ast(ast::Cmd), } impl Cmd { @@ -30,6 +40,8 @@ impl Cmd { pub fn run(&self) -> Result<(), Error> { match &self.sub { Sub::Preprocess(c) => c.run()?, + Sub::GenerateRust(c) => c.run()?, + Sub::Ast(c) => c.run()?, } Ok(()) } diff --git a/xdr-generator-rust/Cargo.lock b/xdr-generator-rust/Cargo.lock index 9c60b644..cfde4840 100644 --- a/xdr-generator-rust/Cargo.lock +++ b/xdr-generator-rust/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -126,9 +126,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -166,9 +166,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cpufeatures" diff --git a/xdr-generator-rust/generator-definitions-json/src/lib.rs b/xdr-generator-rust/generator-definitions-json/src/lib.rs new file mode 100644 index 00000000..d561f64d --- /dev/null +++ b/xdr-generator-rust/generator-definitions-json/src/lib.rs @@ -0,0 +1,104 @@ +//! Emit parsed XDR definitions as a JSON AST for downstream code generators. +//! +//! This library parses XDR `.x` definition files, filters them against a +//! feature set, and writes the resulting definitions as JSON. The primary +//! entry point is [`generate`]. + +mod ir; + +#[cfg(test)] +mod tests; + +use std::collections::HashSet; +use std::fs; +use std::path::Path; +use xdr_parser::ast::{Definition, XdrSpec}; + +/// Generate a JSON AST from the given XDR input files. +/// +/// The `input` files are read and sorted by path, parsed into a single XDR +/// spec, filtered against `features` (cfg resolution), and written to `output` +/// as pretty-printed JSON. +/// +/// `features` is the feature set for cfg resolution: items whose cfg evaluates +/// to true under this set are kept; others are dropped. Feature names are +/// lower-cased before evaluation. An empty set drops items gated on any +/// feature (`not(feature = "X")` still evaluates to true and is kept). +/// +/// # Errors +/// +/// Returns an error if any input file cannot be read, the XDR cannot be +/// parsed, the JSON cannot be serialized, or the output cannot be written. +pub fn generate( + input: &[impl AsRef], + output: impl AsRef, + features: &[String], +) -> Result<(), Box> { + let output = output.as_ref(); + + // Read all input files and sort by filename. + let mut files: Vec<(&Path, String)> = Vec::new(); + for path in input { + let path = path.as_ref(); + let content = fs::read_to_string(path)?; + files.push((path, content)); + } + files.sort_by(|a, b| a.0.cmp(b.0)); + + // Build file list for the parser. + let file_refs: Vec<(&str, &str)> = files + .iter() + .map(|(path, content)| (path.to_str().unwrap_or(""), content.as_str())) + .collect(); + + // Parse the XDR files. + let mut spec = xdr_parser::parser::parse_files(&file_refs)?; + + let features: HashSet = features.iter().map(|f| f.to_lowercase()).collect(); + let mut resolved_features: Vec = features.iter().cloned().collect(); + resolved_features.sort(); + filter_spec(&mut spec, &features); + + let output_ir = ir::build(&spec, resolved_features); + let json = serde_json::to_string_pretty(&output_ir)?; + fs::write(output, json)?; + + Ok(()) +} + +/// Filter an XDR spec in-place: remove definitions and sub-elements whose cfg +/// evaluates to false under the given feature set, then clear all remaining +/// cfg annotations. +fn filter_spec(spec: &mut XdrSpec, features: &HashSet) { + filter_definitions(&mut spec.definitions, features); + for ns in &mut spec.namespaces { + filter_definitions(&mut ns.definitions, features); + } +} + +/// Filter a list of definitions: remove those whose cfg is false, filter +/// sub-elements (union arms, enum members), and clear remaining cfg annotations. +fn filter_definitions(defs: &mut Vec, features: &HashSet) { + defs.retain(|d| d.cfg().map_or(true, |cfg| cfg.evaluate(features))); + + for def in defs.iter_mut() { + match def { + Definition::Union(u) => { + u.arms.retain_mut(|arm| { + let keep = arm.cfg.as_ref().map_or(true, |cfg| cfg.evaluate(features)); + arm.cfg = None; + keep + }); + } + Definition::Enum(e) => { + e.members.retain_mut(|m| { + let keep = m.cfg.as_ref().map_or(true, |cfg| cfg.evaluate(features)); + m.cfg = None; + keep + }); + } + Definition::Struct(_) | Definition::Typedef(_) | Definition::Const(_) => {} + } + def.set_cfg(None); + } +} diff --git a/xdr-generator-rust/generator-definitions-json/src/main.rs b/xdr-generator-rust/generator-definitions-json/src/main.rs index 318c02b6..c7123e8d 100644 --- a/xdr-generator-rust/generator-definitions-json/src/main.rs +++ b/xdr-generator-rust/generator-definitions-json/src/main.rs @@ -1,15 +1,12 @@ //! CLI entry point for emitting XDR definitions as a JSON AST. - -mod ir; - -#[cfg(test)] -mod tests; +//! +//! This is a thin wrapper over the [`generator_definitions_json::generate`] +//! library function. It exists so `xdr-definitions-json/xdr.json` can be +//! generated without first building `stellar-xdr`. The same functionality is +//! also available as `stellar-xdr xfile ast`. use clap::Parser; -use std::collections::HashSet; -use std::fs; use std::path::PathBuf; -use xdr_parser::ast::{Definition, XdrSpec}; /// XDR JSON AST emitter. #[derive(Parser, Debug)] @@ -38,74 +35,7 @@ struct Args { fn main() -> Result<(), Box> { let args = Args::parse(); - - // Read all input files and sort by filename - let mut files: Vec<(PathBuf, String)> = Vec::new(); - for path in &args.input { - let content = fs::read_to_string(path)?; - files.push((path.clone(), content)); - } - files.sort_by(|a, b| a.0.cmp(&b.0)); - - // Build file list for the parser - let file_refs: Vec<(&str, &str)> = files - .iter() - .map(|(path, content)| (path.to_str().unwrap_or(""), content.as_str())) - .collect(); - - // Parse the XDR files - let mut spec = xdr_parser::parser::parse_files(&file_refs)?; - - let features: HashSet = args.feature.iter().map(|f| f.to_lowercase()).collect(); - let mut resolved_features: Vec = features.iter().cloned().collect(); - resolved_features.sort(); - filter_spec(&mut spec, &features); - - let output = ir::build(&spec, resolved_features); - let json = serde_json::to_string_pretty(&output)?; - fs::write(&args.output, json)?; - eprintln!( - "JSON AST: {} ({} definitions)", - args.output.display(), - output.definitions.len(), - ); - + generator_definitions_json::generate(&args.input, &args.output, &args.feature)?; + eprintln!("JSON AST: {}", args.output.display()); Ok(()) } - -/// Filter an XDR spec in-place: remove definitions and sub-elements whose cfg -/// evaluates to false under the given feature set, then clear all remaining -/// cfg annotations. -fn filter_spec(spec: &mut XdrSpec, features: &HashSet) { - filter_definitions(&mut spec.definitions, features); - for ns in &mut spec.namespaces { - filter_definitions(&mut ns.definitions, features); - } -} - -/// Filter a list of definitions: remove those whose cfg is false, filter -/// sub-elements (union arms, enum members), and clear remaining cfg annotations. -fn filter_definitions(defs: &mut Vec, features: &HashSet) { - defs.retain(|d| d.cfg().map_or(true, |cfg| cfg.evaluate(features))); - - for def in defs.iter_mut() { - match def { - Definition::Union(u) => { - u.arms.retain_mut(|arm| { - let keep = arm.cfg.as_ref().map_or(true, |cfg| cfg.evaluate(features)); - arm.cfg = None; - keep - }); - } - Definition::Enum(e) => { - e.members.retain_mut(|m| { - let keep = m.cfg.as_ref().map_or(true, |cfg| cfg.evaluate(features)); - m.cfg = None; - keep - }); - } - Definition::Struct(_) | Definition::Typedef(_) | Definition::Const(_) => {} - } - def.set_cfg(None); - } -} diff --git a/xdr-generator-rust/generator/src/lib.rs b/xdr-generator-rust/generator/src/lib.rs new file mode 100644 index 00000000..81849123 --- /dev/null +++ b/xdr-generator-rust/generator/src/lib.rs @@ -0,0 +1,81 @@ +//! XDR to Rust code generator. +//! +//! This library parses XDR `.x` definition files and generates Rust source +//! code from them. The primary entry point is [`generate`], which reads the +//! input files, parses them, and writes the generated module file plus a +//! directory of per-type files. + +mod generator; +mod naming; +mod options; +mod output; +mod types; + +#[cfg(test)] +mod tests; + +use std::collections::HashSet; +use std::fs; +use std::path::Path; + +pub use generator::RustGenerator; +pub use options::RustOptions; + +/// Generate Rust code from the given XDR input files. +/// +/// The `input` files are read and sorted by path, parsed into a single XDR +/// spec, and then rendered to `output` (the module file, e.g. +/// `src/generated.rs`) plus a sibling directory derived from the module file +/// name (e.g. `src/generated/`) containing the per-type files. +/// +/// `custom_default`, `custom_str`, and `no_display_fromstr` are lists of type +/// names controlling, respectively, which types skip `derive(Default)`, which +/// types use a custom `FromStr`/`Display` implementation, and which types +/// should not have `Display`/`FromStr`/`schemars` generated. +/// +/// # Errors +/// +/// Returns an error if any input file cannot be read, the XDR cannot be +/// parsed, or the generated output cannot be written. +pub fn generate( + input: &[impl AsRef], + output: impl AsRef, + custom_default: &[String], + custom_str: &[String], + no_display_fromstr: &[String], +) -> Result<(), Box> { + let output = output.as_ref(); + + // Read all input files and sort by filename. + let mut files: Vec<(&Path, String)> = Vec::new(); + for path in input { + let path = path.as_ref(); + let content = fs::read_to_string(path)?; + files.push((path, content)); + } + files.sort_by(|a, b| a.0.cmp(b.0)); + + // Build file list for the parser. + let file_refs: Vec<(&str, &str)> = files + .iter() + .map(|(path, content)| (path.to_str().unwrap_or(""), content.as_str())) + .collect(); + + // Parse the XDR files. + let spec = xdr_parser::parser::parse_files(&file_refs)?; + + let options = RustOptions { + custom_default_impl: custom_default.iter().cloned().collect::>(), + custom_str_impl: custom_str.iter().cloned().collect::>(), + no_display_fromstr: no_display_fromstr.iter().cloned().collect::>(), + }; + + let generator = RustGenerator::new(&spec, options); + + // Derive the per-type directory from the module file path: + // e.g. src/generated.rs -> src/generated/ + let output_dir = output.with_extension(""); + generator.generate_to_dir(&spec, output, &output_dir)?; + + Ok(()) +} diff --git a/xdr-generator-rust/generator/src/main.rs b/xdr-generator-rust/generator/src/main.rs index c3a2681c..0ec5b65a 100644 --- a/xdr-generator-rust/generator/src/main.rs +++ b/xdr-generator-rust/generator/src/main.rs @@ -1,19 +1,11 @@ //! CLI entry point for the XDR code generator. - -mod generator; -mod naming; -mod options; -mod output; -mod types; - -#[cfg(test)] -mod tests; +//! +//! This is a thin wrapper over the [`xdr_generator_rust::generate`] library +//! function. It exists so `src/generated.rs` can be regenerated without first +//! building `stellar-xdr` (which depends on the generated code). The same +//! functionality is also available as `stellar-xdr xfile generate-rust`. use clap::Parser; -use generator::RustGenerator; -use options::RustOptions; -use std::collections::HashSet; -use std::fs; use std::path::PathBuf; /// XDR code generator. @@ -44,38 +36,13 @@ struct Args { fn main() -> Result<(), Box> { let args = Args::parse(); - - // Read all input files and sort by filename - let mut files: Vec<(PathBuf, String)> = Vec::new(); - for path in &args.input { - let content = fs::read_to_string(path)?; - files.push((path.clone(), content)); - } - files.sort_by(|a, b| a.0.cmp(&b.0)); - - // Build file list for the parser - let file_refs: Vec<(&str, &str)> = files - .iter() - .map(|(path, content)| (path.to_str().unwrap_or(""), content.as_str())) - .collect(); - - // Parse the XDR files - let spec = xdr_parser::parser::parse_files(&file_refs)?; - - let options = RustOptions { - custom_default_impl: args.custom_default.into_iter().collect::>(), - custom_str_impl: args.custom_str.into_iter().collect::>(), - no_display_fromstr: args.no_display_fromstr.into_iter().collect::>(), - }; - - let generator = RustGenerator::new(&spec, options); - - // Derive the per-type directory from the module file path: - // e.g. src/generated.rs -> src/generated/ - let output_dir = args.output.with_extension(""); - generator.generate_to_dir(&spec, &args.output, &output_dir)?; - + xdr_generator_rust::generate( + &args.input, + &args.output, + &args.custom_default, + &args.custom_str, + &args.no_display_fromstr, + )?; eprintln!("Generated: {}", args.output.display()); - Ok(()) } From e2fab176411775e7e34f3e89201346dbbf4c1d2c Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:27:04 +0000 Subject: [PATCH 2/2] format generator crate files pulled into fmt scope --- .../generator-definitions-json/src/ir.rs | 112 ++++++++++++------ .../xdr-parser/src/tests/types.rs | 12 +- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/xdr-generator-rust/generator-definitions-json/src/ir.rs b/xdr-generator-rust/generator-definitions-json/src/ir.rs index 4770f835..7b6c9b0a 100644 --- a/xdr-generator-rust/generator-definitions-json/src/ir.rs +++ b/xdr-generator-rust/generator-definitions-json/src/ir.rs @@ -164,16 +164,21 @@ pub fn build(spec: &ast::XdrSpec, resolved_features: Vec) -> IR { let const_values = &type_info.const_values; - let definitions = spec.all_definitions() + let definitions = spec + .all_definitions() .map(|def| convert_definition(def, &wire_sizes, const_values, &enum_member_values)) .collect(); IR { version: 1, - files: spec.files.iter().map(|f| XdrFile { - name: f.name.clone(), - sha256: f.sha256.clone(), - }).collect(), + files: spec + .files + .iter() + .map(|f| XdrFile { + name: f.name.clone(), + sha256: f.sha256.clone(), + }) + .collect(), resolved_features, definitions, } @@ -192,10 +197,14 @@ fn convert_definition( fixed_size, source: s.source.clone(), file_index: s.file_index, - fields: s.members.iter().map(|m| Field { - name: m.name.clone(), - type_: convert_type(&m.type_, const_values), - }).collect(), + fields: s + .members + .iter() + .map(|m| Field { + name: m.name.clone(), + type_: convert_type(&m.type_, const_values), + }) + .collect(), }), ast::Definition::Union(u) => Definition::Union(Union { name: u.name.clone(), @@ -206,38 +215,54 @@ fn convert_definition( name: u.discriminant.name.clone(), type_: convert_type(&u.discriminant.type_, const_values), }, - arms: u.arms.iter().map(|arm| { - let cases = arm.cases.iter().map(|c| match &c.value { - ast::UnionCaseValue::Literal(literal) => UnionCase { - value: i64::from(*literal), - name: None, - }, - ast::UnionCaseValue::Ident(ident) => { - let value = enum_member_values.get(ident.as_str()) - .map(|&v| i64::from(v)) - .or_else(|| const_values.get(ident).copied()) - .unwrap_or_else(|| panic!( - "union {}: unresolved case ident '{ident}'", u.name - )); - UnionCase { value, name: Some(ident.clone()) } + arms: u + .arms + .iter() + .map(|arm| { + let cases = arm + .cases + .iter() + .map(|c| match &c.value { + ast::UnionCaseValue::Literal(literal) => UnionCase { + value: i64::from(*literal), + name: None, + }, + ast::UnionCaseValue::Ident(ident) => { + let value = enum_member_values + .get(ident.as_str()) + .map(|&v| i64::from(v)) + .or_else(|| const_values.get(ident).copied()) + .unwrap_or_else(|| { + panic!("union {}: unresolved case ident '{ident}'", u.name) + }); + UnionCase { + value, + name: Some(ident.clone()), + } + } + }) + .collect(); + UnionArm { + cases, + name: arm.name.clone(), + type_: arm.type_.as_ref().map(|t| convert_type(t, const_values)), } - }).collect(); - UnionArm { - cases, - name: arm.name.clone(), - type_: arm.type_.as_ref().map(|t| convert_type(t, const_values)), - } - }).collect(), + }) + .collect(), }), ast::Definition::Enum(e) => Definition::Enum(Enum { name: e.name.clone(), source: e.source.clone(), file_index: e.file_index, member_prefix: e.member_prefix.clone(), - members: e.members.iter().map(|m| EnumMember { - name: m.name.clone(), - value: i64::from(m.value), - }).collect(), + members: e + .members + .iter() + .map(|m| EnumMember { + name: m.name.clone(), + value: i64::from(m.value), + }) + .collect(), }), ast::Definition::Typedef(t) => Definition::Typedef(Typedef { name: t.name.clone(), @@ -280,7 +305,10 @@ fn convert_type(ty: &ast::Type, const_values: &HashMap) -> TypeRef element: Box::new(convert_type(element_type, const_values)), count: resolve_size(size, const_values), }, - ast::Type::VarArray { element_type, max_size } => TypeRef::VarArray { + ast::Type::VarArray { + element_type, + max_size, + } => TypeRef::VarArray { element: Box::new(convert_type(element_type, const_values)), max_count: max_size.as_ref().map(|s| resolve_size(s, const_values)), }, @@ -365,7 +393,12 @@ fn compute_wire_size( } if arm_sizes.is_empty() || arm_sizes.iter().any(|s| s.is_none()) - || arm_sizes.iter().filter_map(|s| *s).collect::>().len() != 1 + || arm_sizes + .iter() + .filter_map(|s| *s) + .collect::>() + .len() + != 1 { None } else { @@ -401,9 +434,10 @@ fn type_wire_size( elem_sz.checked_mul(count) } ast::Type::Ident(name) => compute_wire_size(ti, name, cache, in_progress), - ast::Type::OpaqueVar(_) | ast::Type::String(_) | ast::Type::VarArray { .. } | ast::Type::Optional(_) => { - None - } + ast::Type::OpaqueVar(_) + | ast::Type::String(_) + | ast::Type::VarArray { .. } + | ast::Type::Optional(_) => None, } } diff --git a/xdr-generator-rust/xdr-parser/src/tests/types.rs b/xdr-generator-rust/xdr-parser/src/tests/types.rs index f5e8c808..96110708 100644 --- a/xdr-generator-rust/xdr-parser/src/tests/types.rs +++ b/xdr-generator-rust/xdr-parser/src/tests/types.rs @@ -11,7 +11,11 @@ fn test_union_arm_name() { }; "#; let spec = parse(input).unwrap(); - let u = spec.definitions.iter().find(|d| d.name() == "MyUnion").unwrap(); + let u = spec + .definitions + .iter() + .find(|d| d.name() == "MyUnion") + .unwrap(); if let crate::ast::Definition::Union(union_def) = u { assert_eq!(union_def.arms.len(), 2); assert_eq!(union_def.arms[0].name.as_deref(), Some("myField")); @@ -30,7 +34,11 @@ fn test_inline_struct_arm_name() { }; "#; let spec = parse(input).unwrap(); - let u = spec.definitions.iter().find(|d| d.name() == "Outer").unwrap(); + let u = spec + .definitions + .iter() + .find(|d| d.name() == "Outer") + .unwrap(); if let crate::ast::Definition::Union(union_def) = u { assert_eq!(union_def.arms[0].name.as_deref(), Some("myInlineField")); } else {