Skip to content
Open
48 changes: 31 additions & 17 deletions soroban-spec-rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod syn_ext;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does this need to be pub? It looks like it's only used internally.

pub mod r#trait;
pub mod types;

Expand All @@ -12,11 +13,11 @@ use syn::Error;

use soroban_spec::read::{from_wasm, FromWasmError};

pub use types::GenerateOptions;
use types::{
generate_enum_with_options, generate_error_enum_with_options, generate_event_with_options,
generate_struct_with_options, generate_union_with_options,
};
pub use types::{GenerateError, GenerateOptions};

// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
// streams as rustdocs, because rustdocs can contain Rust code, and that code
Expand All @@ -33,6 +34,8 @@ pub enum GenerateFromFileError {
Parse(stellar_xdr::Error),
#[error("getting contract spec: {0}")]
GetSpec(FromWasmError),
#[error("generating code: {0}")]
Generate(GenerateError),
}

pub fn generate_from_file(
Expand Down Expand Up @@ -70,11 +73,16 @@ pub fn generate_from_wasm_with_options(
}

let spec = from_wasm(wasm).map_err(GenerateFromFileError::GetSpec)?;
let code = generate_with_options(&spec, file, &sha256, opts);
let code = generate_with_options(&spec, file, &sha256, opts)
.map_err(GenerateFromFileError::Generate)?;
Ok(code)
}

pub fn generate(specs: &[ScSpecEntry], file: &str, sha256: &str) -> TokenStream {
pub fn generate(
specs: &[ScSpecEntry],
file: &str,
sha256: &str,
) -> Result<TokenStream, GenerateError> {
generate_with_options(specs, file, sha256, &GenerateOptions::default())
}

Expand All @@ -83,22 +91,22 @@ pub fn generate_with_options(
file: &str,
sha256: &str,
opts: &GenerateOptions,
) -> TokenStream {
let generated = generate_without_file_with_options(specs, opts);
quote! {
) -> Result<TokenStream, GenerateError> {
let generated = generate_without_file_with_options(specs, opts)?;
Ok(quote! {
pub const WASM: &[u8] = soroban_sdk::contractfile!(file = #file, sha256 = #sha256);
#generated
}
})
}

pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream {
pub fn generate_without_file(specs: &[ScSpecEntry]) -> Result<TokenStream, GenerateError> {
generate_without_file_with_options(specs, &GenerateOptions::default())
}

pub fn generate_without_file_with_options(
specs: &[ScSpecEntry],
opts: &GenerateOptions,
) -> TokenStream {
) -> Result<TokenStream, GenerateError> {
let mut spec_fns = Vec::new();
let mut spec_structs = Vec::new();
let mut spec_unions = Vec::new();
Expand All @@ -118,24 +126,29 @@ pub fn generate_without_file_with_options(

let trait_name = "Contract";

let trait_ = r#trait::generate_trait(trait_name, &spec_fns);
let trait_ = r#trait::generate_trait(trait_name, &spec_fns)?;
let structs = spec_structs
.iter()
.map(|s| generate_struct_with_options(s, opts));
.map(|s| generate_struct_with_options(s, opts))
.collect::<Result<Vec<_>, _>>()?;
let unions = spec_unions
.iter()
.map(|s| generate_union_with_options(s, opts));
.map(|s| generate_union_with_options(s, opts))
.collect::<Result<Vec<_>, _>>()?;
let enums = spec_enums
.iter()
.map(|s| generate_enum_with_options(s, opts));
.map(|s| generate_enum_with_options(s, opts))
.collect::<Result<Vec<_>, _>>()?;
let error_enums = spec_error_enums
.iter()
.map(|s| generate_error_enum_with_options(s, opts));
.map(|s| generate_error_enum_with_options(s, opts))
.collect::<Result<Vec<_>, _>>()?;
let events = spec_events
.iter()
.map(|s| generate_event_with_options(s, opts));
.map(|s| generate_event_with_options(s, opts))
.collect::<Result<Vec<_>, _>>()?;

quote! {
Ok(quote! {
#[soroban_sdk::contractargs(name = "Args")]
#[soroban_sdk::contractclient(name = "Client")]
#trait_
Expand All @@ -145,7 +158,7 @@ pub fn generate_without_file_with_options(
#(#enums)*
#(#error_enums)*
#(#events)*
}
})
}

/// Implemented by types that can be converted into pretty formatted Strings of
Expand Down Expand Up @@ -177,6 +190,7 @@ mod test {
fn example() {
let entries = from_wasm(EXAMPLE_WASM).unwrap();
let rust = generate(&entries, "<file>", "<sha256>")
.unwrap()
.to_formatted_string()
.unwrap();
assert_eq!(
Expand Down
35 changes: 35 additions & 0 deletions soroban-spec-rust/src/syn_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use proc_macro2::Ident;
use stellar_xdr::curr::{ScSymbol, StringM};

use crate::types::GenerateError;

pub trait IntoIdent {
fn into_ident(&self) -> Result<Ident, GenerateError>;
}

impl IntoIdent for str {
fn into_ident(&self) -> Result<Ident, GenerateError> {
syn::parse_str::<Ident>(self).map_err(|_| GenerateError::InvalidIdent(self.to_string()))
}
}

impl<const N: u32> IntoIdent for StringM<N> {
fn into_ident(&self) -> Result<Ident, GenerateError> {
let s = self
.to_utf8_string()
.map_err(|_| GenerateError::InvalidUtf8)?;
s.as_str().into_ident()
}
}

impl IntoIdent for ScSymbol {
fn into_ident(&self) -> Result<Ident, GenerateError> {
self.0.into_ident()
}
}

/// Creates a Rust identifier from a string or spec name, returning an error if
/// it contains invalid UTF-8 or is not a valid identifier.
pub fn str_to_ident(s: &(impl IntoIdent + ?Sized)) -> Result<Ident, GenerateError> {
s.into_ident()
}
44 changes: 28 additions & 16 deletions soroban-spec-rust/src/trait.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use quote::quote;
use stellar_xdr::curr as stellar_xdr;
use stellar_xdr::ScSpecFunctionV0;

use super::types::generate_type_ident;
use super::syn_ext::str_to_ident;
use super::types::{generate_type_ident, GenerateError};

// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
// streams as rustdocs, because rustdocs can contain Rust code, and that code
Expand All @@ -12,12 +13,18 @@ use super::types::generate_type_ident;

/// Constructs a token stream containing a single trait that has a function for
/// every function spec.
pub fn generate_trait(name: &str, specs: &[&ScSpecFunctionV0]) -> TokenStream {
let trait_ident = format_ident!("{}", name);
let fns: Vec<_> = specs.iter().map(|s| generate_function(*s)).collect();
quote! {
pub fn generate_trait(
name: &str,
specs: &[&ScSpecFunctionV0],
) -> Result<TokenStream, GenerateError> {
let trait_ident = str_to_ident(name)?;
let fns = specs
.iter()
.map(|s| generate_function(s))
.collect::<Result<Vec<_>, _>>()?;
Ok(quote! {
pub trait #trait_ident { #(#fns;)* }
}
})
}

/// Constructs a token stream representing a single function definition based on the provided
Expand All @@ -28,19 +35,24 @@ pub fn generate_trait(name: &str, specs: &[&ScSpecFunctionV0]) -> TokenStream {
///
/// # Returns
/// A `TokenStream` containing the generated function definition.
pub fn generate_function(s: &ScSpecFunctionV0) -> TokenStream {
let fn_ident = format_ident!("{}", s.name.to_utf8_string().unwrap());
let fn_inputs = s.inputs.iter().map(|input| {
let name = format_ident!("{}", input.name.to_utf8_string().unwrap());
let type_ident = generate_type_ident(&input.type_);
quote! { #name: #type_ident }
});
pub fn generate_function(s: &ScSpecFunctionV0) -> Result<TokenStream, GenerateError> {
let fn_ident = str_to_ident(&s.name)?;
let fn_inputs = s
.inputs
.iter()
.map(|input| {
let name = str_to_ident(&input.name)?;
let type_ident = generate_type_ident(&input.type_)?;
Ok(quote! { #name: #type_ident })
})
.collect::<Result<Vec<_>, GenerateError>>()?;
let fn_output = s
.outputs
.to_option()
.map(|t| generate_type_ident(&t))
.transpose()?
.map(|t| quote! { -> #t });
quote! {
Ok(quote! {
fn #fn_ident(env: soroban_sdk::Env, #(#fn_inputs),*) #fn_output
}
})
}
Loading
Loading