Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 363 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ schemars = { version = "0.8.16", optional = true }
ethnum = { version = "1.5.3", optional = true }
rand = { version = "0.9.0", optional = true }
sha2 = { version = "0.10.9", optional = true }
xdr-generator-rust = { version = "0.1.0", path = "xdr-generator-rust/generator", optional = true }
generator-definitions-json = { version = "0.1.0", path = "xdr-generator-rust/generator-definitions-json", optional = true }
Comment on lines +37 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish the generator crates before enabling them

When this crate is packaged for crates.io, Cargo resolves path dependencies that also specify a version from the registry rather than from the packaged subdirectory. These new optional dependencies are enabled by cli, and the existing release workflow packages the crate and then builds the packaged copy with --features cli, while the README advertises cargo install stellar-xdr --features cli; both paths will fail unless xdr-generator-rust and generator-definitions-json are published to crates.io in the right order. Please either publish/include these crates as real registry dependencies or keep the CLI generators inside the main package without registry-only path deps.

Useful? React with 👍 / 👎.


[dev-dependencies]
serde_json = "1.0.89"
Expand Down Expand Up @@ -61,7 +63,7 @@ hex = []
rand = ["dep:rand"]

# Features for the CLI.
cli = ["std", "type_enum", "base64", "serde", "serde_json", "schemars", "arbitrary", "rand", "dep:clap", "dep:thiserror", "dep:ethnum"]
cli = ["std", "type_enum", "base64", "serde", "serde_json", "schemars", "arbitrary", "rand", "dep:clap", "dep:thiserror", "dep:ethnum", "dep:xdr-generator-rust", "dep:generator-definitions-json"]

[package.metadata.docs.rs]
all-features = true
Expand Down
42 changes: 42 additions & 0 deletions src/cli/xfile/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::path::PathBuf;

use clap::Args;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("error generating JSON AST: {0}")]
Generate(Box<dyn std::error::Error>),
}

/// Emit parsed XDR definitions as a JSON AST
#[derive(Args, Debug, Clone)]
#[command()]
pub struct Cmd {
/// Input XDR files
#[arg(long, required = true)]
pub input: Vec<PathBuf>,

/// Output JSON AST path
#[arg(long)]
pub output: PathBuf,

/// Feature set for cfg resolution. Items whose cfg evaluates to true under
/// this feature set are kept; others are dropped. Accepts comma-separated
/// values or may be repeated (e.g. --feature curr,next or --feature curr
/// --feature next).
#[arg(long, value_delimiter = ',')]
pub feature: Vec<String>,
}

impl Cmd {
/// Run the CLIs xfile ast command.
///
/// ## Errors
///
/// If the input files cannot be read or parsed, or the JSON AST cannot be
/// written.
pub fn run(&self) -> Result<(), Error> {
generator_definitions_json::generate(&self.input, &self.output, &self.feature)
.map_err(Error::Generate)
}
}
53 changes: 53 additions & 0 deletions src/cli/xfile/generate_rust.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::path::PathBuf;

use clap::Args;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("error generating Rust code: {0}")]
Generate(Box<dyn std::error::Error>),
}

/// Generate Rust code from XDR .x files
#[derive(Args, Debug, Clone)]
#[command()]
pub struct Cmd {
/// Input XDR files
#[arg(long, required = true)]
pub input: Vec<PathBuf>,

/// Output module file (e.g. src/generated.rs)
#[arg(long)]
pub output: PathBuf,

/// Types with a custom Default implementation (skip derive(Default))
#[arg(long, value_delimiter = ',')]
pub custom_default: Vec<String>,

/// Types with a custom FromStr/Display implementation (use SerializeDisplay)
#[arg(long, value_delimiter = ',')]
pub custom_str: Vec<String>,

/// Types that should NOT have Display/FromStr/schemars generated
#[arg(long, value_delimiter = ',')]
pub no_display_fromstr: Vec<String>,
}

impl Cmd {
/// Run the CLIs xfile generate-rust command.
///
/// ## Errors
///
/// If the input files cannot be read or parsed, or the generated output
/// cannot be written.
pub fn run(&self) -> Result<(), Error> {
xdr_generator_rust::generate(
&self.input,
&self.output,
&self.custom_default,
&self.custom_str,
&self.no_display_fromstr,
)
.map_err(Error::Generate)
}
}
12 changes: 12 additions & 0 deletions src/cli/xfile/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod ast;
pub mod generate_rust;
pub mod preprocess;

use clap::{Args, Subcommand};
Expand All @@ -6,6 +8,10 @@ use clap::{Args, Subcommand};
pub enum Error {
#[error("{0}")]
Preprocess(#[from] preprocess::Error),
#[error("{0}")]
GenerateRust(#[from] generate_rust::Error),
#[error("{0}")]
Ast(#[from] ast::Error),
}

#[derive(Args, Debug, Clone)]
Expand All @@ -19,6 +25,10 @@ pub struct Cmd {
pub enum Sub {
/// Preprocess XDR .x files by evaluating #ifdef/#ifndef/#elif/#else/#endif directives
Preprocess(preprocess::Cmd),
/// Generate Rust code from XDR .x files
GenerateRust(generate_rust::Cmd),
/// Emit parsed XDR definitions as a JSON AST
Ast(ast::Cmd),
}

impl Cmd {
Expand All @@ -30,6 +40,8 @@ impl Cmd {
pub fn run(&self) -> Result<(), Error> {
match &self.sub {
Sub::Preprocess(c) => c.run()?,
Sub::GenerateRust(c) => c.run()?,
Sub::Ast(c) => c.run()?,
}
Ok(())
}
Expand Down
16 changes: 8 additions & 8 deletions xdr-generator-rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 73 additions & 39 deletions xdr-generator-rust/generator-definitions-json/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,21 @@ pub fn build(spec: &ast::XdrSpec, resolved_features: Vec<String>) -> IR {

let const_values = &type_info.const_values;

let definitions = spec.all_definitions()
let definitions = spec
.all_definitions()
.map(|def| convert_definition(def, &wire_sizes, const_values, &enum_member_values))
.collect();

IR {
version: 1,
files: spec.files.iter().map(|f| XdrFile {
name: f.name.clone(),
sha256: f.sha256.clone(),
}).collect(),
files: spec
.files
.iter()
.map(|f| XdrFile {
name: f.name.clone(),
sha256: f.sha256.clone(),
})
.collect(),
resolved_features,
definitions,
}
Expand All @@ -192,10 +197,14 @@ fn convert_definition(
fixed_size,
source: s.source.clone(),
file_index: s.file_index,
fields: s.members.iter().map(|m| Field {
name: m.name.clone(),
type_: convert_type(&m.type_, const_values),
}).collect(),
fields: s
.members
.iter()
.map(|m| Field {
name: m.name.clone(),
type_: convert_type(&m.type_, const_values),
})
.collect(),
}),
ast::Definition::Union(u) => Definition::Union(Union {
name: u.name.clone(),
Expand All @@ -206,38 +215,54 @@ fn convert_definition(
name: u.discriminant.name.clone(),
type_: convert_type(&u.discriminant.type_, const_values),
},
arms: u.arms.iter().map(|arm| {
let cases = arm.cases.iter().map(|c| match &c.value {
ast::UnionCaseValue::Literal(literal) => UnionCase {
value: i64::from(*literal),
name: None,
},
ast::UnionCaseValue::Ident(ident) => {
let value = enum_member_values.get(ident.as_str())
.map(|&v| i64::from(v))
.or_else(|| const_values.get(ident).copied())
.unwrap_or_else(|| panic!(
"union {}: unresolved case ident '{ident}'", u.name
));
UnionCase { value, name: Some(ident.clone()) }
arms: u
.arms
.iter()
.map(|arm| {
let cases = arm
.cases
.iter()
.map(|c| match &c.value {
ast::UnionCaseValue::Literal(literal) => UnionCase {
value: i64::from(*literal),
name: None,
},
ast::UnionCaseValue::Ident(ident) => {
let value = enum_member_values
.get(ident.as_str())
.map(|&v| i64::from(v))
.or_else(|| const_values.get(ident).copied())
.unwrap_or_else(|| {
panic!("union {}: unresolved case ident '{ident}'", u.name)
});
UnionCase {
value,
name: Some(ident.clone()),
}
}
})
.collect();
UnionArm {
cases,
name: arm.name.clone(),
type_: arm.type_.as_ref().map(|t| convert_type(t, const_values)),
}
}).collect();
UnionArm {
cases,
name: arm.name.clone(),
type_: arm.type_.as_ref().map(|t| convert_type(t, const_values)),
}
}).collect(),
})
.collect(),
}),
ast::Definition::Enum(e) => Definition::Enum(Enum {
name: e.name.clone(),
source: e.source.clone(),
file_index: e.file_index,
member_prefix: e.member_prefix.clone(),
members: e.members.iter().map(|m| EnumMember {
name: m.name.clone(),
value: i64::from(m.value),
}).collect(),
members: e
.members
.iter()
.map(|m| EnumMember {
name: m.name.clone(),
value: i64::from(m.value),
})
.collect(),
}),
ast::Definition::Typedef(t) => Definition::Typedef(Typedef {
name: t.name.clone(),
Expand Down Expand Up @@ -280,7 +305,10 @@ fn convert_type(ty: &ast::Type, const_values: &HashMap<String, i64>) -> TypeRef
element: Box::new(convert_type(element_type, const_values)),
count: resolve_size(size, const_values),
},
ast::Type::VarArray { element_type, max_size } => TypeRef::VarArray {
ast::Type::VarArray {
element_type,
max_size,
} => TypeRef::VarArray {
element: Box::new(convert_type(element_type, const_values)),
max_count: max_size.as_ref().map(|s| resolve_size(s, const_values)),
},
Expand Down Expand Up @@ -365,7 +393,12 @@ fn compute_wire_size(
}
if arm_sizes.is_empty()
|| arm_sizes.iter().any(|s| s.is_none())
|| arm_sizes.iter().filter_map(|s| *s).collect::<HashSet<_>>().len() != 1
|| arm_sizes
.iter()
.filter_map(|s| *s)
.collect::<HashSet<_>>()
.len()
!= 1
{
None
} else {
Expand Down Expand Up @@ -401,9 +434,10 @@ fn type_wire_size(
elem_sz.checked_mul(count)
}
ast::Type::Ident(name) => compute_wire_size(ti, name, cache, in_progress),
ast::Type::OpaqueVar(_) | ast::Type::String(_) | ast::Type::VarArray { .. } | ast::Type::Optional(_) => {
None
}
ast::Type::OpaqueVar(_)
| ast::Type::String(_)
| ast::Type::VarArray { .. }
| ast::Type::Optional(_) => None,
}
}

Expand Down
Loading
Loading