diff --git a/.gitignore b/.gitignore index ea8c4bf7..f2d2f93e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +generated-rust.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86094042..5c0a03d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,15 @@ # Contributing +## XDR Generators + +This repository has two XDR-to-Rust code generators: + +1. **Ruby generator** (`xdr-generator/`) - The original generator using xdrgen. Outputs to `src/*/generated.rs`. + +2. **Rust generator** (`xdr-generator-rust/`) - A new generator written in Rust. Outputs to `src/*/generated-rust.rs`. + +The plan is to migrate to the Rust generator once it has been validated to produce identical output. Currently both generators run during `make generate` and the outputs are compared to ensure they match. + ## How to Regenerate From XDR To regenerate types from XDR definitions: diff --git a/Makefile b/Makefile index 0cee0a6a..19bf6e1e 100644 --- a/Makefile +++ b/Makefile @@ -30,13 +30,42 @@ readme: watch: cargo watch --clear --watch-when-idle --shell '$(MAKE)' -generate: generate-xdrgen-files xdr/curr-version xdr/next-version xdr-json/curr xdr-json/next +generate: generate-xdrgen-files generate-rust-files xdr/curr-version xdr/next-version xdr-json/curr xdr-json/next check-generated-match generate-xdrgen-files: src/curr/generated.rs src/next/generated.rs docker run -i --rm -v $$PWD:/wd -w /wd docker.io/library/ruby:3.1 /bin/bash -c \ 'cd xdr-generator && bundle install --quiet && bundle exec ruby generate.rb' rustfmt $^ +CUSTOM_DEFAULT_IMPL=TransactionEnvelope +CUSTOM_STR_IMPL=PublicKey,AccountId,ContractId,MuxedAccount,MuxedAccountMed25519,SignerKey,SignerKeyEd25519SignedPayload,NodeId,ScAddress,AssetCode,AssetCode4,AssetCode12,ClaimableBalanceId,PoolId,MuxedEd25519Account,Int128Parts,UInt128Parts,Int256Parts,UInt256Parts + +generate-rust-files: src/curr/generated-rust.rs src/next/generated-rust.rs + +src/curr/generated-rust.rs: $(sort $(wildcard xdr/curr/*.x)) + cargo run --manifest-path xdr-generator-rust/Cargo.toml -- \ + $(addprefix --input ,$(sort $(wildcard xdr/curr/*.x))) \ + --output $@ \ + --custom-default $(CUSTOM_DEFAULT_IMPL) \ + --custom-str $(CUSTOM_STR_IMPL) + rustfmt $@ + +src/next/generated-rust.rs: $(sort $(wildcard xdr/next/*.x)) + cargo run --manifest-path xdr-generator-rust/Cargo.toml -- \ + $(addprefix --input ,$(sort $(wildcard xdr/next/*.x))) \ + --output $@ \ + --custom-default $(CUSTOM_DEFAULT_IMPL) \ + --custom-str $(CUSTOM_STR_IMPL) + rustfmt $@ + +check-generated-match: + @echo "Checking that Ruby and Rust generators produce identical output..." + @diff -q src/curr/generated.rs src/curr/generated-rust.rs || \ + (echo "ERROR: src/curr/generated.rs and src/curr/generated-rust.rs differ" && exit 1) + @diff -q src/next/generated.rs src/next/generated-rust.rs || \ + (echo "ERROR: src/next/generated.rs and src/next/generated-rust.rs differ" && exit 1) + @echo "OK: Ruby and Rust generator outputs match" + src/next/generated.rs: $(sort $(wildcard xdr/curr/*.x)) > $@ @@ -59,6 +88,7 @@ xdr-json/next: src/next/generated.rs clean: rm -f src/*/generated.rs + rm -f src/*/generated-rust.rs rm -f xdr/*-version rm -fr xdr-json/curr rm -fr xdr-json/next diff --git a/xdr-generator-rust/.gitignore b/xdr-generator-rust/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/xdr-generator-rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/xdr-generator-rust/Cargo.lock b/xdr-generator-rust/Cargo.lock new file mode 100644 index 00000000..1194358a --- /dev/null +++ b/xdr-generator-rust/Cargo.lock @@ -0,0 +1,522 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[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", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[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", +] + +[[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 = "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 = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[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.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[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", +] + +[[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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "xdr-generator-rust" +version = "0.1.0" +dependencies = [ + "askama", + "clap", + "heck", + "logos", + "pretty_assertions", + "sha2", + "thiserror", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/xdr-generator-rust/Cargo.toml b/xdr-generator-rust/Cargo.toml new file mode 100644 index 00000000..ffbd5f06 --- /dev/null +++ b/xdr-generator-rust/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xdr-generator-rust" +description = "XDR to Rust code generator for Stellar" +version = "0.1.0" +edition = "2021" +rust-version = "1.84.0" + +[[bin]] +name = "xdr-generator-rust" +path = "src/main.rs" + +[dependencies] +askama = "0.14" +clap = { version = "4", features = ["derive"] } +sha2 = "0.10" +heck = "0.5" +thiserror = "2" +logos = "0.15" + +[dev-dependencies] +pretty_assertions = "1" diff --git a/xdr-generator-rust/src/ast.rs b/xdr-generator-rust/src/ast.rs new file mode 100644 index 00000000..aca4fc86 --- /dev/null +++ b/xdr-generator-rust/src/ast.rs @@ -0,0 +1,213 @@ +//! AST types for XDR definitions. + +use crate::lexer::IntBase; + +/// The root of a parsed XDR file or collection of files. +#[derive(Debug, Clone, Default)] +pub struct XdrSpec { + pub namespaces: Vec, + pub definitions: Vec, +} + +impl XdrSpec { + /// Get all definitions, including those in namespaces. + pub fn all_definitions(&self) -> impl Iterator { + self.definitions + .iter() + .chain(self.namespaces.iter().flat_map(|ns| &ns.definitions)) + } +} + +/// A namespace containing definitions. +#[derive(Debug, Clone)] +pub struct Namespace { + pub name: String, + pub definitions: Vec, +} + +/// A top-level definition. +#[derive(Debug, Clone)] +pub enum Definition { + Struct(Struct), + Enum(Enum), + Union(Union), + Typedef(Typedef), + Const(Const), +} + +impl Definition { + /// Get the name of this definition. + pub fn name(&self) -> &str { + match self { + Definition::Struct(s) => &s.name, + Definition::Enum(e) => &e.name, + Definition::Union(u) => &u.name, + Definition::Typedef(t) => &t.name, + Definition::Const(c) => &c.name, + } + } + + /// Check if this definition is nested (inline struct/union extracted from parent). + pub fn is_nested(&self) -> bool { + match self { + Definition::Struct(s) => s.is_nested, + Definition::Union(u) => u.is_nested, + // Enums, typedefs, and consts are never nested + Definition::Enum(_) | Definition::Typedef(_) | Definition::Const(_) => false, + } + } + + /// Get the parent type name if this is a nested definition. + pub fn parent(&self) -> Option<&str> { + match self { + Definition::Struct(s) => s.parent.as_deref(), + Definition::Union(u) => u.parent.as_deref(), + // Enums, typedefs, and consts have no parent + Definition::Enum(_) | Definition::Typedef(_) | Definition::Const(_) => None, + } + } +} + +/// A struct definition. +#[derive(Debug, Clone)] +pub struct Struct { + pub name: String, + pub members: Vec, + /// Original XDR source text for documentation. + pub source: String, + /// True if this is a nested/inline struct extracted from a union arm. + pub is_nested: bool, + /// Name of the parent type if this is nested, for ordering purposes. + pub parent: Option, +} + +/// An enum definition. +#[derive(Debug, Clone)] +pub struct Enum { + pub name: String, + pub members: Vec, + /// Original XDR source text for documentation. + pub source: String, +} + +/// A union definition. +#[derive(Debug, Clone)] +pub struct Union { + pub name: String, + pub discriminant: UnionDiscriminant, + pub arms: Vec, + /// Original XDR source text for documentation. + pub source: String, + /// True if this is a nested/inline union extracted from a struct field. + pub is_nested: bool, + /// Name of the parent type if this is nested, for ordering purposes. + pub parent: Option, +} + +/// A typedef definition. +#[derive(Debug, Clone)] +pub struct Typedef { + pub name: String, + pub type_: Type, + /// Original XDR source text for documentation. + pub source: String, +} + +/// A const definition. +#[derive(Debug, Clone)] +pub struct Const { + pub name: String, + pub value: i64, + /// The base (radix) of the literal in the source. + pub base: IntBase, + /// Original XDR source text for documentation. + pub source: String, +} + +/// XDR type specification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + /// `int` - 32-bit signed integer + Int, + /// `unsigned int` - 32-bit unsigned integer + UnsignedInt, + /// `hyper` - 64-bit signed integer + Hyper, + /// `unsigned hyper` - 64-bit unsigned integer + UnsignedHyper, + /// `float` - 32-bit floating point + Float, + /// `double` - 64-bit floating point + Double, + /// `bool` - boolean + Bool, + /// `opaque[N]` - fixed-length opaque data + OpaqueFixed(Size), + /// `opaque` or `opaque<>` - variable-length opaque data + OpaqueVar(Option), + /// `string` or `string<>` - variable-length string + String(Option), + /// Reference to another type by name + Ident(String), + /// `T*` - optional type + Optional(Box), + /// `T[N]` - fixed-length array + Array { element_type: Box, size: Size }, + /// `T` or `T<>` - variable-length array + VarArray { + element_type: Box, + max_size: Option, + }, +} + +/// A member of a struct. +#[derive(Debug, Clone)] +pub struct StructMember { + pub name: String, + pub type_: Type, +} + +/// A member of an enum. +#[derive(Debug, Clone)] +pub struct EnumMember { + pub name: String, + pub value: i32, +} + +/// The discriminant of a union. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnionDiscriminant { + pub name: String, + pub type_: Type, +} + +/// An arm of a union (one or more cases with the same type). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnionArm { + pub cases: Vec, + /// The type for this arm. None means `void`. + pub type_: Option, +} + +/// A case in a union. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnionCase { + /// The case value - either an identifier (enum variant) or a literal. + pub value: UnionCaseValue, +} + +/// Value for a union case. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnionCaseValue { + /// Named identifier (typically an enum variant) + Ident(String), + /// Literal integer value + Literal(i32), +} + +/// A size specification, either a literal number or a named constant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Size { + Literal(u32), + Named(String), +} diff --git a/xdr-generator-rust/src/generator.rs b/xdr-generator-rust/src/generator.rs new file mode 100644 index 00000000..6ebb424d --- /dev/null +++ b/xdr-generator-rust/src/generator.rs @@ -0,0 +1,632 @@ +//! Code generator that prepares data for templates and renders output. + +use std::collections::{HashMap, HashSet}; + +use crate::ast::{ + Const, Definition, Enum, Struct, StructMember, Type, Typedef, Union, UnionArm, UnionCaseValue, + XdrSpec, +}; +use crate::lexer::IntBase; +use crate::types::{ + base_rust_type_ref, element_type_for_vec, is_builtin_type, is_fixed_array, is_fixed_opaque, + is_var_array, rust_field_name, rust_read_call_type, rust_type_name, rust_type_ref, + serde_as_type, size_to_rust, Options, TypeInfo, +}; +use askama::Template; +use sha2::{Digest, Sha256}; + +/// Generator for producing Rust code from XDR specs. +pub struct Generator { + pub options: Options, + pub type_info: TypeInfo, +} + +impl Generator { + pub fn new(spec: &XdrSpec, options: Options) -> Self { + let type_info = TypeInfo::build(spec); + Self { options, type_info } + } + + /// Generate output for the entire spec. + pub fn generate( + &self, + spec: &XdrSpec, + input_files: &[(String, String)], // (path, content) + header: &str, + ) -> GeneratedTemplate { + // Compute SHA256 hashes for input files + let xdr_files_sha256: Vec<(String, String)> = input_files + .iter() + .map(|(path, content)| { + let mut hasher = Sha256::new(); + hasher.update(content.as_bytes()); + let hash = format!("{:x}", hasher.finalize()); + (path.clone(), hash) + }) + .collect(); + + // Generate each definition's output and collect type names for the + // TypeVariant enum (which needs parent-before-children ordering). + let mut definitions: Vec = Vec::new(); + let mut all_type_names: Vec<(String, Option)> = Vec::new(); // (name, parent) + + for def in spec.all_definitions() { + let name = rust_type_name(def.name()); + + // Only add non-const types to the type enum + if !matches!(def, Definition::Const(_)) { + let parent = def.parent().map(|p| rust_type_name(p)); + all_type_names.push((name, parent)); + } + + let output = self.generate_definition(def); + definitions.push(output); + } + + let types = order_types_parent_first(&all_type_names); + + let namespace = spec + .namespaces + .first() + .map(|ns| ns.name.clone()) + .unwrap_or_default(); + + GeneratedTemplate { + namespace, + xdr_files_sha256, + header: header.to_string(), + definitions, + type_variant_enum: TypeEnumOutput { types }, + } + } + + fn generate_definition(&self, def: &Definition) -> DefinitionOutput { + match def { + Definition::Struct(s) => DefinitionOutput::Struct(self.generate_struct(s)), + Definition::Enum(e) => DefinitionOutput::Enum(self.generate_enum(e)), + Definition::Union(u) => DefinitionOutput::Union(self.generate_union(u)), + Definition::Typedef(t) => self.generate_typedef(t), + Definition::Const(c) => DefinitionOutput::Const(self.generate_const(c)), + } + } + + fn generate_struct(&self, s: &Struct) -> StructOutput { + let name = rust_type_name(&s.name); + let custom_default = self.options.custom_default_impl.contains(&name); + let custom_str = self.options.custom_str_impl.contains(&name); + + let members: Vec = s + .members + .iter() + .map(|m| self.generate_member(m, &name, custom_str)) + .collect(); + + let member_names: String = members + .iter() + .map(|m| m.name.as_str()) + .collect::>() + .join(", "); + + let type_kind = if s.is_nested { + "NestedStruct" + } else { + "Struct" + }; + StructOutput { + name, + source_comment: format_source_comment(&s.source, type_kind), + has_default: !custom_default, + is_custom_str: custom_str, + members, + member_names, + } + } + + fn generate_enum(&self, e: &Enum) -> EnumOutput { + let name = rust_type_name(&e.name); + let custom_default = self.options.custom_default_impl.contains(&name); + let custom_str = self.options.custom_str_impl.contains(&name); + + // Find common prefix to strip from member names + let member_names: Vec<&str> = e.members.iter().map(|m| m.name.as_str()).collect(); + let prefix = find_common_prefix(&member_names); + + let members: Vec = e + .members + .iter() + .enumerate() + .map(|(i, m)| { + let stripped = strip_prefix(&m.name, prefix); + EnumStructMemberOutput { + name: rust_type_name(&stripped), + value: m.value, + is_first: i == 0, + } + }) + .collect(); + + EnumOutput { + name, + source_comment: format_source_comment(&e.source, "Enum"), + has_default: !custom_default, + is_custom_str: custom_str, + members, + } + } + + fn generate_union(&self, u: &Union) -> UnionOutput { + let name = rust_type_name(&u.name); + let custom_default = self.options.custom_default_impl.contains(&name); + let custom_str = self.options.custom_str_impl.contains(&name); + + let discriminant_type = rust_type_ref(&u.discriminant.type_, None, &self.type_info); + let discriminant_is_builtin = is_builtin_type(&u.discriminant.type_) + || matches!(&u.discriminant.type_, Type::Ident(n) if { + // Check if it resolves to a builtin + self.type_info.definitions.get(&rust_type_name(n)) + .map(|d| matches!(d, Definition::Typedef(t) if is_builtin_type(&t.type_))) + .unwrap_or(false) + }); + + // Find the discriminant enum's common prefix for stripping case names + let discriminant_prefix = if !discriminant_is_builtin { + if let Type::Ident(enum_name) = &u.discriminant.type_ { + if let Some(Definition::Enum(enum_def)) = + self.type_info.definitions.get(&rust_type_name(enum_name)) + { + let member_names: Vec<&str> = + enum_def.members.iter().map(|m| m.name.as_str()).collect(); + find_common_prefix(&member_names).to_string() + } else { + String::new() + } + } else { + String::new() + } + } else { + String::new() + }; + + let arms: Vec = u + .arms + .iter() + .flat_map(|arm| { + self.generate_union_arm( + arm, + &name, + &discriminant_type, + discriminant_is_builtin, + &discriminant_prefix, + custom_str, + ) + }) + .collect(); + + // For default impl: get the case name and type of the first arm + let first_arm_case_name = if !arms.is_empty() { + arms[0].case_name.clone() + } else { + String::new() + }; + let first_arm_type = if !arms.is_empty() && !arms[0].is_void { + arms[0].read_call.clone() + } else { + None + }; + + let type_kind = if u.is_nested { "NestedUnion" } else { "Union" }; + + UnionOutput { + name, + source_comment: format_source_comment(&u.source, type_kind), + has_default: !custom_default, + is_custom_str: custom_str, + discriminant_type, + discriminant_is_builtin, + arms, + first_arm_case_name, + first_arm_type, + } + } + + fn generate_typedef(&self, t: &Typedef) -> DefinitionOutput { + let name = rust_type_name(&t.name); + + // Typedefs whose underlying type is an XDR keyword primitive (int, hyper, + // etc.) AND whose XDR name is one of the standard aliases (uint64, int64, + // uint32, int32) become simple type aliases. Other typedefs to primitives + // (like SequenceNumber, Duration, TimePoint) become newtypes because their + // XDR source uses the alias name (e.g. `int64`) which the parser resolves + // to an Ident, not a keyword primitive. This matches the Ruby generator + // where `is_builtin_type` checks the AST node type, not the resolved type. + let is_primitive_alias = matches!( + name.as_str(), + "Uint64" | "Int64" | "Uint32" | "Int32" | "Float" | "Double" + ); + if is_primitive_alias && is_builtin_type(&t.type_) { + return DefinitionOutput::TypedefAlias(TypedefAliasOutput { + name, + source_comment: format_source_comment(&t.source, "Typedef"), + type_ref: base_rust_type_ref(&t.type_, None), + }); + } + + let custom_default = self.options.custom_default_impl.contains(&name); + let custom_str = self.options.custom_str_impl.contains(&name); + let no_display_fromstr = self.options.no_display_fromstr.contains(&name); + let is_fixed_opaque_type = is_fixed_opaque(&t.type_); + let is_fixed_array_type = is_fixed_array(&t.type_); + let is_var_array_type = is_var_array(&t.type_); + + let type_ref = rust_type_ref(&t.type_, None, &self.type_info); + let read_call = rust_read_call_type(&t.type_, None, &self.type_info); + let serde_as = if custom_str { + None + } else { + serde_as_type(&t.type_) + }; + + let element_type = element_type_for_vec(&t.type_); + let size = match &t.type_ { + Type::OpaqueFixed(s) | Type::Array { size: s, .. } => Some(size_to_rust(s)), + _ => None, + }; + + DefinitionOutput::TypedefNewtype(TypedefNewtypeOutput { + name, + source_comment: format_source_comment(&t.source, "Typedef"), + has_default: !custom_default, + is_var_array: is_var_array_type, + is_fixed_opaque: is_fixed_opaque_type, + is_fixed_array: is_fixed_array_type, + is_custom_str: custom_str, + type_ref, + read_call, + serde_as_type: serde_as, + element_type, + size, + custom_debug: is_fixed_opaque_type, + custom_display_fromstr: is_fixed_opaque_type && !custom_str && !no_display_fromstr, + custom_schemars: is_fixed_opaque_type && !custom_str && !no_display_fromstr, + }) + } + + fn generate_const(&self, c: &Const) -> ConstOutput { + let value_str = match c.base { + IntBase::Hexadecimal => format!("0x{:X}", c.value), + IntBase::Decimal => c.value.to_string(), + }; + ConstOutput { + name: rust_field_name(&c.name).to_uppercase(), + doc_name: rust_type_name(&c.name), + source_comment: format_source_comment(&c.source, "Const"), + value_str, + } + } + + fn generate_member( + &self, + m: &StructMember, + parent: &str, + custom_str: bool, + ) -> StructMemberOutput { + let name = rust_field_name(&m.name); + let type_ref = rust_type_ref(&m.type_, Some(parent), &self.type_info); + let read_call = rust_read_call_type(&m.type_, Some(parent), &self.type_info); + // For custom_str types, we skip serde_as since they use SerializeDisplay + let serde_as = if custom_str { + None + } else { + serde_as_type(&m.type_) + }; + + StructMemberOutput { + name, + type_ref, + read_call, + serde_as_type: serde_as, + } + } + + fn generate_union_arm( + &self, + arm: &UnionArm, + parent: &str, + discriminant_type: &str, + discriminant_is_builtin: bool, + discriminant_prefix: &str, + custom_str: bool, + ) -> Vec { + arm.cases + .iter() + .map(|case| { + let (case_name, case_value) = match &case.value { + UnionCaseValue::Ident(name) => { + // Strip common prefix from case name + let stripped = strip_prefix(name, discriminant_prefix); + let rust_name = rust_type_name(&stripped); + let value = if discriminant_is_builtin { + rust_name.clone() + } else { + format!("{discriminant_type}::{rust_name}") + }; + (rust_name, value) + } + UnionCaseValue::Literal(n) => { + let case_name = format!("V{n}"); + let value = if discriminant_is_builtin { + n.to_string() + } else { + format!("{discriminant_type}({n})") + }; + (case_name, value) + } + }; + + let (type_ref, read_call, serde_as) = if let Some(t) = &arm.type_ { + let type_ref = rust_type_ref(t, Some(parent), &self.type_info); + let read_call = rust_read_call_type(t, Some(parent), &self.type_info); + // Get serde_as type for i64/u64 types (unless custom_str) + let serde_as = if custom_str { None } else { serde_as_type(t) }; + (Some(type_ref), Some(read_call), serde_as) + } else { + (None, None, None) + }; + + UnionArmOutput { + case_name, + case_value, + is_void: arm.type_.is_none(), + type_ref, + read_call, + serde_as_type: serde_as, + } + }) + .collect() + } +} + +/// Data for rendering the main generated file. +#[derive(Template)] +#[template(path = "generated.rs.jinja", escape = "none")] +pub struct GeneratedTemplate { + pub namespace: String, + pub xdr_files_sha256: Vec<(String, String)>, + pub header: String, + pub definitions: Vec, + pub type_variant_enum: TypeEnumOutput, +} + +/// Output for a single definition. +pub enum DefinitionOutput { + Struct(StructOutput), + Enum(EnumOutput), + Union(UnionOutput), + TypedefAlias(TypedefAliasOutput), + TypedefNewtype(TypedefNewtypeOutput), + Const(ConstOutput), +} + +/// Data for rendering a struct. +pub struct StructOutput { + pub name: String, + pub source_comment: String, + pub has_default: bool, + pub is_custom_str: bool, + pub members: Vec, + /// Comma-separated list of member names for destructuring (e.g., "id, ed25519,") + pub member_names: String, +} + +pub struct StructMemberOutput { + pub name: String, + pub type_ref: String, + pub read_call: String, + /// The serde_as type for i64/u64 fields (e.g., "NumberOrString"). + pub serde_as_type: Option, +} + +/// Data for rendering an enum. +pub struct EnumOutput { + pub name: String, + pub source_comment: String, + pub has_default: bool, + pub is_custom_str: bool, + pub members: Vec, +} + +pub struct EnumStructMemberOutput { + pub name: String, + pub value: i32, + pub is_first: bool, +} + +/// Data for rendering a union. +pub struct UnionOutput { + pub name: String, + pub source_comment: String, + pub has_default: bool, + pub is_custom_str: bool, + pub discriminant_type: String, + pub discriminant_is_builtin: bool, + pub arms: Vec, + /// For default impl: case name of the first arm. + pub first_arm_case_name: String, + /// For default impl: type to call ::default() on (read_call of first arm, if non-void) + pub first_arm_type: Option, +} + +pub struct UnionArmOutput { + pub case_name: String, + pub case_value: String, + pub is_void: bool, + pub type_ref: Option, + pub read_call: Option, + /// The serde_as type for i64/u64 fields (e.g., "NumberOrString"). + pub serde_as_type: Option, +} + +/// Data for a typedef that's a simple type alias. +pub struct TypedefAliasOutput { + pub name: String, + pub source_comment: String, + pub type_ref: String, +} + +/// Data for a typedef that's a newtype struct. +pub struct TypedefNewtypeOutput { + pub name: String, + pub source_comment: String, + pub has_default: bool, + pub is_var_array: bool, + pub is_fixed_opaque: bool, + pub is_fixed_array: bool, + pub is_custom_str: bool, + pub type_ref: String, + pub read_call: String, + /// The serde_as type for i64/u64 fields (e.g., "NumberOrString"). + pub serde_as_type: Option, + pub element_type: String, + pub size: Option, + /// Fixed opaque types have custom Debug impl (hex format). + pub custom_debug: bool, + pub custom_display_fromstr: bool, + pub custom_schemars: bool, +} + +/// Data for a const. +pub struct ConstOutput { + /// The Rust const name (SCREAMING_SNAKE_CASE). + pub name: String, + /// The name for the doc comment (UpperCamelCase). + pub doc_name: String, + pub source_comment: String, + /// The formatted value string (decimal or hex). + pub value_str: String, +} + +/// Data for the TypeVariant and Type enums. +pub struct TypeEnumOutput { + pub types: Vec, +} + +/// Order type names so that parents appear before their children. +/// +/// Definitions in the generated file are in "nested before parent" order (so the +/// Rust compiler sees nested types before they're referenced), but the `TypeVariant` +/// enum needs the opposite: parents first, then children. This function builds a +/// parent-child tree and traverses it parent-first while preserving definition order +/// among siblings. +fn order_types_parent_first(all_type_names: &[(String, Option)]) -> Vec { + let mut children_of: HashMap> = HashMap::new(); + for (name, parent) in all_type_names { + if let Some(ref parent_name) = parent { + children_of + .entry(parent_name.clone()) + .or_default() + .push(name.clone()); + } + } + + let mut types: Vec = Vec::new(); + let mut added: HashSet = HashSet::new(); + + fn add_type_and_children( + name: &str, + types: &mut Vec, + added: &mut HashSet, + children_of: &HashMap>, + ) { + if added.contains(name) { + return; + } + added.insert(name.to_string()); + types.push(name.to_string()); + + if let Some(children) = children_of.get(name) { + for child in children { + add_type_and_children(child, types, added, children_of); + } + } + } + + // Process types in definition order, but only add root types (no parent) + // and let recursion handle adding children + for (name, parent) in all_type_names { + if parent.is_none() { + add_type_and_children(name, &mut types, &mut added, &children_of); + } + } + + // Add any remaining types that weren't reached (shouldn't happen, but just in case) + for (name, _) in all_type_names { + if !added.contains(name) { + types.push(name.clone()); + } + } + + types +} + +fn format_source_comment(source: &str, kind: &str) -> String { + if source.is_empty() { + return String::new(); + } + // Filter out empty lines at the start and trim the source + let trimmed = source.trim(); + let lines: Vec<&str> = trimmed.lines().collect(); + let formatted: Vec = lines.iter().map(|l| format!("/// {l}")).collect(); + // The template outputs `/// {name}` so we start with ` is an XDR` (note the space) + format!( + " is an XDR {kind} defined as:\n///\n/// ```text\n{}\n/// ```\n///", + formatted.join("\n") + ) +} + +/// Find the common prefix to strip from enum member names. +/// Returns the prefix up to and including the last underscore that is common to all names. +fn find_common_prefix<'a>(names: &[&'a str]) -> &'a str { + if names.is_empty() || names.len() == 1 { + return ""; + } + + let first = names[0]; + + // Find longest common prefix among all names + let common_len = names.iter().skip(1).fold(first.len(), |len, name| { + first + .bytes() + .zip(name.bytes()) + .take(len) + .take_while(|(a, b)| a == b) + .count() + }); + + let common = &first[..common_len]; + + // Find the last underscore in the common prefix + // If found, strip up to and including the underscore + if let Some(last_underscore) = common.rfind('_') { + &first[..=last_underscore] + } else { + "" + } +} + +/// Strip common prefix from an enum member name. +/// If the result would start with a digit, keep the first letter of the prefix. +fn strip_prefix(name: &str, prefix: &str) -> String { + if !prefix.is_empty() && name.starts_with(prefix) { + let stripped = &name[prefix.len()..]; + // If result starts with digit, keep the first letter of the prefix + // e.g., "BINARY_FUSE_FILTER_8_BIT" with prefix "BINARY_FUSE_FILTER_" -> "B8_BIT" + if stripped.chars().next().is_some_and(|c| c.is_ascii_digit()) { + if let Some(first_char) = prefix.chars().next() { + return format!("{first_char}{stripped}"); + } + } + stripped.to_string() + } else { + name.to_string() + } +} diff --git a/xdr-generator-rust/src/lexer.rs b/xdr-generator-rust/src/lexer.rs new file mode 100644 index 00000000..1e9f34c0 --- /dev/null +++ b/xdr-generator-rust/src/lexer.rs @@ -0,0 +1,279 @@ +//! XDR lexer (tokenizer) using Logos. + +use logos::{Logos, SpannedIter}; +use thiserror::Error; + +/// Token type for XDR lexing. +#[derive(Logos, Debug, Clone, PartialEq, Eq)] +#[logos(skip r"[ \t\n\r\f]+")] // Skip whitespace +#[logos(skip r"//[^\n]*")] // Skip line comments +#[logos(skip r"/\*[^*]*\*+(?:[^/*][^*]*\*+)*/")] // Skip block comments +#[logos(skip r"%[^\n]*\n?")] // Skip preprocessor directives +pub enum Token { + // Keywords + #[token("struct")] + Struct, + #[token("enum")] + Enum, + #[token("union")] + Union, + #[token("typedef")] + Typedef, + #[token("const")] + Const, + #[token("namespace")] + Namespace, + #[token("switch")] + Switch, + #[token("case")] + Case, + #[token("default")] + Default, + #[token("void")] + Void, + #[token("unsigned")] + Unsigned, + #[token("int")] + Int, + #[token("hyper")] + Hyper, + #[token("float")] + Float, + #[token("double")] + Double, + #[token("bool")] + Bool, + #[token("opaque")] + Opaque, + #[token("string")] + String, + + // Identifiers + #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice().to_string())] + Ident(std::string::String), + + // Integer literals - hex must have higher priority than decimal + #[regex(r"0x[0-9a-fA-F]+", parse_hex, priority = 2)] + #[regex(r"-?[0-9]+", parse_decimal, priority = 1)] + IntLiteral((i64, IntBase)), + + // Symbols + #[token("{")] + LBrace, + #[token("}")] + RBrace, + #[token("[")] + LBracket, + #[token("]")] + RBracket, + #[token("<")] + LAngle, + #[token(">")] + RAngle, + #[token("(")] + LParen, + #[token(")")] + RParen, + #[token(";")] + Semi, + #[token(":")] + Colon, + #[token(",")] + Comma, + #[token("*")] + Star, + #[token("=")] + Eq, + + // End of file (not produced by Logos, added manually) + Eof, +} + +/// The base (radix) of an integer literal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IntBase { + Decimal, + Hexadecimal, +} + +fn parse_hex(lex: &logos::Lexer) -> Option<(i64, IntBase)> { + let slice = lex.slice(); + // Parse as u64 first to handle the full range of hex values (e.g., 0xFFFFFFFFFFFFFFFF), + // then reinterpret as i64. This preserves the bit pattern for large unsigned values. + u64::from_str_radix(&slice[2..], 16) + .ok() + .map(|v| (v as i64, IntBase::Hexadecimal)) +} + +fn parse_decimal(lex: &logos::Lexer) -> Option<(i64, IntBase)> { + lex.slice().parse().ok().map(|v| (v, IntBase::Decimal)) +} + +/// A token with its byte span in the source. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpannedToken { + pub token: Token, + pub start: usize, + pub end: usize, +} + +#[derive(Debug, Error)] +pub enum LexError { + #[error("unexpected character: {0}")] + UnexpectedChar(char), + #[error("lexer error at position {0}")] + LexerError(usize), +} + +/// The main lexer for tokenizing XDR source text. +pub struct Lexer<'a> { + source: &'a str, + inner: SpannedIter<'a, Token>, +} + +impl<'a> Lexer<'a> { + pub fn new(input: &'a str) -> Self { + Self { + source: input, + inner: Token::lexer(input).spanned(), + } + } + + /// Tokenize with span information for each token. + pub fn tokenize_with_spans(self) -> Result<(Vec, std::string::String), LexError> { + let source = self.source.to_string(); + let source_len = self.source.len(); + let mut tokens = Vec::new(); + + for (result, span) in self.inner { + let token = match result { + Ok(t) => t, + Err(()) => { + if let Some(c) = self.source[span.start..].chars().next() { + return Err(LexError::UnexpectedChar(c)); + } + return Err(LexError::LexerError(span.start)); + } + }; + + tokens.push(SpannedToken { + token, + start: span.start, + end: span.end, + }); + } + + // Add EOF token + tokens.push(SpannedToken { + token: Token::Eof, + start: source_len, + end: source_len, + }); + + Ok((tokens, source)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simple() { + let input = "struct Foo { int x; };"; + let lexer = Lexer::new(input); + let (spanned_tokens, _) = lexer.tokenize_with_spans().unwrap(); + let tokens: Vec = spanned_tokens.into_iter().map(|st| st.token).collect(); + assert_eq!( + tokens, + vec![ + Token::Struct, + Token::Ident("Foo".into()), + Token::LBrace, + Token::Int, + Token::Ident("x".into()), + Token::Semi, + Token::RBrace, + Token::Semi, + Token::Eof, + ] + ); + } + + #[test] + fn test_comments() { + let input = r#" + // line comment + struct /* block comment */ Foo { }; + "#; + let lexer = Lexer::new(input); + let (spanned_tokens, _) = lexer.tokenize_with_spans().unwrap(); + let tokens: Vec = spanned_tokens.into_iter().map(|st| st.token).collect(); + assert_eq!( + tokens, + vec![ + Token::Struct, + Token::Ident("Foo".into()), + Token::LBrace, + Token::RBrace, + Token::Semi, + Token::Eof, + ] + ); + } + + #[test] + fn test_hex() { + let input = "KEY_TYPE_MUXED_ED25519 = 0x100"; + let lexer = Lexer::new(input); + let (spanned_tokens, _) = lexer.tokenize_with_spans().unwrap(); + let tokens: Vec = spanned_tokens.into_iter().map(|st| st.token).collect(); + assert_eq!( + tokens, + vec![ + Token::Ident("KEY_TYPE_MUXED_ED25519".into()), + Token::Eq, + Token::IntLiteral((256, IntBase::Hexadecimal)), + Token::Eof, + ] + ); + } + + #[test] + fn test_negative_number() { + let input = "const FOO = -1;"; + let lexer = Lexer::new(input); + let (spanned_tokens, _) = lexer.tokenize_with_spans().unwrap(); + let tokens: Vec = spanned_tokens.into_iter().map(|st| st.token).collect(); + assert_eq!( + tokens, + vec![ + Token::Const, + Token::Ident("FOO".into()), + Token::Eq, + Token::IntLiteral((-1, IntBase::Decimal)), + Token::Semi, + Token::Eof, + ] + ); + } + + #[test] + fn test_preprocessor() { + let input = "%#include \"xdr.h\"\nstruct Foo {};"; + let lexer = Lexer::new(input); + let (spanned_tokens, _) = lexer.tokenize_with_spans().unwrap(); + let tokens: Vec = spanned_tokens.into_iter().map(|st| st.token).collect(); + assert_eq!( + tokens, + vec![ + Token::Struct, + Token::Ident("Foo".into()), + Token::LBrace, + Token::RBrace, + Token::Semi, + Token::Eof, + ] + ); + } +} diff --git a/xdr-generator-rust/src/lib.rs b/xdr-generator-rust/src/lib.rs new file mode 100644 index 00000000..0bbfdd78 --- /dev/null +++ b/xdr-generator-rust/src/lib.rs @@ -0,0 +1,5 @@ +pub mod ast; +pub mod generator; +pub mod lexer; +pub mod parser; +pub mod types; diff --git a/xdr-generator-rust/src/main.rs b/xdr-generator-rust/src/main.rs new file mode 100644 index 00000000..33553195 --- /dev/null +++ b/xdr-generator-rust/src/main.rs @@ -0,0 +1,94 @@ +//! CLI entry point for the XDR to Rust code generator. + +use askama::Template; +use clap::Parser; +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; +use xdr_generator_rust::generator::Generator; +use xdr_generator_rust::parser; +use xdr_generator_rust::types::Options; + +/// XDR to Rust code generator. +#[derive(Parser, Debug)] +#[command(name = "xdr-generator-rust")] +#[command(about = "Generate Rust code from XDR definitions")] +struct Args { + /// Input XDR files + #[arg(short, long, required = true)] + input: Vec, + + /// Output Rust file + #[arg(short, long)] + output: PathBuf, + + /// Types with custom Default implementation (skip derive(Default)) + #[arg(long, value_delimiter = ',')] + custom_default: Vec, + + /// Types with custom FromStr/Display implementation (use SerializeDisplay) + #[arg(long, value_delimiter = ',')] + custom_str: Vec, + + /// Types that should NOT have Display/FromStr/schemars generated (have special handling elsewhere) + #[arg(long, value_delimiter = ',')] + no_display_fromstr: Vec, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + // Read all input files and sort by filename (ASCII byte order, matching Ruby's sort) + 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)); + } + // Sort by filename to match Ruby's Dir.glob().sort behavior + files.sort_by(|a, b| a.0.cmp(&b.0)); + + // Build combined XDR in sorted order + let mut combined_xdr = String::new(); + for (_, content) in &files { + combined_xdr.push_str(content); + combined_xdr.push('\n'); + } + + // Build input_files list (same order as files for SHA256 hashes) + let input_files: Vec<(String, String)> = files + .iter() + .map(|(path, content)| (path.to_string_lossy().to_string(), content.clone())) + .collect(); + + // Parse the combined XDR + let spec = parser::parse(&combined_xdr)?; + + // Read the header file + let header_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("xdr-generator") + .join("generator") + .join("header.rs"); + let header = fs::read_to_string(&header_path)?; + + // Build options + let options = Options { + 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::>(), + }; + + // Generate the output + let generator = Generator::new(&spec, options); + let template = generator.generate(&spec, &input_files, &header); + + // Render the template + let output = template.render()?; + + // Write the output + fs::write(&args.output, output)?; + + eprintln!("Generated: {}", args.output.display()); + + Ok(()) +} diff --git a/xdr-generator-rust/src/parser.rs b/xdr-generator-rust/src/parser.rs new file mode 100644 index 00000000..c84ca401 --- /dev/null +++ b/xdr-generator-rust/src/parser.rs @@ -0,0 +1,1090 @@ +//! XDR parser (recursive descent). + +use std::collections::HashMap; + +use crate::ast::*; +use crate::lexer::{IntBase, LexError, Lexer, SpannedToken, Token}; +use heck::ToUpperCamelCase; +use thiserror::Error; + +/// Parse XDR source text into an AST. +pub fn parse(source: &str) -> Result { + let mut parser = Parser::new(source)?; + parser.parse() +} + +struct Parser { + tokens: Vec, + pos: usize, + /// The original source text, for extracting source snippets + source: String, + /// Track the token position of the current definition start for source extraction + def_start_pos: usize, + /// Extracted nested type definitions (anonymous unions, inline structs) + extracted_definitions: Vec, + /// Root parent type name for generating nested type names + root_parent: Option, + /// Global map of enum member names and const names to their values + global_values: HashMap, +} + +impl Parser { + fn new(source: &str) -> Result { + let lexer = Lexer::new(source); + let (tokens, source) = lexer.tokenize_with_spans()?; + Ok(Self { + tokens, + pos: 0, + source, + def_start_pos: 0, + extracted_definitions: Vec::new(), + root_parent: None, + global_values: HashMap::new(), + }) + } + + /// Parse the entire input. + fn parse(&mut self) -> Result { + let mut spec = XdrSpec::default(); + + while *self.peek() != Token::Eof { + // Skip any extra semicolons at the top level + while *self.peek() == Token::Semi { + self.advance(); + } + if *self.peek() == Token::Eof { + break; + } + match self.peek() { + Token::Namespace => { + let ns = self.parse_namespace()?; + spec.namespaces.push(ns); + } + _ => { + self.parse_definition_into(&mut spec.definitions)?; + } + } + } + + // Any remaining extracted definitions (shouldn't be any, but just in case) + for extracted in self.extracted_definitions.drain(..) { + spec.definitions.push(extracted); + } + + Ok(spec) + } + + fn parse_namespace(&mut self) -> Result { + self.expect(Token::Namespace)?; + let name = self.expect_ident()?; + self.expect(Token::LBrace)?; + + let mut definitions = Vec::new(); + while *self.peek() != Token::RBrace && *self.peek() != Token::Eof { + // Skip any extra semicolons + while *self.peek() == Token::Semi { + self.advance(); + } + if *self.peek() == Token::RBrace { + break; + } + + self.parse_definition_into(&mut definitions)?; + } + + self.expect(Token::RBrace)?; + + Ok(Namespace { name, definitions }) + } + + /// Parse a single definition, prepending any extracted nested definitions + /// (anonymous unions, inline structs) so they appear just before their parent. + fn parse_definition_into(&mut self, out: &mut Vec) -> Result<(), ParseError> { + let extract_start = self.extracted_definitions.len(); + + let def = self.parse_definition()?; + + // Insert any newly extracted definitions before this definition + for extracted in self.extracted_definitions.drain(extract_start..) { + out.push(extracted); + } + + out.push(def); + Ok(()) + } + + fn parse_definition(&mut self) -> Result { + // Mark the start of this definition for source extraction + self.def_start_pos = self.pos; + + match self.peek() { + Token::Struct => self.parse_struct().map(Definition::Struct), + Token::Enum => self.parse_enum().map(Definition::Enum), + Token::Union => self.parse_union().map(Definition::Union), + Token::Typedef => self.parse_typedef().map(Definition::Typedef), + Token::Const => self.parse_const().map(Definition::Const), + other => Err(self.unexpected_token_error( + "struct, enum, union, typedef, or const".to_string(), + other.clone(), + )), + } + } + + fn parse_struct(&mut self) -> Result { + self.expect(Token::Struct)?; + let name = self.expect_ident()?; + self.expect(Token::LBrace)?; + + // Set root_parent for nested type name generation + let prev_root = self.root_parent.take(); + self.root_parent = Some(name.clone()); + + let mut members = Vec::new(); + while *self.peek() != Token::RBrace { + let member = self.parse_member()?; + members.push(member); + self.expect(Token::Semi)?; + } + + // Restore previous root_parent + self.root_parent = prev_root; + + self.expect(Token::RBrace)?; + self.expect(Token::Semi)?; + + // Extract source text (simplified - just use the name for now) + let source = self.extract_definition_source(); + + Ok(Struct { + name, + members, + source, + is_nested: false, + parent: None, + }) + } + + fn parse_enum(&mut self) -> Result { + self.expect(Token::Enum)?; + let name = self.expect_ident()?; + self.expect(Token::LBrace)?; + + let mut members = Vec::new(); + loop { + let member_name = self.expect_ident()?; + self.expect(Token::Eq)?; + + // Value can be integer or identifier (reference to another enum value) + let value = match self.peek().clone() { + Token::IntLiteral((value, _)) => { + self.advance(); + self.try_i64_to_i32(value)? + } + Token::Ident(ref_name) => { + self.advance(); + self.resolve_enum_value(&ref_name, &members)? + } + other => { + return Err( + self.unexpected_token_error("integer or identifier".to_string(), other) + ) + } + }; + + members.push(EnumMember { + name: member_name, + value, + }); + + match self.peek() { + Token::Comma => { + self.advance(); + if *self.peek() == Token::RBrace { + break; + } + } + Token::RBrace => break, + other => { + return Err(self.unexpected_token_error(", or }".to_string(), other.clone())) + } + } + } + + self.expect(Token::RBrace)?; + self.expect(Token::Semi)?; + + let source = self.extract_definition_source(); + + // Add all members to global_values for cross-enum resolution + for m in &members { + self.global_values.insert(m.name.clone(), m.value as i64); + } + + Ok(Enum { + name, + members, + source, + }) + } + + fn parse_union(&mut self) -> Result { + self.expect(Token::Union)?; + let name = self.expect_ident()?; + self.expect(Token::Switch)?; + self.expect(Token::LParen)?; + + // Parse discriminant + let disc_type = self.parse_type()?; + let disc_name = self.expect_ident()?; + + self.expect(Token::RParen)?; + self.expect(Token::LBrace)?; + + // Set root_parent for inline struct extraction + let prev_root = self.root_parent.take(); + self.root_parent = Some(name.clone()); + + let arms = self.parse_union_body()?; + + // Restore previous root_parent + self.root_parent = prev_root; + + self.expect(Token::RBrace)?; + self.expect(Token::Semi)?; + + let source = self.extract_definition_source(); + + Ok(Union { + name, + discriminant: UnionDiscriminant { + name: disc_name, + type_: disc_type, + }, + arms, + source, + is_nested: false, + parent: None, + }) + } + + fn parse_typedef(&mut self) -> Result { + self.expect(Token::Typedef)?; + + let type_ = self.parse_type()?; + let name = self.expect_ident()?; + + // Handle array suffix on typedef name + let type_ = self.parse_type_suffix(type_)?; + + self.expect(Token::Semi)?; + + let source = self.extract_definition_source(); + + Ok(Typedef { + name, + type_, + source, + }) + } + + fn parse_const(&mut self) -> Result { + self.expect(Token::Const)?; + let name = self.expect_ident()?; + self.expect(Token::Eq)?; + let (value, base) = self.expect_int_with_base()?; + self.expect(Token::Semi)?; + + let source = self.extract_definition_source(); + + // Add const value to global_values for enum reference resolution + self.global_values.insert(name.clone(), value); + + Ok(Const { + name, + value, + base, + source, + }) + } + + fn parse_member(&mut self) -> Result { + let ctx = self.type_parse_context(); + let parsed = self.parse_type_or_anon_union()?; + let type_end_byte = self.prev_end_byte(); + + let name = self.expect_ident()?; + + let type_ = match parsed { + ParsedType::AnonymousUnion { discriminant, arms } => { + self.extract_anonymous_union(discriminant, arms, &name, &ctx, type_end_byte) + } + ParsedType::Type(t) => self.parse_type_suffix(t)?, + }; + + Ok(StructMember { name, type_ }) + } + + /// Parse the body of a union (the arms inside the braces). + /// Default arms are not supported — the generated code always emits a + /// catch-all `_ => Err(Error::Invalid)`. If a default arm is encountered, + /// a parse error is returned. + fn parse_union_body(&mut self) -> Result, ParseError> { + let mut arms = Vec::new(); + + while *self.peek() != Token::RBrace { + let arm = self.parse_union_arm()?; + if arm.cases.is_empty() { + let (line, col) = self.current_position(); + return Err(ParseError::UnsupportedDefaultArm { line, col }); + } + arms.push(arm); + } + + Ok(arms) + } + + fn parse_union_arm(&mut self) -> Result { + let mut cases = Vec::new(); + + // Parse case(s) - multiple cases can share the same arm + loop { + match self.peek() { + Token::Case => { + self.advance(); + let value = match self.peek().clone() { + Token::Ident(name) => { + self.advance(); + UnionCaseValue::Ident(name) + } + Token::IntLiteral((value, _)) => { + self.advance(); + UnionCaseValue::Literal(self.try_i64_to_i32(value)?) + } + other => { + return Err(self.unexpected_token_error("case value".to_string(), other)) + } + }; + self.expect(Token::Colon)?; + cases.push(UnionCase { value }); + } + Token::Default => { + self.advance(); + self.expect(Token::Colon)?; + // Default has no cases + break; + } + _ => break, + } + } + + // Parse the arm type + let type_ = if *self.peek() == Token::Void { + self.advance(); + self.expect(Token::Semi)?; + None + } else if *self.peek() == Token::Struct { + // Inline struct in a union arm. XDR syntax: + // case FOO: struct { int x; } fieldName; + // + // We need the field name *before* parsing the struct body so we can + // set root_parent correctly (nested types inside the struct derive + // their names from it). This requires a two-pass approach: + // Pass 1 (lookahead): skip the struct body by counting braces, + // read the field name and semicolon, record positions. + // Pass 2 (rewind): rewind into the struct body and parse + // members with root_parent set to the generated name, + // then skip forward past the already-consumed tokens. + Some(self.parse_inline_struct()?) + } else { + let ctx = self.type_parse_context(); + let parsed = self.parse_type_or_anon_union()?; + let type_end_byte = self.prev_end_byte(); + + // The field name is consumed but not stored in UnionArm — the + // generated Rust enum variant names come from the case values, not + // the field name. The field name is only used below for naming + // extracted anonymous unions. + let field_name = self.expect_ident()?; + + let type_ = match parsed { + ParsedType::AnonymousUnion { discriminant, arms } => self.extract_anonymous_union( + discriminant, + arms, + &field_name, + &ctx, + type_end_byte, + ), + ParsedType::Type(t) => self.parse_type_suffix(t)?, + }; + self.expect(Token::Semi)?; + + Some(type_) + }; + + Ok(UnionArm { cases, type_ }) + } + + /// Parse an inline struct definition inside a union arm and extract it as a + /// separate named type. Returns a `Type::Ident` referencing the extracted struct. + /// + /// Expects the parser to be positioned at the `struct` keyword. + fn parse_inline_struct(&mut self) -> Result { + // Record position before 'struct' keyword for source extraction + let source_start_byte = self.current_start_byte(); + + self.advance(); // consume 'struct' + self.expect(Token::LBrace)?; + + // --- Pass 1: lookahead to find the field name after the struct body --- + let body_start_pos = self.pos; + let mut brace_depth = 1; + while brace_depth > 0 { + match self.advance() { + Token::LBrace => brace_depth += 1, + Token::RBrace => brace_depth -= 1, + Token::Eof => return Err(ParseError::UnexpectedEof), + _ => {} + } + } + + let source_end_byte = self.prev_end_byte(); + + let field_name = self.expect_ident()?; + self.expect(Token::Semi)?; + let after_semi_pos = self.pos; + + // --- Pass 2: rewind and parse the struct body properly --- + self.pos = body_start_pos; + + let struct_name = if let Some(ref parent) = self.root_parent { + generate_nested_type_name(parent, &field_name) + } else { + field_name.to_upper_camel_case() + }; + + let prev_root = self.root_parent.take(); + self.root_parent = Some(struct_name.clone()); + + let mut members = Vec::new(); + while *self.peek() != Token::RBrace { + let member = self.parse_member()?; + members.push(member); + self.expect(Token::Semi)?; + } + self.expect(Token::RBrace)?; + + self.root_parent = prev_root; + + // Advance past the field name and semicolon already consumed in pass 1 + self.pos = after_semi_pos; + + let source = self.source_slice(source_start_byte, source_end_byte); + + let struct_def = Struct { + name: struct_name.clone(), + members, + source, + is_nested: true, + parent: self.root_parent.clone(), + }; + + self.extracted_definitions + .push(Definition::Struct(struct_def)); + + Ok(Type::Ident(struct_name)) + } + + /// Parse a type that must be a regular type (not an anonymous union). + /// Used for discriminants, typedefs, and other contexts where inline + /// unions cannot appear. + fn parse_type(&mut self) -> Result { + match self.parse_type_or_anon_union()? { + ParsedType::Type(t) => Ok(t), + ParsedType::AnonymousUnion { .. } => { + Err(self.unexpected_token_error("type".to_string(), Token::Union)) + } + } + } + + /// Parse a type expression, which may be an anonymous union. + /// Callers must handle the `ParsedType::AnonymousUnion` case by + /// extracting it into a named definition. + fn parse_type_or_anon_union(&mut self) -> Result { + match self.peek().clone() { + Token::Int => { + self.advance(); + Ok(ParsedType::Type(Type::Int)) + } + Token::Unsigned => { + self.advance(); + match self.peek() { + Token::Int => { + self.advance(); + Ok(ParsedType::Type(Type::UnsignedInt)) + } + Token::Hyper => { + self.advance(); + Ok(ParsedType::Type(Type::UnsignedHyper)) + } + other => { + Err(self.unexpected_token_error("int or hyper".to_string(), other.clone())) + } + } + } + Token::Hyper => { + self.advance(); + Ok(ParsedType::Type(Type::Hyper)) + } + Token::Float => { + self.advance(); + Ok(ParsedType::Type(Type::Float)) + } + Token::Double => { + self.advance(); + Ok(ParsedType::Type(Type::Double)) + } + Token::Bool => { + self.advance(); + Ok(ParsedType::Type(Type::Bool)) + } + Token::Opaque => { + self.advance(); + self.parse_opaque_suffix().map(ParsedType::Type) + } + Token::String => { + self.advance(); + self.parse_string_suffix().map(ParsedType::Type) + } + Token::Union => { + // Anonymous union inside struct + // union switch (type name) { ... } + self.advance(); + self.expect(Token::Switch)?; + self.expect(Token::LParen)?; + let disc_type = self.parse_type()?; + let disc_name = self.expect_ident()?; + self.expect(Token::RParen)?; + self.expect(Token::LBrace)?; + + let arms = self.parse_union_body()?; + self.expect(Token::RBrace)?; + + Ok(ParsedType::AnonymousUnion { + discriminant: UnionDiscriminant { + name: disc_name, + type_: disc_type, + }, + arms, + }) + } + Token::Ident(name) => { + self.advance(); + // Handle built-in type aliases + let base_type = match name.as_str() { + "uint64" => Type::UnsignedHyper, + "int64" => Type::Hyper, + "uint32" => Type::UnsignedInt, + "int32" => Type::Int, + "TRUE" | "FALSE" => Type::Bool, + _ => Type::Ident(name), + }; + // Check for optional type suffix (Type* field) + if *self.peek() == Token::Star { + self.advance(); + Ok(ParsedType::Type(Type::Optional(Box::new(base_type)))) + } else { + Ok(ParsedType::Type(base_type)) + } + } + other => Err(self.unexpected_token_error("type".to_string(), other)), + } + } + + fn parse_type_suffix(&mut self, base: Type) -> Result { + match self.peek() { + Token::LBracket => { + self.advance(); + let size = self.parse_size()?; + self.expect(Token::RBracket)?; + + // Special case: opaque name[size] or string name[size] + // means fixed opaque/string, not an array of opaque/string + match base { + Type::OpaqueVar(None) => Ok(Type::OpaqueFixed(size)), + Type::String(None) => Ok(Type::OpaqueFixed(size)), // string with fixed size is opaque + _ => Ok(Type::Array { + element_type: Box::new(base), + size, + }), + } + } + Token::LAngle => { + self.advance(); + let max = if *self.peek() == Token::RAngle { + None + } else { + Some(self.parse_size()?) + }; + self.expect(Token::RAngle)?; + + // Special case: opaque name or string name + // means variable opaque/string with max, not a var array + match base { + Type::OpaqueVar(None) => Ok(Type::OpaqueVar(max)), + Type::String(None) => Ok(Type::String(max)), + _ => Ok(Type::VarArray { + element_type: Box::new(base), + max_size: max, + }), + } + } + Token::Star => { + // Optional: type *name + self.advance(); + Ok(Type::Optional(Box::new(base))) + } + _ => Ok(base), + } + } + + fn parse_opaque_suffix(&mut self) -> Result { + match self.peek() { + Token::LBracket => { + // Fixed: opaque[size] + self.advance(); + let size = self.parse_size()?; + self.expect(Token::RBracket)?; + Ok(Type::OpaqueFixed(size)) + } + Token::LAngle => { + // Variable: opaque or opaque<> + self.advance(); + let max = if *self.peek() == Token::RAngle { + None + } else { + Some(self.parse_size()?) + }; + self.expect(Token::RAngle)?; + Ok(Type::OpaqueVar(max)) + } + _ => { + // Bare opaque - variable with no max (rare) + Ok(Type::OpaqueVar(None)) + } + } + } + + fn parse_string_suffix(&mut self) -> Result { + match self.peek() { + Token::LAngle => { + self.advance(); + let max = if *self.peek() == Token::RAngle { + None + } else { + Some(self.parse_size()?) + }; + self.expect(Token::RAngle)?; + Ok(Type::String(max)) + } + _ => Ok(Type::String(None)), + } + } + + fn parse_size(&mut self) -> Result { + match self.peek().clone() { + Token::IntLiteral((value, _)) => { + self.advance(); + Ok(Size::Literal(self.try_i64_to_u32(value)?)) + } + Token::Ident(name) => { + self.advance(); + Ok(Size::Named(name)) + } + other => { + Err(self.unexpected_token_error("size (integer or identifier)".to_string(), other)) + } + } + } + + fn peek(&self) -> &Token { + self.tokens + .get(self.pos) + .map(|st| &st.token) + .unwrap_or(&Token::Eof) + } + + fn advance(&mut self) -> &Token { + let token = self + .tokens + .get(self.pos) + .map(|st| &st.token) + .unwrap_or(&Token::Eof); + self.pos += 1; + token + } + + /// Get the byte offset where the current token starts. + fn current_start_byte(&self) -> usize { + self.tokens.get(self.pos).map(|st| st.start).unwrap_or(0) + } + + /// Get the byte offset where the previous token ends. + fn prev_end_byte(&self) -> usize { + if self.pos > 0 { + self.tokens + .get(self.pos - 1) + .map(|st| st.end) + .unwrap_or(self.source.len()) + } else { + 0 + } + } + + /// Extract a source slice between two byte offsets. + fn source_slice(&self, start: usize, end: usize) -> String { + debug_assert!( + start < end && end <= self.source.len(), + "source_slice out of bounds: start={start}, end={end}, len={}", + self.source.len() + ); + if start < end && end <= self.source.len() { + self.source[start..end].to_string() + } else { + String::new() + } + } + + /// Compute the (line, column) for the current token position, both 1-based. + fn current_position(&self) -> (usize, usize) { + let byte_offset = self + .tokens + .get(self.pos.saturating_sub(1)) + .map(|st| st.start) + .unwrap_or(self.source.len()); + let prefix = &self.source[..byte_offset.min(self.source.len())]; + let line = prefix.chars().filter(|&c| c == '\n').count() + 1; + let col = match prefix.rfind('\n') { + Some(nl) => byte_offset - nl, + None => byte_offset + 1, + }; + (line, col) + } + + /// Try to convert an i64 to the target integer type, returning an error with position on overflow. + fn try_i64_to_i32(&self, value: i64) -> Result { + i32::try_from(value).map_err(|_| { + let (line, col) = self.current_position(); + ParseError::IntegerOverflow { value, line, col } + }) + } + + fn try_i64_to_u32(&self, value: i64) -> Result { + u32::try_from(value).map_err(|_| { + let (line, col) = self.current_position(); + ParseError::IntegerOverflow { value, line, col } + }) + } + + /// Create an `UnexpectedToken` error with the current position. + fn unexpected_token_error(&self, expected: String, got: Token) -> ParseError { + let (line, col) = self.current_position(); + ParseError::UnexpectedToken { + expected, + got, + line, + col, + } + } + + fn expect(&mut self, expected: Token) -> Result<(), ParseError> { + let token = self.advance().clone(); + if token == expected { + Ok(()) + } else { + Err(self.unexpected_token_error(format!("{expected:?}"), token)) + } + } + + fn expect_ident(&mut self) -> Result { + let token = self.advance().clone(); + match token { + Token::Ident(s) => Ok(s), + _ => Err(self.unexpected_token_error("identifier".to_string(), token)), + } + } + + /// Parse an integer literal, returning both the value and whether it was in hex format. + fn expect_int_with_base(&mut self) -> Result<(i64, IntBase), ParseError> { + let token = self.advance().clone(); + match token { + Token::IntLiteral((value, base)) => Ok((value, base)), + _ => Err(self.unexpected_token_error("integer literal".to_string(), token)), + } + } + + /// Snapshot the parser state needed before parsing a type expression, so + /// that anonymous unions can later be extracted with correct source spans + /// and parent fixups. + fn type_parse_context(&self) -> TypeParseContext { + TypeParseContext { + start_byte: self.current_start_byte(), + extract_start_idx: self.extracted_definitions.len(), + } + } + + /// Extract an anonymous union as a named `Union` definition and return + /// a `Type::Ident` referencing it. + /// + /// `field_name` is the field that holds the union (used for name generation). + /// `ctx` captures the parser state from before the type was parsed. + /// `type_end_byte` is the byte offset where the type expression ended. + fn extract_anonymous_union( + &mut self, + discriminant: UnionDiscriminant, + arms: Vec, + field_name: &str, + ctx: &TypeParseContext, + type_end_byte: usize, + ) -> Type { + // Generate the name: root_parent + field_name + let union_name = if let Some(ref parent) = self.root_parent { + generate_nested_type_name(parent, field_name) + } else { + generate_nested_type_name(field_name, "Union") + }; + + // Extract source text for the anonymous union + let source = self.source_slice(ctx.start_byte, type_end_byte); + + let union_def = Union { + name: union_name.clone(), + discriminant, + arms, + source, + is_nested: true, + parent: self.root_parent.clone(), + }; + + // Fix up parent relationships for any definitions extracted during union parsing + // (e.g., inline structs inside union arms should have this union as their parent) + // Only update if current parent is the root_parent (not already a more specific parent) + for def in &mut self.extracted_definitions[ctx.extract_start_idx..] { + match def { + Definition::Struct(s) if s.is_nested && s.parent == self.root_parent => { + s.parent = Some(union_name.clone()); + } + Definition::Union(u) if u.is_nested && u.parent == self.root_parent => { + u.parent = Some(union_name.clone()); + } + _ => {} + } + } + + // Add to extracted definitions + self.extracted_definitions + .push(Definition::Union(union_def)); + + // Return a reference to the extracted type + Type::Ident(union_name) + } + + /// Resolve an enum value reference, searching the current enum members + /// and then previously parsed enums/consts. + fn resolve_enum_value(&self, name: &str, members: &[EnumMember]) -> Result { + // First check if it's in the current enum being parsed + for m in members { + if m.name == name { + return Ok(m.value); + } + } + // Check global values (previously parsed enums and consts) + if let Some(&value) = self.global_values.get(name) { + return self.try_i64_to_i32(value); + } + let (line, col) = self.current_position(); + Err(ParseError::UnresolvedEnumValue { + name: name.to_string(), + line, + col, + }) + } + + /// Extract the source text for a definition using the tracked start position. + fn extract_definition_source(&self) -> String { + let start_byte = self + .tokens + .get(self.def_start_pos) + .map(|st| st.start) + .unwrap_or(0); + let end_byte = self.prev_end_byte(); + self.source_slice(start_byte, end_byte) + } +} + +#[derive(Debug, Error)] +pub enum ParseError { + #[error("lexer error: {0}")] + Lex(#[from] LexError), + #[error("{line}:{col}: unexpected token: expected {expected}, got {got:?}")] + UnexpectedToken { + expected: String, + got: Token, + line: usize, + col: usize, + }, + #[error("unexpected end of file")] + UnexpectedEof, + #[error("{line}:{col}: unresolved enum value reference: {name}")] + UnresolvedEnumValue { + name: String, + line: usize, + col: usize, + }, + #[error("{line}:{col}: default arms in unions are not supported")] + UnsupportedDefaultArm { line: usize, col: usize }, + #[error("{line}:{col}: integer value {value} overflows target type")] + IntegerOverflow { value: i64, line: usize, col: usize }, +} + +/// Result of `parse_type`: either a regular AST type or an anonymous union +/// that must be extracted into a named definition before entering the AST. +enum ParsedType { + /// A regular type that can appear in the final AST. + Type(Type), + /// An inline `union switch (…) { … }` inside a struct. The parser + /// converts this to a named `Union` definition via `extract_anonymous_union` + /// and replaces it with a `Type::Ident` reference. + AnonymousUnion { + discriminant: UnionDiscriminant, + arms: Vec, + }, +} + +/// State captured before parsing a type expression, used when extracting +/// anonymous unions into named definitions. +struct TypeParseContext { + /// Byte offset where the type expression starts (for source extraction). + start_byte: usize, + /// Index into `extracted_definitions` before parsing (for parent fixup). + extract_start_idx: usize, +} + +/// Generate a nested type name from parent and field name. +fn generate_nested_type_name(parent: &str, field: &str) -> String { + format!("{}{}", parent, field.to_upper_camel_case()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_struct() { + let input = "struct Foo { int x; unsigned hyper y; };"; + let mut parser = Parser::new(input).unwrap(); + let spec = parser.parse().unwrap(); + + assert_eq!(spec.definitions.len(), 1); + if let Definition::Struct(s) = &spec.definitions[0] { + assert_eq!(s.name, "Foo"); + assert_eq!(s.members.len(), 2); + assert_eq!(s.members[0].name, "x"); + assert_eq!(s.members[0].type_, Type::Int); + assert_eq!(s.members[1].name, "y"); + assert_eq!(s.members[1].type_, Type::UnsignedHyper); + } else { + panic!("Expected struct"); + } + } + + #[test] + fn test_parse_enum() { + let input = "enum Color { RED = 0, GREEN = 1, BLUE = 2 };"; + let mut parser = Parser::new(input).unwrap(); + let spec = parser.parse().unwrap(); + + assert_eq!(spec.definitions.len(), 1); + if let Definition::Enum(e) = &spec.definitions[0] { + assert_eq!(e.name, "Color"); + assert_eq!(e.members.len(), 3); + assert_eq!(e.members[0].name, "RED"); + assert_eq!(e.members[0].value, 0); + } else { + panic!("Expected enum"); + } + } + + #[test] + fn test_parse_typedef() { + let input = "typedef opaque Hash[32];"; + let mut parser = Parser::new(input).unwrap(); + let spec = parser.parse().unwrap(); + + assert_eq!(spec.definitions.len(), 1); + if let Definition::Typedef(t) = &spec.definitions[0] { + assert_eq!(t.name, "Hash"); + assert_eq!(t.type_, Type::OpaqueFixed(Size::Literal(32))); + } else { + panic!("Expected typedef"); + } + } + + #[test] + fn test_parse_namespace() { + let input = "namespace stellar { struct Foo { int x; }; };"; + let mut parser = Parser::new(input).unwrap(); + let spec = parser.parse().unwrap(); + + assert_eq!(spec.namespaces.len(), 1); + assert_eq!(spec.namespaces[0].name, "stellar"); + assert_eq!(spec.namespaces[0].definitions.len(), 1); + } + + #[test] + fn test_deeply_nested_parents_assigned_during_parse() { + let input = r#" + union Outer switch (int v) { + case 0: + struct { + union switch (int w) { case 0: void; } innerField; + } outerField; + }; + "#; + let mut parser = Parser::new(input).unwrap(); + // Temporarily disable fix_parent_relationships to see raw assignments + let spec = parser.parse().unwrap(); + + // Print actual assignments for debugging + for def in &spec.definitions { + eprintln!( + "name={} nested={} parent={:?}", + def.name(), + def.is_nested(), + def.parent() + ); + } + + // Check that all parents are correctly assigned + let inner_union = spec + .definitions + .iter() + .find(|d| d.name() == "OuterOuterFieldInnerField") + .unwrap(); + assert_eq!( + inner_union.parent(), + Some("OuterOuterField"), + "inner union parent should be the inline struct" + ); + + let inline_struct = spec + .definitions + .iter() + .find(|d| d.name() == "OuterOuterField") + .unwrap(); + assert_eq!( + inline_struct.parent(), + Some("Outer"), + "inline struct parent should be the top-level union" + ); + } +} diff --git a/xdr-generator-rust/src/types.rs b/xdr-generator-rust/src/types.rs new file mode 100644 index 00000000..136703c6 --- /dev/null +++ b/xdr-generator-rust/src/types.rs @@ -0,0 +1,448 @@ +//! Type resolution, reference computation, and attribute generation. + +use crate::ast::{Definition, Size, Type, XdrSpec}; +use heck::{ToSnakeCase, ToUpperCamelCase}; +use std::collections::{HashMap, HashSet}; + +/// Configuration options for code generation. +#[derive(Debug, Clone, Default)] +pub struct Options { + /// Types that have custom Default implementations (skip derive(Default)) + pub custom_default_impl: HashSet, + /// Types that have custom FromStr/Display implementations (use SerializeDisplay) + pub custom_str_impl: HashSet, + /// Types that should NOT have Display/FromStr/schemars generated (have special handling elsewhere) + pub no_display_fromstr: HashSet, +} + +/// Type information collected from the XDR spec. +pub struct TypeInfo { + /// Map from type name to its definition + pub definitions: HashMap, + /// Map from type name to the types it references in its fields + pub type_field_types: HashMap>, + /// Map from const name to its value + pub const_values: HashMap, +} + +impl TypeInfo { + /// Build type information from an XDR spec. + pub fn build(spec: &XdrSpec) -> Self { + let mut definitions = HashMap::new(); + let mut type_field_types: HashMap> = HashMap::new(); + let mut const_values = HashMap::new(); + + // Collect all definitions + for def in spec.all_definitions() { + let name = rust_type_name(def.name()); + definitions.insert(name.clone(), def.clone()); + + // Collect field types for cyclic detection + let field_types = collect_field_types(def); + type_field_types.insert(name, field_types); + + // Collect const values + if let Definition::Const(c) = def { + const_values.insert(c.name.clone(), c.value); + } + } + + Self { + definitions, + type_field_types, + const_values, + } + } + + /// Resolve a size to a string, using const values for named sizes. + pub fn size_to_literal(&self, size: &Size) -> String { + match size { + Size::Literal(n) => n.to_string(), + Size::Named(name) => { + // Look up the const value and return the literal + if let Some(&value) = self.const_values.get(name) { + value.to_string() + } else { + // Fallback to the name if not found (shouldn't happen) + rust_type_name(name) + } + } + } + } + + /// Check if `type_with_fields` has a cyclic reference to `target_type`. + pub fn is_cyclic(&self, type_with_fields: &str, target_type: &str) -> bool { + self.is_cyclic_inner(type_with_fields, target_type, &mut HashSet::new()) + } + + fn is_cyclic_inner( + &self, + type_with_fields: &str, + target_type: &str, + seen: &mut HashSet, + ) -> bool { + if seen.contains(type_with_fields) { + return false; + } + seen.insert(type_with_fields.to_string()); + + if let Some(field_types) = self.type_field_types.get(type_with_fields) { + for ft in field_types { + if ft == target_type { + return true; + } + if self.is_cyclic_inner(ft, target_type, seen) { + return true; + } + } + } + false + } +} + +// ============================================================================= +// Public API functions - main exports used by generator +// ============================================================================= + +/// Convert an XDR name to a Rust type name (UpperCamelCase). +pub fn rust_type_name(name: &str) -> String { + let name = escape_name(name); + name.to_upper_camel_case() +} + +/// Convert an XDR name to a Rust field name (snake_case). +pub fn rust_field_name(name: &str) -> String { + let snake = name.to_snake_case(); + // Apply escape AFTER snake_case since to_snake_case strips trailing underscores + escape_field_name(&snake) +} + +/// Get the Rust type reference for an XDR type. +/// Wraps in Box for cyclic :simple and :optional types (matching Ruby behavior). +/// Does NOT wrap arrays/var_arrays even if cyclic. +pub fn rust_type_ref(type_: &Type, parent_type: Option<&str>, type_info: &TypeInfo) -> String { + let base = base_rust_type_ref(type_, Some(type_info)); + + // Check for cyclic reference (only for simple and optional types) + let is_cyclic = if let Some(parent) = parent_type { + if let Some(base_name) = base_type_name(type_) { + type_info.is_cyclic(&base_name, parent) + } else { + false + } + } else { + false + }; + + if !is_cyclic { + return base; + } + + // Cyclic types: wrap in Box. Arrays/VarArrays are excluded from boxing. + match type_ { + Type::Optional(inner) => { + let inner_ref = base_rust_type_ref(inner, Some(type_info)); + format!("Option>") + } + Type::Array { .. } | Type::VarArray { .. } => base, + _ => format!("Box<{base}>"), + } +} + +/// Get the base Rust type reference (without Box/Option wrapping). +/// When `type_info` is provided, const-named sizes are resolved to literal values. +/// When `None`, const-named sizes are kept as type names. +pub fn base_rust_type_ref(type_: &Type, type_info: Option<&TypeInfo>) -> String { + let resolve_size = |size: &Size| -> String { + match type_info { + Some(ti) => ti.size_to_literal(size), + None => size_to_rust(size), + } + }; + match type_ { + Type::Int => "i32".to_string(), + Type::UnsignedInt => "u32".to_string(), + Type::Hyper => "i64".to_string(), + Type::UnsignedHyper => "u64".to_string(), + Type::Float => "f32".to_string(), + Type::Double => "f64".to_string(), + Type::Bool => "bool".to_string(), + Type::OpaqueFixed(size) => format!("[u8; {}]", resolve_size(size)), + Type::OpaqueVar(max) => match max { + Some(size) => format!("BytesM::<{}>", resolve_size(size)), + None => "BytesM".to_string(), + }, + Type::String(max) => match max { + Some(size) => format!("StringM::<{}>", resolve_size(size)), + None => "StringM".to_string(), + }, + Type::Ident(name) => rust_type_name(name), + Type::Optional(inner) => format!("Option<{}>", base_rust_type_ref(inner, type_info)), + Type::Array { element_type, size } => { + format!( + "[{}; {}]", + base_rust_type_ref(element_type, type_info), + resolve_size(size) + ) + } + Type::VarArray { + element_type, + max_size, + } => { + let elem = base_rust_type_ref(element_type, type_info); + match max_size { + Some(size) => format!("VecM<{elem}, {}>", resolve_size(size)), + None => format!("VecM<{elem}>"), + } + } + } +} + +/// Get the type to use in a read_xdr call (handles turbofish syntax). +/// +/// Pattern-matches on the `Type` AST directly rather than parsing the string +/// output of `rust_type_ref`, so that turbofish insertion is structural. +pub fn rust_read_call_type( + type_: &Type, + parent_type: Option<&str>, + type_info: &TypeInfo, +) -> String { + // Check for cyclic reference to decide if Box wrapping is needed + let is_cyclic = if let Some(parent) = parent_type { + if let Some(base_name) = base_type_name(type_) { + type_info.is_cyclic(&base_name, parent) + } else { + false + } + } else { + false + }; + + match type_ { + // Fixed arrays: [T; N] needs wrapping as <[T; N]> + Type::OpaqueFixed(size) => { + format!("<[u8; {}]>", type_info.size_to_literal(size)) + } + Type::Array { element_type, size } => { + let elem = base_rust_type_ref(element_type, Some(type_info)); + format!("<[{elem}; {}]>", type_info.size_to_literal(size)) + } + // Optional types: Option:: or Option::> if cyclic + Type::Optional(inner) => { + let inner_ref = base_rust_type_ref(inner, Some(type_info)); + if is_cyclic { + format!("Option::>") + } else { + format!("Option::<{inner_ref}>") + } + } + // VarArray types: VecM:: or VecM:: + Type::VarArray { + element_type, + max_size, + } => { + let elem = base_rust_type_ref(element_type, Some(type_info)); + match max_size { + Some(size) => format!("VecM::<{elem}, {}>", type_info.size_to_literal(size)), + None => format!("VecM::<{elem}>"), + } + } + // Simple ident types: Box:: if cyclic, otherwise just T + _ if is_cyclic => { + let base = base_rust_type_ref(type_, Some(type_info)); + format!("Box::<{base}>") + } + // Everything else: no turbofish needed + _ => base_rust_type_ref(type_, Some(type_info)), + } +} + +/// Get the element type for a VecM/array (for conversion impls). +pub fn element_type_for_vec(type_: &Type) -> String { + match type_ { + Type::OpaqueFixed(_) | Type::OpaqueVar(_) | Type::String(_) => "u8".to_string(), + Type::Array { element_type, .. } | Type::VarArray { element_type, .. } => { + base_rust_type_ref(element_type, None) + } + Type::Ident(name) => rust_type_name(name), + _ => "u8".to_string(), + } +} + +/// Get the serde_as type for i64/u64 fields (e.g., "NumberOrString", "Option"). +/// Returns None if no serde_as is needed. +pub fn serde_as_type(type_: &Type) -> Option { + let base = get_base_numeric_type(type_); + match base.as_deref() { + Some("i64") | Some("u64") => Some(rust_type_ref_for_serde(type_, "NumberOrString")), + _ => None, + } +} + +// ============================================================================= +// Type classification utilities +// ============================================================================= + +/// Check if a type is a builtin (maps directly to a Rust primitive). +pub fn is_builtin_type(type_: &Type) -> bool { + matches!( + type_, + Type::Int + | Type::UnsignedInt + | Type::Hyper + | Type::UnsignedHyper + | Type::Float + | Type::Double + | Type::Bool + ) +} + +/// Check if a type is a fixed-length opaque array. +pub fn is_fixed_opaque(type_: &Type) -> bool { + matches!(type_, Type::OpaqueFixed(_)) +} + +/// Check if a type is a fixed-length array (including fixed opaque). +pub fn is_fixed_array(type_: &Type) -> bool { + matches!(type_, Type::OpaqueFixed(_) | Type::Array { .. }) +} + +/// Check if a type is a variable-length array. +pub fn is_var_array(type_: &Type) -> bool { + matches!( + type_, + Type::OpaqueVar(_) | Type::String(_) | Type::VarArray { .. } + ) +} + +// ============================================================================= +// Internal utilities +// ============================================================================= + +/// Collect the base type names referenced in a definition's fields. +fn collect_field_types(def: &Definition) -> Vec { + match def { + Definition::Struct(s) => s + .members + .iter() + .filter_map(|m| base_type_name(&m.type_)) + .collect(), + Definition::Union(u) => u + .arms + .iter() + .filter_map(|arm| arm.type_.as_ref().and_then(base_type_name)) + .collect(), + // Note: Typedefs are NOT included in cycle detection, matching Ruby behavior. + // Only structs and unions have their field types tracked for cycle detection. + Definition::Typedef(_) | Definition::Enum(_) | Definition::Const(_) => vec![], + } +} + +/// Get the base type name from a Type (for cyclic detection). +fn base_type_name(type_: &Type) -> Option { + match type_ { + Type::Ident(name) => Some(rust_type_name(name)), + Type::Optional(inner) => base_type_name(inner), + Type::Array { element_type, .. } => base_type_name(element_type), + Type::VarArray { element_type, .. } => base_type_name(element_type), + _ => None, + } +} + +/// Convert a Size to a Rust string representation. Named sizes become type names. +pub fn size_to_rust(size: &Size) -> String { + match size { + Size::Literal(n) => n.to_string(), + Size::Named(name) => rust_type_name(name), + } +} + +/// Escape reserved names for type names. +/// +/// - `"type"` conflicts with the Rust keyword. +/// - `"Error"` conflicts with the crate's own `Error` type (and `std::error::Error`). +/// The `S` prefix matches the Ruby xdrgen convention (`SError`), maintaining backward +/// compatibility with the existing generated API. +fn escape_name(name: &str) -> String { + match name { + "type" => "type_".to_string(), + "Error" => "SError".to_string(), + _ => name.to_string(), + } +} + +/// Escape reserved names for field names (after snake_case conversion). +fn escape_field_name(name: &str) -> String { + match name { + "type" => "type_".to_string(), + _ => name.to_string(), + } +} + +fn get_base_numeric_type(type_: &Type) -> Option { + match type_ { + Type::Hyper => Some("i64".to_string()), + Type::UnsignedHyper => Some("u64".to_string()), + Type::Optional(inner) => get_base_numeric_type(inner), + Type::Array { element_type, .. } => get_base_numeric_type(element_type), + Type::VarArray { element_type, .. } => get_base_numeric_type(element_type), + _ => None, + } +} + +fn rust_type_ref_for_serde(type_: &Type, number_wrapper: &str) -> String { + match type_ { + Type::Hyper | Type::UnsignedHyper => number_wrapper.to_string(), + Type::Optional(inner) => { + format!("Option<{}>", rust_type_ref_for_serde(inner, number_wrapper)) + } + Type::Array { element_type, size } => { + format!( + "[{}; {}]", + rust_type_ref_for_serde(element_type, number_wrapper), + size_to_rust(size) + ) + } + Type::VarArray { + element_type, + max_size, + } => { + let elem = rust_type_ref_for_serde(element_type, number_wrapper); + match max_size { + Some(size) => format!("VecM<{elem}, {}>", size_to_rust(size)), + None => format!("VecM<{elem}>"), + } + } + _ => base_rust_type_ref(type_, None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rust_type_name() { + assert_eq!(rust_type_name("public_key"), "PublicKey"); + assert_eq!( + rust_type_name("PUBLIC_KEY_TYPE_ED25519"), + "PublicKeyTypeEd25519" + ); + } + + #[test] + fn test_rust_field_name() { + assert_eq!(rust_field_name("publicKey"), "public_key"); + assert_eq!(rust_field_name("type"), "type_"); + } + + #[test] + fn test_base_rust_type_ref() { + assert_eq!(base_rust_type_ref(&Type::Int, None), "i32"); + assert_eq!(base_rust_type_ref(&Type::UnsignedHyper, None), "u64"); + assert_eq!( + base_rust_type_ref(&Type::OpaqueFixed(Size::Literal(32)), None), + "[u8; 32]" + ); + } +} diff --git a/xdr-generator-rust/templates/const.rs.jinja b/xdr-generator-rust/templates/const.rs.jinja new file mode 100644 index 00000000..8e92f1e2 --- /dev/null +++ b/xdr-generator-rust/templates/const.rs.jinja @@ -0,0 +1,5 @@ +{# XDR consts are always emitted as u64 to match the Ruby xdrgen generator, #} +{# which uses u64 unconditionally regardless of the const's declared type. #} +/// {{ c.doc_name }}{{ c.source_comment }} +pub const {{ c.name }}: u64 = {{ c.value_str }}; + diff --git a/xdr-generator-rust/templates/enum.rs.jinja b/xdr-generator-rust/templates/enum.rs.jinja new file mode 100644 index 00000000..83798d55 --- /dev/null +++ b/xdr-generator-rust/templates/enum.rs.jinja @@ -0,0 +1,117 @@ +/// {{ e.name }}{{ e.source_comment }} +// enum +{%- if e.has_default %} +#[cfg_attr(feature = "alloc", derive(Default))] +{%- endif %} +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +{%- if e.is_custom_str %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr) +)] +{%- else %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case") +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +{%- endif %} +#[repr(i32)] +pub enum {{ e.name }} { +{%- for m in e.members %} +{%- if m.is_first %} + #[cfg_attr(feature = "alloc", default)] +{%- endif %} + {{ m.name }} = {{ m.value }}, +{%- endfor %} +} + +impl {{ e.name }} { + pub const VARIANTS: [{{ e.name }}; {{ e.members.len() }}] = [ +{%- for m in e.members %} + {{ e.name }}::{{ m.name }}, +{%- endfor %} + ]; + pub const VARIANTS_STR: [&'static str; {{ e.members.len() }}] = [{% for m in e.members %}"{{ m.name }}", {% endfor %}]; + + #[must_use] + pub const fn name(&self) -> &'static str { + match self { +{%- for m in e.members %} + Self::{{ m.name }} => "{{ m.name }}", +{%- endfor %} + } + } + + #[must_use] + pub const fn variants() -> [{{ e.name }}; {{ e.members.len() }}] { + Self::VARIANTS + } +} + +impl Name for {{ e.name }} { + #[must_use] + fn name(&self) -> &'static str { + Self::name(self) + } +} + +impl Variants<{{ e.name }}> for {{ e.name }} { + fn variants() -> slice::Iter<'static, {{ e.name }}> { + Self::VARIANTS.iter() + } +} + +impl Enum for {{ e.name }} {} + +impl fmt::Display for {{ e.name }} { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl TryFrom for {{ e.name }} { + type Error = Error; + + fn try_from(i: i32) -> Result { + let e = match i { +{%- for m in e.members %} + {{ m.value }} => {{ e.name }}::{{ m.name }}, +{%- endfor %} + #[allow(unreachable_patterns)] + _ => return Err(Error::Invalid), + }; + Ok(e) + } +} + +impl From<{{ e.name }}> for i32 { + #[must_use] + fn from(e: {{ e.name }}) -> Self { + e as Self + } +} + +impl ReadXdr for {{ e.name }} { + #[cfg(feature = "std")] + fn read_xdr(r: &mut Limited) -> Result { + r.with_limited_depth(|r| { + let e = i32::read_xdr(r)?; + let v: Self = e.try_into()?; + Ok(v) + }) + } +} + +impl WriteXdr for {{ e.name }} { + #[cfg(feature = "std")] + fn write_xdr(&self, w: &mut Limited) -> Result<(), Error> { + w.with_limited_depth(|w| { + let i: i32 = (*self).into(); + i.write_xdr(w) + }) + } +} + diff --git a/xdr-generator-rust/templates/generated.rs.jinja b/xdr-generator-rust/templates/generated.rs.jinja new file mode 100644 index 00000000..d596b218 --- /dev/null +++ b/xdr-generator-rust/templates/generated.rs.jinja @@ -0,0 +1,35 @@ +// Module {# namespace omitted for backward compatibility with the Ruby generator, which renders it as an empty string #} is generated from: +{%- for (file, _) in xdr_files_sha256 %} +// {{ file }} +{%- endfor %} + +#![allow(clippy::missing_errors_doc, clippy::unreadable_literal)] + +/// `XDR_FILES_SHA256` is a list of pairs of source files and their SHA256 hashes. +pub const XDR_FILES_SHA256: [(&str, &str); {{ xdr_files_sha256.len() }}] = [ +{%- for (file, hash) in xdr_files_sha256 %} + ( + "{{ file }}", + "{{ hash }}", + ), +{%- endfor %} +]; + +{{ header }} +{%- for def in definitions %} +{%- match def %} +{%- when DefinitionOutput::Struct(s) %} +{% include "struct.rs.jinja" %} +{%- when DefinitionOutput::Enum(e) %} +{% include "enum.rs.jinja" %} +{%- when DefinitionOutput::Union(u) %} +{% include "union.rs.jinja" %} +{%- when DefinitionOutput::TypedefAlias(t) %} +{% include "typedef_alias.rs.jinja" %} +{%- when DefinitionOutput::TypedefNewtype(t) %} +{% include "typedef_newtype.rs.jinja" %} +{%- when DefinitionOutput::Const(c) %} +{% include "const.rs.jinja" %} +{%- endmatch %} +{%- endfor %} +{% include "type_enum.rs.jinja" %} diff --git a/xdr-generator-rust/templates/struct.rs.jinja b/xdr-generator-rust/templates/struct.rs.jinja new file mode 100644 index 00000000..e4a7ad5a --- /dev/null +++ b/xdr-generator-rust/templates/struct.rs.jinja @@ -0,0 +1,88 @@ +/// {{ s.name }}{{ s.source_comment }} +{%- if s.has_default %} +#[cfg_attr(feature = "alloc", derive(Default))] +{%- endif %} +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_eval::cfg_eval] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +{%- if s.is_custom_str %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde_with::SerializeDisplay) +)] +{%- else %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case") +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +{%- endif %} +pub struct {{ s.name }} { +{%- for m in s.members %} +{%- if let Some(serde_as) = m.serde_as_type %} + #[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_as(as = "{{ serde_as }}") + )] +{%- endif %} + pub {{ m.name }}: {{ m.type_ref }}, +{%- endfor %} +} + +impl ReadXdr for {{ s.name }} { + #[cfg(feature = "std")] + fn read_xdr(r: &mut Limited) -> Result { + r.with_limited_depth(|r| { + Ok(Self { +{%- for m in s.members %} + {{ m.name }}: {{ m.read_call }}::read_xdr(r)?, +{%- endfor %} + }) + }) + } +} + +impl WriteXdr for {{ s.name }} { + #[cfg(feature = "std")] + fn write_xdr(&self, w: &mut Limited) -> Result<(), Error> { + w.with_limited_depth(|w| { +{%- for m in s.members %} + self.{{ m.name }}.write_xdr(w)?; +{%- endfor %} + Ok(()) + }) + } +} +{%- if s.is_custom_str %} +#[cfg(all(feature = "serde", feature = "alloc"))] +impl<'de> serde::Deserialize<'de> for {{ s.name }} { + fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de> { + use serde::Deserialize; + #[derive(Deserialize)] + struct {{ s.name }} { +{%- for m in s.members %} + {{ m.name }}: {{ m.type_ref }}, +{%- endfor %} + } + #[derive(Deserialize)] + #[serde(untagged)] + enum {{ s.name }}OrString<'a> { + Str(&'a str), + String(String), + {{ s.name }}({{ s.name }}), + } + match {{ s.name }}OrString::deserialize(deserializer)? { + {{ s.name }}OrString::Str(s) => s.parse().map_err(serde::de::Error::custom), + {{ s.name }}OrString::String(s) => s.parse().map_err(serde::de::Error::custom), + {{ s.name }}OrString::{{ s.name }}({{ s.name }} { + {{ s.member_names }}, + }) => Ok(self::{{ s.name }} { + {{ s.member_names }}, + }), + } + } +} +{%- endif %} + diff --git a/xdr-generator-rust/templates/type_enum.rs.jinja b/xdr-generator-rust/templates/type_enum.rs.jinja new file mode 100644 index 00000000..b1621f5e --- /dev/null +++ b/xdr-generator-rust/templates/type_enum.rs.jinja @@ -0,0 +1,305 @@ +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case") +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum TypeVariant { +{%- for t in type_variant_enum.types %} + {{ t }}, +{%- endfor %} +} + +impl TypeVariant { + pub const VARIANTS: [TypeVariant; {{ type_variant_enum.types.len() }}] = [ {%- for t in type_variant_enum.types %}TypeVariant::{{ t }}, +{%- endfor %} ]; + pub const VARIANTS_STR: [&'static str; {{ type_variant_enum.types.len() }}] = [ {%- for t in type_variant_enum.types %}"{{ t }}", +{%- endfor %} ]; + + #[must_use] + #[allow(clippy::too_many_lines)] + pub const fn name(&self) -> &'static str { + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }} => "{{ t }}", +{%- endfor %} + } + } + + #[must_use] + #[allow(clippy::too_many_lines)] + pub const fn variants() -> [TypeVariant; {{ type_variant_enum.types.len() }}] { + Self::VARIANTS + } + + #[cfg(feature = "schemars")] + #[must_use] + #[allow(clippy::too_many_lines)] + pub fn json_schema(&self, gen: schemars::gen::SchemaGenerator) -> schemars::schema::RootSchema { + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }} => gen.into_root_schema_for::<{{ t }}>(), +{%- endfor %} + } + } +} + +impl Name for TypeVariant { + #[must_use] + fn name(&self) -> &'static str { + Self::name(self) + } +} + +impl Variants for TypeVariant { + fn variants() -> slice::Iter<'static, TypeVariant> { + Self::VARIANTS.iter() + } +} + +impl core::str::FromStr for TypeVariant { + type Err = Error; + #[allow(clippy::too_many_lines)] + fn from_str(s: &str) -> Result { + match s { +{%- for t in type_variant_enum.types %} + "{{ t }}" => Ok(Self::{{ t }}), +{%- endfor %} + _ => Err(Error::Invalid), + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case"), + serde(untagged), +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum Type { +{%- for t in type_variant_enum.types %} + {{ t }}(Box<{{ t }}>), +{%- endfor %} +} + +impl Type { + pub const VARIANTS: [TypeVariant; {{ type_variant_enum.types.len() }}] = [ {%- for t in type_variant_enum.types %}TypeVariant::{{ t }}, +{%- endfor %} ]; + pub const VARIANTS_STR: [&'static str; {{ type_variant_enum.types.len() }}] = [ {%- for t in type_variant_enum.types %}"{{ t }}", +{%- endfor %} ]; + + #[cfg(feature = "std")] + #[allow(clippy::too_many_lines)] + pub fn read_xdr(v: TypeVariant, r: &mut Limited) -> Result { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => r.with_limited_depth(|r| Ok(Self::{{ t }}(Box::new({{ t }}::read_xdr(r)?)))), +{%- endfor %} + } + } + + #[cfg(feature = "base64")] + pub fn read_xdr_base64(v: TypeVariant, r: &mut Limited) -> Result { + let mut dec = Limited::new( + base64::read::DecoderReader::new( + SkipWhitespace::new(&mut r.inner), + &base64::engine::general_purpose::STANDARD, + ), + r.limits.clone(), + ); + let t = Self::read_xdr(v, &mut dec)?; + Ok(t) + } + + #[cfg(feature = "std")] + pub fn read_xdr_to_end(v: TypeVariant, r: &mut Limited) -> Result { + let s = Self::read_xdr(v, r)?; + // Check that any further reads, such as this read of one byte, read no + // data, indicating EOF. If a byte is read the data is invalid. + if r.read(&mut [0u8; 1])? == 0 { + Ok(s) + } else { + Err(Error::Invalid) + } + } + + #[cfg(feature = "base64")] + pub fn read_xdr_base64_to_end(v: TypeVariant, r: &mut Limited) -> Result { + let mut dec = Limited::new( + base64::read::DecoderReader::new( + SkipWhitespace::new(&mut r.inner), + &base64::engine::general_purpose::STANDARD, + ), + r.limits.clone(), + ); + let t = Self::read_xdr_to_end(v, &mut dec)?; + Ok(t) + } + + #[cfg(feature = "std")] + #[allow(clippy::too_many_lines)] + pub fn read_xdr_iter(v: TypeVariant, r: &mut Limited) -> Box> + '_> { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Box::new(ReadXdrIter::<_, {{ t }}>::new(&mut r.inner, r.limits.clone()).map(|r| r.map(|t| Self::{{ t }}(Box::new(t))))), +{%- endfor %} + } + } + + #[cfg(feature = "std")] + #[allow(clippy::too_many_lines)] + pub fn read_xdr_framed_iter(v: TypeVariant, r: &mut Limited) -> Box> + '_> { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Box::new(ReadXdrIter::<_, Frame<{{ t }}>>::new(&mut r.inner, r.limits.clone()).map(|r| r.map(|t| Self::{{ t }}(Box::new(t.0))))), +{%- endfor %} + } + } + + #[cfg(feature = "base64")] + #[allow(clippy::too_many_lines)] + pub fn read_xdr_base64_iter(v: TypeVariant, r: &mut Limited) -> Box> + '_> { + let dec = base64::read::DecoderReader::new( + SkipWhitespace::new(&mut r.inner), + &base64::engine::general_purpose::STANDARD, + ); + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Box::new(ReadXdrIter::<_, {{ t }}>::new(dec, r.limits.clone()).map(|r| r.map(|t| Self::{{ t }}(Box::new(t))))), +{%- endfor %} + } + } + + #[cfg(feature = "std")] + pub fn from_xdr>(v: TypeVariant, bytes: B, limits: Limits) -> Result { + let mut cursor = Limited::new(Cursor::new(bytes.as_ref()), limits); + let t = Self::read_xdr_to_end(v, &mut cursor)?; + Ok(t) + } + + #[cfg(feature = "base64")] + pub fn from_xdr_base64(v: TypeVariant, b64: impl AsRef<[u8]>, limits: Limits) -> Result { + let mut dec = Limited::new( + base64::read::DecoderReader::new( + SkipWhitespace::new(Cursor::new(b64)), + &base64::engine::general_purpose::STANDARD, + ), + limits, + ); + let t = Self::read_xdr_to_end(v, &mut dec)?; + Ok(t) + } + + #[cfg(all(feature = "std", feature = "serde_json"))] + #[deprecated(note = "use from_json")] + pub fn read_json(v: TypeVariant, r: impl Read) -> Result { + Self::from_json(v, r) + } + + #[cfg(all(feature = "std", feature = "serde_json"))] + #[allow(clippy::too_many_lines)] + pub fn from_json(v: TypeVariant, r: impl Read) -> Result { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Ok(Self::{{ t }}(Box::new(serde_json::from_reader(r)?))), +{%- endfor %} + } + } + + #[cfg(all(feature = "std", feature = "serde_json"))] + #[allow(clippy::too_many_lines)] + pub fn deserialize_json<'r, R: serde_json::de::Read<'r>>(v: TypeVariant, r: &mut serde_json::de::Deserializer) -> Result { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Ok(Self::{{ t }}(Box::new(serde::de::Deserialize::deserialize(r)?))), +{%- endfor %} + } + } + + #[cfg(feature = "arbitrary")] + #[allow(clippy::too_many_lines)] + pub fn arbitrary(v: TypeVariant, u: &mut arbitrary::Unstructured<'_>) -> Result { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Ok(Self::{{ t }}(Box::new({{ t }}::arbitrary(u)?))), +{%- endfor %} + } + } + + #[cfg(feature = "alloc")] + #[must_use] + #[allow(clippy::too_many_lines)] + pub fn default(v: TypeVariant) -> Self { + match v { +{%- for t in type_variant_enum.types %} + TypeVariant::{{ t }} => Self::{{ t }}(Box::default()), +{%- endfor %} + } + } + + #[cfg(feature = "alloc")] + #[must_use] + #[allow(clippy::too_many_lines)] + pub fn value(&self) -> &dyn core::any::Any { + #[allow(clippy::match_same_arms)] + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }}(ref v) => v.as_ref(), +{%- endfor %} + } + } + + #[must_use] + #[allow(clippy::too_many_lines)] + pub const fn name(&self) -> &'static str { + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }}(_) => "{{ t }}", +{%- endfor %} + } + } + + #[must_use] + #[allow(clippy::too_many_lines)] + pub const fn variants() -> [TypeVariant; {{ type_variant_enum.types.len() }}] { + Self::VARIANTS + } + + #[must_use] + #[allow(clippy::too_many_lines)] + pub const fn variant(&self) -> TypeVariant { + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }}(_) => TypeVariant::{{ t }}, +{%- endfor %} + } + } +} + +impl Name for Type { + #[must_use] + fn name(&self) -> &'static str { + Self::name(self) + } +} + +impl Variants for Type { + fn variants() -> slice::Iter<'static, TypeVariant> { + Self::VARIANTS.iter() + } +} + +impl WriteXdr for Type { + #[cfg(feature = "std")] + #[allow(clippy::too_many_lines)] + fn write_xdr(&self, w: &mut Limited) -> Result<(), Error> { + match self { +{%- for t in type_variant_enum.types %} + Self::{{ t }}(v) => v.write_xdr(w), +{%- endfor %} + } + } +} diff --git a/xdr-generator-rust/templates/typedef_alias.rs.jinja b/xdr-generator-rust/templates/typedef_alias.rs.jinja new file mode 100644 index 00000000..80621336 --- /dev/null +++ b/xdr-generator-rust/templates/typedef_alias.rs.jinja @@ -0,0 +1,3 @@ +/// {{ t.name }}{{ t.source_comment }} +pub type {{ t.name }} = {{ t.type_ref }}; + diff --git a/xdr-generator-rust/templates/typedef_newtype.rs.jinja b/xdr-generator-rust/templates/typedef_newtype.rs.jinja new file mode 100644 index 00000000..8eaca6c7 --- /dev/null +++ b/xdr-generator-rust/templates/typedef_newtype.rs.jinja @@ -0,0 +1,236 @@ +/// {{ t.name }}{{ t.source_comment }} +#[cfg_eval::cfg_eval] +{%- if t.has_default && !t.is_var_array %} +#[cfg_attr(feature = "alloc", derive(Default))] +{%- endif %} +{%- if t.has_default && t.is_var_array %} +#[derive(Default, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +{%- else %} +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +{%- endif %} +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +{%- if t.is_custom_str || t.custom_display_fromstr %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr) +)] +{%- else %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case") +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +{%- endif %} +{%- if !t.is_fixed_opaque %} +#[derive(Debug)] +{%- endif %} +{%- if let Some(serde_as) = t.serde_as_type %} +pub struct {{ t.name }}( + #[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_as(as = "{{ serde_as }}") + )] + pub {{ t.type_ref }}, +); +{%- else %} +pub struct {{ t.name }}(pub {{ t.type_ref }}); +{%- endif %} + +{% if t.custom_debug -%} +impl core::fmt::Debug for {{ t.name }} { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let v = &self.0; + write!(f, "{{ t.name }}(")?; + for b in v { + write!(f, "{b:02x}")?; + } + write!(f, ")")?; + Ok(()) + } +} +{%- endif %} +{%- if t.custom_display_fromstr %} +impl core::fmt::Display for {{ t.name }} { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let v = &self.0; + for b in v { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + +#[cfg(feature = "alloc")] +impl core::str::FromStr for {{ t.name }} { + type Err = Error; + fn from_str(s: &str) -> core::result::Result { + hex::decode(s).map_err(|_| Error::InvalidHex)?.try_into() + } +} +{%- endif %} +{%- if t.custom_schemars %} +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for {{ t.name }} { + fn schema_name() -> String { + "{{ t.name }}".to_string() + } + + fn is_referenceable() -> bool { + false + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = String::json_schema(gen); + if let schemars::schema::Schema::Object(mut schema) = schema { + schema.extensions.insert( + "contentEncoding".to_owned(), + serde_json::Value::String("hex".to_string()), + ); + schema.extensions.insert( + "contentMediaType".to_owned(), + serde_json::Value::String("application/binary".to_string()), + ); + let string = *schema.string.unwrap_or_default().clone(); + schema.string = Some(Box::new(schemars::schema::StringValidation { + max_length: {{ t.size.as_ref().unwrap() }}_u32.checked_mul(2).map(Some).unwrap_or_default(), + min_length: {{ t.size.as_ref().unwrap() }}_u32.checked_mul(2).map(Some).unwrap_or_default(), + ..string + })); + schema.into() + } else { + schema + } + } +} +{%- endif %} +impl From<{{ t.name }}> for {{ t.type_ref }} { + #[must_use] + fn from(x: {{ t.name }}) -> Self { + x.0 + } +} + +impl From<{{ t.type_ref }}> for {{ t.name }} { + #[must_use] + fn from(x: {{ t.type_ref }}) -> Self { + {{ t.name }}(x) + } +} + +impl AsRef<{{ t.type_ref }}> for {{ t.name }} { + #[must_use] + fn as_ref(&self) -> &{{ t.type_ref }} { + &self.0 + } +} + +impl ReadXdr for {{ t.name }} { + #[cfg(feature = "std")] + fn read_xdr(r: &mut Limited) -> Result { + r.with_limited_depth(|r| { + let i = {{ t.read_call }}::read_xdr(r)?; + let v = {{ t.name }}(i); + Ok(v) + }) + } +} + +impl WriteXdr for {{ t.name }} { + #[cfg(feature = "std")] + fn write_xdr(&self, w: &mut Limited) -> Result<(), Error> { + w.with_limited_depth(|w| { self.0.write_xdr(w) }) + } +} +{%- if t.is_fixed_array %} + +impl {{ t.name }} { + #[must_use] + pub fn as_slice(&self) -> &[{{ t.element_type }}] { + &self.0 + } +} + +#[cfg(feature = "alloc")] +impl TryFrom> for {{ t.name }} { + type Error = Error; + fn try_from(x: Vec<{{ t.element_type }}>) -> Result { + x.as_slice().try_into() + } +} + +#[cfg(feature = "alloc")] +impl TryFrom<&Vec<{{ t.element_type }}>> for {{ t.name }} { + type Error = Error; + fn try_from(x: &Vec<{{ t.element_type }}>) -> Result { + x.as_slice().try_into() + } +} + +impl TryFrom<&[{{ t.element_type }}]> for {{ t.name }} { + type Error = Error; + fn try_from(x: &[{{ t.element_type }}]) -> Result { + Ok({{ t.name }}(x.try_into()?)) + } +} + +impl AsRef<[{{ t.element_type }}]> for {{ t.name }} { + #[must_use] + fn as_ref(&self) -> &[{{ t.element_type }}] { + &self.0 + } +} +{%- endif %} +{%- if t.is_var_array %} + +impl Deref for {{ t.name }} { + type Target = {{ t.type_ref }}; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<{{ t.name }}> for Vec<{{ t.element_type }}> { + #[must_use] + fn from(x: {{ t.name }}) -> Self { + x.0.0 + } +} + +impl TryFrom> for {{ t.name }} { + type Error = Error; + fn try_from(x: Vec<{{ t.element_type }}>) -> Result { + Ok({{ t.name }}(x.try_into()?)) + } +} + +#[cfg(feature = "alloc")] +impl TryFrom<&Vec<{{ t.element_type }}>> for {{ t.name }} { + type Error = Error; + fn try_from(x: &Vec<{{ t.element_type }}>) -> Result { + Ok({{ t.name }}(x.try_into()?)) + } +} + +impl AsRef> for {{ t.name }} { + #[must_use] + fn as_ref(&self) -> &Vec<{{ t.element_type }}> { + &self.0.0 + } +} + +impl AsRef<[{{ t.element_type }}]> for {{ t.name }} { + #[cfg(feature = "alloc")] + #[must_use] + fn as_ref(&self) -> &[{{ t.element_type }}] { + &self.0.0 + } + #[cfg(not(feature = "alloc"))] + #[must_use] + fn as_ref(&self) -> &[{{ t.element_type }}] { + self.0.0 + } +} +{%- endif %} + diff --git a/xdr-generator-rust/templates/union.rs.jinja b/xdr-generator-rust/templates/union.rs.jinja new file mode 100644 index 00000000..41626406 --- /dev/null +++ b/xdr-generator-rust/templates/union.rs.jinja @@ -0,0 +1,156 @@ +/// {{ u.name }}{{ u.source_comment }} +// union with discriminant {{ u.discriminant_type }} +#[cfg_eval::cfg_eval] +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +{%- if u.is_custom_str %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr) +)] +{%- else %} +#[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "snake_case") +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +{%- endif %} +#[allow(clippy::large_enum_variant)] +pub enum {{ u.name }} { +{%- for arm in u.arms %} +{%- if arm.is_void %} + {{ arm.case_name }}, +{%- else %} + {{ arm.case_name }}( +{%- if let Some(serde_as) = arm.serde_as_type %} + #[cfg_attr( + all(feature = "serde", feature = "alloc"), + serde_as(as = "{{ serde_as }}") + )] +{%- endif %} + {{ arm.type_ref.as_ref().unwrap() }}, + ), +{%- endif %} +{%- endfor %} +} +{%- if u.has_default %} + +#[cfg(feature = "alloc")] +impl Default for {{ u.name }} { + fn default() -> Self { +{%- if let Some(first_type) = u.first_arm_type %} + Self::{{ u.first_arm_case_name }}({{ first_type }}::default()) +{%- else %} + Self::{{ u.first_arm_case_name }} +{%- endif %} + } +} +{%- endif %} + +impl {{ u.name }} { + pub const VARIANTS: [{{ u.discriminant_type }}; {{ u.arms.len() }}] = [ +{%- for arm in u.arms %} + {{ arm.case_value }}, +{%- endfor %} + ]; + pub const VARIANTS_STR: [&'static str; {{ u.arms.len() }}] = [{% for arm in u.arms %}"{{ arm.case_name }}", {% endfor %}]; + + #[must_use] + pub const fn name(&self) -> &'static str { + match self { +{%- for arm in u.arms %} +{%- if arm.is_void %} + Self::{{ arm.case_name }} => "{{ arm.case_name }}", +{%- else %} + Self::{{ arm.case_name }}(_) => "{{ arm.case_name }}", +{%- endif %} +{%- endfor %} + } + } + + #[must_use] + pub const fn discriminant(&self) -> {{ u.discriminant_type }} { + #[allow(clippy::match_same_arms)] + match self { +{%- for arm in u.arms %} +{%- if arm.is_void %} + Self::{{ arm.case_name }} => {{ arm.case_value }}, +{%- else %} + Self::{{ arm.case_name }}(_) => {{ arm.case_value }}, +{%- endif %} +{%- endfor %} + } + } + + #[must_use] + pub const fn variants() -> [{{ u.discriminant_type }}; {{ u.arms.len() }}] { + Self::VARIANTS + } +} + +impl Name for {{ u.name }} { + #[must_use] + fn name(&self) -> &'static str { + Self::name(self) + } +} + +impl Discriminant<{{ u.discriminant_type }}> for {{ u.name }} { + #[must_use] + fn discriminant(&self) -> {{ u.discriminant_type }} { + Self::discriminant(self) + } +} + +impl Variants<{{ u.discriminant_type }}> for {{ u.name }} { + fn variants() -> slice::Iter<'static, {{ u.discriminant_type }}> { + Self::VARIANTS.iter() + } +} + +impl Union<{{ u.discriminant_type }}> for {{ u.name }} {} + +impl ReadXdr for {{ u.name }} { + #[cfg(feature = "std")] + fn read_xdr(r: &mut Limited) -> Result { + r.with_limited_depth(|r| { + let dv: {{ u.discriminant_type }} = <{{ u.discriminant_type }} as ReadXdr>::read_xdr(r)?; + #[allow(clippy::match_same_arms, clippy::match_wildcard_for_single_variants)] + let v = match dv { +{%- for arm in u.arms %} +{%- if arm.is_void %} + {{ arm.case_value }} => Self::{{ arm.case_name }}, +{%- else %} + {{ arm.case_value }} => Self::{{ arm.case_name }}({{ arm.read_call.as_ref().unwrap() }}::read_xdr(r)?), +{%- endif %} +{%- endfor %} + #[allow(unreachable_patterns)] + _ => return Err(Error::Invalid), + }; + Ok(v) + }) + } +} + +impl WriteXdr for {{ u.name }} { + #[cfg(feature = "std")] + fn write_xdr(&self, w: &mut Limited) -> Result<(), Error> { + w.with_limited_depth(|w| { + self.discriminant().write_xdr(w)?; + #[allow(clippy::match_same_arms)] + match self { +{%- for arm in u.arms %} +{%- if arm.is_void %} + Self::{{ arm.case_name }} => ().write_xdr(w)?, +{%- else %} + Self::{{ arm.case_name }}(v) => v.write_xdr(w)?, +{%- endif %} +{%- endfor %} + }; + Ok(()) + }) + } +} +