diff --git a/soroban-spec-rust/src/lib.rs b/soroban-spec-rust/src/lib.rs index 966d81697..d2214f811 100644 --- a/soroban-spec-rust/src/lib.rs +++ b/soroban-spec-rust/src/lib.rs @@ -1,3 +1,4 @@ +pub mod syn_ext; pub mod r#trait; pub mod types; @@ -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 @@ -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( @@ -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 { generate_with_options(specs, file, sha256, &GenerateOptions::default()) } @@ -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 { + 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 { generate_without_file_with_options(specs, &GenerateOptions::default()) } pub fn generate_without_file_with_options( specs: &[ScSpecEntry], opts: &GenerateOptions, -) -> TokenStream { +) -> Result { let mut spec_fns = Vec::new(); let mut spec_structs = Vec::new(); let mut spec_unions = Vec::new(); @@ -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::, _>>()?; let unions = spec_unions .iter() - .map(|s| generate_union_with_options(s, opts)); + .map(|s| generate_union_with_options(s, opts)) + .collect::, _>>()?; let enums = spec_enums .iter() - .map(|s| generate_enum_with_options(s, opts)); + .map(|s| generate_enum_with_options(s, opts)) + .collect::, _>>()?; 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::, _>>()?; let events = spec_events .iter() - .map(|s| generate_event_with_options(s, opts)); + .map(|s| generate_event_with_options(s, opts)) + .collect::, _>>()?; - quote! { + Ok(quote! { #[soroban_sdk::contractargs(name = "Args")] #[soroban_sdk::contractclient(name = "Client")] #trait_ @@ -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 @@ -177,6 +190,7 @@ mod test { fn example() { let entries = from_wasm(EXAMPLE_WASM).unwrap(); let rust = generate(&entries, "", "") + .unwrap() .to_formatted_string() .unwrap(); assert_eq!( diff --git a/soroban-spec-rust/src/syn_ext.rs b/soroban-spec-rust/src/syn_ext.rs new file mode 100644 index 000000000..da077a128 --- /dev/null +++ b/soroban-spec-rust/src/syn_ext.rs @@ -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; +} + +impl IntoIdent for str { + fn into_ident(&self) -> Result { + syn::parse_str::(self).map_err(|_| GenerateError::InvalidIdent(self.to_string())) + } +} + +impl IntoIdent for StringM { + fn into_ident(&self) -> Result { + let s = self + .to_utf8_string() + .map_err(|_| GenerateError::InvalidUtf8)?; + s.as_str().into_ident() + } +} + +impl IntoIdent for ScSymbol { + fn into_ident(&self) -> Result { + 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 { + s.into_ident() +} diff --git a/soroban-spec-rust/src/trait.rs b/soroban-spec-rust/src/trait.rs index dad73e25a..424a1ad6f 100644 --- a/soroban-spec-rust/src/trait.rs +++ b/soroban-spec-rust/src/trait.rs @@ -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 @@ -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 { + let trait_ident = str_to_ident(name)?; + let fns = specs + .iter() + .map(|s| generate_function(s)) + .collect::, _>>()?; + Ok(quote! { pub trait #trait_ident { #(#fns;)* } - } + }) } /// Constructs a token stream representing a single function definition based on the provided @@ -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 { + 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::, 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 - } + }) } diff --git a/soroban-spec-rust/src/types.rs b/soroban-spec-rust/src/types.rs index 922de7933..d88daad6e 100644 --- a/soroban-spec-rust/src/types.rs +++ b/soroban-spec-rust/src/types.rs @@ -1,20 +1,25 @@ use proc_macro2::{Literal, TokenStream}; -use quote::{format_ident, quote}; +use quote::quote; use stellar_xdr::curr as stellar_xdr; use stellar_xdr::{ ScSpecEventParamLocationV0, ScSpecEventV0, ScSpecTypeDef, ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0, ScSpecUdtStructV0, ScSpecUdtUnionV0, }; +use crate::syn_ext::str_to_ident; + // 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 // will be executed. Generated code may be generated from untrusted Wasm // containing untrusted spec docs. -// TODO: Replace the unwrap()s in this code with returning Result. -// TODO: Create Idents in a way that we can get a Result back and return it too -// because at the moment the format_ident! calls can panic if the inputs do not -// result in a valid ident. +#[derive(thiserror::Error, Debug)] +pub enum GenerateError { + #[error("invalid UTF-8 in contract spec string")] + InvalidUtf8, + #[error("invalid Rust identifier: {0:?}")] + InvalidIdent(String), +} /// Options for controlling code generation behavior. #[derive(Default)] @@ -27,7 +32,7 @@ pub struct GenerateOptions { /// Constructs a token stream containing a single struct that mirrors the struct /// spec. -pub fn generate_struct(spec: &ScSpecUdtStructV0) -> TokenStream { +pub fn generate_struct(spec: &ScSpecUdtStructV0) -> Result { generate_struct_with_options(spec, &GenerateOptions::default()) } @@ -36,118 +41,150 @@ pub fn generate_struct(spec: &ScSpecUdtStructV0) -> TokenStream { pub fn generate_struct_with_options( spec: &ScSpecUdtStructV0, opts: &GenerateOptions, -) -> TokenStream { - let ident = format_ident!("{}", spec.name.to_utf8_string().unwrap()); +) -> Result { + let ident = str_to_ident(&spec.name)?; if spec.lib.len() > 0 { - let lib_ident = format_ident!("{}", spec.lib.to_utf8_string().unwrap()); - quote! { + let lib_ident = str_to_ident(&spec.lib)?; + Ok(quote! { type #ident = ::#lib_ident::#ident; - } + }) } else if spec .fields .iter() - .all(|f| f.name.to_utf8_string().unwrap().parse::().is_ok()) + .map(|f| { + f.name + .to_utf8_string() + .map_err(|_| GenerateError::InvalidUtf8) + .map(|n| n.parse::().is_ok()) + }) + .collect::, _>>()? + .iter() + .all(|is_num| *is_num) { - let fields = spec.fields.iter().map(|f| { - let f_type = generate_type_ident(&f.type_); - quote! { pub #f_type } - }); + let fields = spec + .fields + .iter() + .map(|f| { + let f_type = generate_type_ident(&f.type_)?; + Ok(quote! { pub #f_type }) + }) + .collect::, GenerateError>>()?; let contracttype_attr = contracttype_attr(opts.export); - quote! { + Ok(quote! { #contracttype_attr #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct #ident ( #(#fields),* ); - } + }) } else { - let fields = spec.fields.iter().map(|f| { - let f_ident = format_ident!("{}", f.name.to_utf8_string().unwrap()); - let f_type = generate_type_ident(&f.type_); - quote! { pub #f_ident: #f_type } - }); + let fields = spec + .fields + .iter() + .map(|f| { + let f_ident = str_to_ident(&f.name)?; + let f_type = generate_type_ident(&f.type_)?; + Ok(quote! { pub #f_ident: #f_type }) + }) + .collect::, GenerateError>>()?; let contracttype_attr = contracttype_attr(opts.export); - quote! { + Ok(quote! { #contracttype_attr #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct #ident { #(#fields,)* } - } + }) } } /// Constructs a token stream containing a single enum that mirrors the union /// spec. -pub fn generate_union(spec: &ScSpecUdtUnionV0) -> TokenStream { +pub fn generate_union(spec: &ScSpecUdtUnionV0) -> Result { generate_union_with_options(spec, &GenerateOptions::default()) } /// Constructs a token stream containing a single enum that mirrors the union /// spec, with configurable options. -pub fn generate_union_with_options(spec: &ScSpecUdtUnionV0, opts: &GenerateOptions) -> TokenStream { - let ident = format_ident!("{}", spec.name.to_utf8_string().unwrap()); +pub fn generate_union_with_options( + spec: &ScSpecUdtUnionV0, + opts: &GenerateOptions, +) -> Result { + let ident = str_to_ident(&spec.name)?; if spec.lib.len() > 0 { - let lib_ident = format_ident!("{}", spec.lib.to_utf8_string_lossy()); - quote! { + let lib_ident = str_to_ident(&spec.lib)?; + Ok(quote! { pub type #ident = ::#lib_ident::#ident; - } + }) } else { - let variants = spec.cases.iter().map(|c| { - let name = match c { - stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(v) => v.name.clone(), - stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => t.name.clone(), - }; - let v_ident = format_ident!("{}", name.to_utf8_string_lossy()); - match c { - stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(_) => { - quote! { #v_ident } - } - stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => { - let v_type = t.type_.iter().map(generate_type_ident); - quote! { #v_ident ( #(#v_type),* ) } + let variants = spec + .cases + .iter() + .map(|c| { + let name = match c { + stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(v) => &v.name, + stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => &t.name, + }; + let v_ident = str_to_ident(name)?; + match c { + stellar_xdr::ScSpecUdtUnionCaseV0::VoidV0(_) => Ok(quote! { #v_ident }), + stellar_xdr::ScSpecUdtUnionCaseV0::TupleV0(t) => { + let v_type = t + .type_ + .iter() + .map(generate_type_ident) + .collect::, _>>()?; + Ok(quote! { #v_ident ( #(#v_type),* ) }) + } } - } - }); + }) + .collect::, GenerateError>>()?; let contracttype_attr = contracttype_attr(opts.export); - quote! { + Ok(quote! { #contracttype_attr #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum #ident { #(#variants,)* } - } + }) } } /// Constructs a token stream containing a single enum that mirrors the enum /// spec. -pub fn generate_enum(spec: &ScSpecUdtEnumV0) -> TokenStream { +pub fn generate_enum(spec: &ScSpecUdtEnumV0) -> Result { generate_enum_with_options(spec, &GenerateOptions::default()) } /// Constructs a token stream containing a single enum that mirrors the enum /// spec, with configurable options. -pub fn generate_enum_with_options(spec: &ScSpecUdtEnumV0, opts: &GenerateOptions) -> TokenStream { - let ident = format_ident!("{}", spec.name.to_utf8_string().unwrap()); +pub fn generate_enum_with_options( + spec: &ScSpecUdtEnumV0, + opts: &GenerateOptions, +) -> Result { + let ident = str_to_ident(&spec.name)?; if spec.lib.len() > 0 { - let lib_ident = format_ident!("{}", spec.lib.to_utf8_string_lossy()); - quote! { + let lib_ident = str_to_ident(&spec.lib)?; + Ok(quote! { pub type #ident = ::#lib_ident::#ident; - } + }) } else { - let variants = spec.cases.iter().map(|c| { - let v_ident = format_ident!("{}", c.name.to_utf8_string().unwrap()); - let v_value = Literal::u32_unsuffixed(c.value); - quote! { #v_ident = #v_value } - }); + let variants = spec + .cases + .iter() + .map(|c| { + let v_ident = str_to_ident(&c.name)?; + let v_value = Literal::u32_unsuffixed(c.value); + Ok(quote! { #v_ident = #v_value }) + }) + .collect::, GenerateError>>()?; let contracttype_attr = contracttype_attr(opts.export); - quote! { + Ok(quote! { #contracttype_attr #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum #ident { #(#variants,)* } - } + }) } } /// Constructs a token stream containing a single enum that mirrors the enum /// spec, that is intended for use with errors. -pub fn generate_error_enum(spec: &ScSpecUdtErrorEnumV0) -> TokenStream { +pub fn generate_error_enum(spec: &ScSpecUdtErrorEnumV0) -> Result { generate_error_enum_with_options(spec, &GenerateOptions::default()) } @@ -156,73 +193,84 @@ pub fn generate_error_enum(spec: &ScSpecUdtErrorEnumV0) -> TokenStream { pub fn generate_error_enum_with_options( spec: &ScSpecUdtErrorEnumV0, opts: &GenerateOptions, -) -> TokenStream { - let ident = format_ident!("{}", spec.name.to_utf8_string().unwrap()); +) -> Result { + let ident = str_to_ident(&spec.name)?; if spec.lib.len() > 0 { - let lib_ident = format_ident!("{}", spec.lib.to_utf8_string_lossy()); - quote! { + let lib_ident = str_to_ident(&spec.lib)?; + Ok(quote! { pub type #ident = ::#lib_ident::#ident; - } + }) } else { - let variants = spec.cases.iter().map(|c| { - let v_ident = format_ident!("{}", c.name.to_utf8_string().unwrap()); - let v_value = Literal::u32_unsuffixed(c.value); - quote! { #v_ident = #v_value } - }); + let variants = spec + .cases + .iter() + .map(|c| { + let v_ident = str_to_ident(&c.name)?; + let v_value = Literal::u32_unsuffixed(c.value); + Ok(quote! { #v_ident = #v_value }) + }) + .collect::, GenerateError>>()?; let contracterror_attr = if opts.export { quote! { #[soroban_sdk::contracterror] } } else { quote! { #[soroban_sdk::contracterror(export = false)] } }; - quote! { + Ok(quote! { #contracterror_attr #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum #ident { #(#variants,)* } - } + }) } } /// Constructs a token stream containing a single struct that mirrors the event /// spec. -pub fn generate_event(spec: &ScSpecEventV0) -> TokenStream { +pub fn generate_event(spec: &ScSpecEventV0) -> Result { generate_event_with_options(spec, &GenerateOptions::default()) } /// Constructs a token stream containing a single struct that mirrors the event /// spec, with configurable options. -pub fn generate_event_with_options(spec: &ScSpecEventV0, opts: &GenerateOptions) -> TokenStream { - let ident = format_ident!("{}", spec.name.to_utf8_string().unwrap()); +pub fn generate_event_with_options( + spec: &ScSpecEventV0, + opts: &GenerateOptions, +) -> Result { + let ident = str_to_ident(&spec.name)?; if spec.lib.len() > 0 { - let lib_ident = format_ident!("{}", spec.lib.to_utf8_string().unwrap()); - quote! { + let lib_ident = str_to_ident(&spec.lib)?; + Ok(quote! { type #ident = ::#lib_ident::#ident; - } + }) } else { let topics = spec.prefix_topics.iter().map(|t| t.to_string()); - let fields = spec.params.iter().map(|p| { - let p_ident = format_ident!("{}", p.name.to_utf8_string().unwrap()); - let p_type = generate_type_ident(&p.type_); - match p.location { - ScSpecEventParamLocationV0::TopicList => quote! { - #[topic] - pub #p_ident: #p_type - }, - ScSpecEventParamLocationV0::Data => quote! { - pub #p_ident: #p_type - }, - } - }); + let fields = spec + .params + .iter() + .map(|p| { + let p_ident = str_to_ident(&p.name)?; + let p_type = generate_type_ident(&p.type_)?; + Ok(match p.location { + ScSpecEventParamLocationV0::TopicList => quote! { + #[topic] + pub #p_ident: #p_type + }, + ScSpecEventParamLocationV0::Data => quote! { + pub #p_ident: #p_type + }, + }) + }) + .collect::, GenerateError>>()?; let export_attr = if opts.export { quote! {} } else { quote! { export = false, } }; - quote! { + Ok(quote! { #[soroban_sdk::contractevent(#export_attr topics = [#(#topics,)*])] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct #ident { #(#fields,)* } - } + }) } } @@ -236,57 +284,61 @@ fn contracttype_attr(export: bool) -> TokenStream { } } -pub fn generate_type_ident(spec: &ScSpecTypeDef) -> TokenStream { +pub fn generate_type_ident(spec: &ScSpecTypeDef) -> Result { match spec { - ScSpecTypeDef::Val => quote! { soroban_sdk::Val }, - ScSpecTypeDef::U64 => quote! { u64 }, - ScSpecTypeDef::I64 => quote! { i64 }, - ScSpecTypeDef::U32 => quote! { u32 }, - ScSpecTypeDef::I32 => quote! { i32 }, - ScSpecTypeDef::U128 => quote! { u128 }, - ScSpecTypeDef::I128 => quote! { i128 }, - ScSpecTypeDef::Bool => quote! { bool }, - ScSpecTypeDef::Symbol => quote! { soroban_sdk::Symbol }, - ScSpecTypeDef::Error => quote! { soroban_sdk::Error }, - ScSpecTypeDef::Bytes => quote! { soroban_sdk::Bytes }, - ScSpecTypeDef::Address => quote! { soroban_sdk::Address }, - ScSpecTypeDef::MuxedAddress => quote! { soroban_sdk::MuxedAddress }, - ScSpecTypeDef::String => quote! { soroban_sdk::String }, + ScSpecTypeDef::Val => Ok(quote! { soroban_sdk::Val }), + ScSpecTypeDef::U64 => Ok(quote! { u64 }), + ScSpecTypeDef::I64 => Ok(quote! { i64 }), + ScSpecTypeDef::U32 => Ok(quote! { u32 }), + ScSpecTypeDef::I32 => Ok(quote! { i32 }), + ScSpecTypeDef::U128 => Ok(quote! { u128 }), + ScSpecTypeDef::I128 => Ok(quote! { i128 }), + ScSpecTypeDef::Bool => Ok(quote! { bool }), + ScSpecTypeDef::Symbol => Ok(quote! { soroban_sdk::Symbol }), + ScSpecTypeDef::Error => Ok(quote! { soroban_sdk::Error }), + ScSpecTypeDef::Bytes => Ok(quote! { soroban_sdk::Bytes }), + ScSpecTypeDef::Address => Ok(quote! { soroban_sdk::Address }), + ScSpecTypeDef::MuxedAddress => Ok(quote! { soroban_sdk::MuxedAddress }), + ScSpecTypeDef::String => Ok(quote! { soroban_sdk::String }), ScSpecTypeDef::Option(o) => { - let value_ident = generate_type_ident(&o.value_type); - quote! { Option<#value_ident> } + let value_ident = generate_type_ident(&o.value_type)?; + Ok(quote! { Option<#value_ident> }) } ScSpecTypeDef::Result(r) => { - let ok_ident = generate_type_ident(&r.ok_type); - let error_ident = generate_type_ident(&r.error_type); - quote! { Result<#ok_ident, #error_ident> } + let ok_ident = generate_type_ident(&r.ok_type)?; + let error_ident = generate_type_ident(&r.error_type)?; + Ok(quote! { Result<#ok_ident, #error_ident> }) } ScSpecTypeDef::Vec(v) => { - let element_ident = generate_type_ident(&v.element_type); - quote! { soroban_sdk::Vec<#element_ident> } + let element_ident = generate_type_ident(&v.element_type)?; + Ok(quote! { soroban_sdk::Vec<#element_ident> }) } ScSpecTypeDef::Map(m) => { - let key_ident = generate_type_ident(&m.key_type); - let value_ident = generate_type_ident(&m.value_type); - quote! { soroban_sdk::Map<#key_ident, #value_ident> } + let key_ident = generate_type_ident(&m.key_type)?; + let value_ident = generate_type_ident(&m.value_type)?; + Ok(quote! { soroban_sdk::Map<#key_ident, #value_ident> }) } ScSpecTypeDef::Tuple(t) => { - let type_idents = t.value_types.iter().map(generate_type_ident); - quote! { (#(#type_idents,)*) } + let type_idents = t + .value_types + .iter() + .map(generate_type_ident) + .collect::, _>>()?; + Ok(quote! { (#(#type_idents,)*) }) } ScSpecTypeDef::BytesN(b) => { let n = Literal::u32_unsuffixed(b.n); - quote! { soroban_sdk::BytesN<#n> } + Ok(quote! { soroban_sdk::BytesN<#n> }) } ScSpecTypeDef::Udt(u) => { - let ident = format_ident!("{}", u.name.to_utf8_string().unwrap()); - quote! { #ident } + let ident = str_to_ident(&u.name)?; + Ok(quote! { #ident }) } - ScSpecTypeDef::Void => quote! { () }, - ScSpecTypeDef::Timepoint => quote! { soroban_sdk::Timepoint }, - ScSpecTypeDef::Duration => quote! { soroban_sdk::Duration }, - ScSpecTypeDef::U256 => quote! { soroban_sdk::U256 }, - ScSpecTypeDef::I256 => quote! { soroban_sdk::I256 }, + ScSpecTypeDef::Void => Ok(quote! { () }), + ScSpecTypeDef::Timepoint => Ok(quote! { soroban_sdk::Timepoint }), + ScSpecTypeDef::Duration => Ok(quote! { soroban_sdk::Duration }), + ScSpecTypeDef::U256 => Ok(quote! { soroban_sdk::U256 }), + ScSpecTypeDef::I256 => Ok(quote! { soroban_sdk::I256 }), } } @@ -294,12 +346,12 @@ pub fn generate_type_ident(spec: &ScSpecTypeDef) -> TokenStream { mod test { use crate::ToFormattedString; - use super::generate_event; + use super::{generate_event, generate_struct, GenerateError}; use quote::quote; use stellar_xdr::curr as stellar_xdr; use stellar_xdr::{ ScSpecEventDataFormat, ScSpecEventParamLocationV0, ScSpecEventParamV0, ScSpecEventV0, - ScSpecTypeDef, ScSymbol, + ScSpecTypeDef, ScSpecUdtStructFieldV0, ScSpecUdtStructV0, ScSymbol, VecM, }; #[test] @@ -311,7 +363,8 @@ mod test { prefix_topics: [].try_into().unwrap(), params: [].try_into().unwrap(), data_format: ScSpecEventDataFormat::Map, - }); + }) + .unwrap(); let expect = quote! { #[soroban_sdk::contractevent(export = false, topics = [])] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] @@ -349,7 +402,8 @@ mod test { .try_into() .unwrap(), data_format: ScSpecEventDataFormat::Map, - }); + }) + .unwrap(); let expect = quote! { #[soroban_sdk::contractevent(export = false, topics = ["my_event"])] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] @@ -364,4 +418,46 @@ mod test { expect.to_formatted_string().unwrap() ); } + + #[test] + fn test_invalid_utf8_returns_error() { + let spec = ScSpecUdtStructV0 { + doc: "".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: vec![0xff, 0xfe].try_into().unwrap(), + fields: VecM::default(), + }; + let result = generate_struct(&spec); + assert!(matches!(result, Err(GenerateError::InvalidUtf8))); + } + + #[test] + fn test_invalid_ident_returns_error() { + let spec = ScSpecUdtStructV0 { + doc: "".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: "not a valid ident".try_into().unwrap(), + fields: VecM::default(), + }; + let result = generate_struct(&spec); + assert!(matches!(result, Err(GenerateError::InvalidIdent(_)))); + } + + #[test] + fn test_invalid_utf8_in_field_name_returns_error() { + let spec = ScSpecUdtStructV0 { + doc: "".try_into().unwrap(), + lib: "".try_into().unwrap(), + name: "ValidName".try_into().unwrap(), + fields: vec![ScSpecUdtStructFieldV0 { + doc: "".try_into().unwrap(), + name: vec![0xff].try_into().unwrap(), + type_: ScSpecTypeDef::U32, + }] + .try_into() + .unwrap(), + }; + let result = generate_struct(&spec); + assert!(result.is_err()); + } }