use super::runtime::to_pascal_case;
use crate::structured_ir::SchemaElements;
pub(crate) fn emit_conversion_traits(src: &mut String) {
src.push_str(
"/// Convert from a wire type to an application type.\n\
#[diagnostic::on_unimplemented(\n\
message = \"`{Self}` has no `TryFromSbe<{Wire}>` impl\",\n\
label = \"missing `impl TryFromSbe<{Wire}> for {Self}`\",\n\
note = \"if this field uses DomainImpl::Manual, the generated `try_*` \
accessor's doc comment has a ready-to-paste starting point\"\n\
)]\n\
pub trait TryFromSbe<Wire>: Sized {\n\
type Error: core::fmt::Debug + core::fmt::Display;\n\
fn try_from_sbe(wire: Wire) -> Result<Self, Self::Error>;\n\
}\n\n\
/// Convert from an application type to a wire type.\n\
#[diagnostic::on_unimplemented(\n\
message = \"`{Self}` has no `TryToSbe<{Wire}>` impl\",\n\
label = \"missing `impl TryToSbe<{Wire}> for {Self}`\",\n\
note = \"if this field uses DomainImpl::Manual, the generated `try_*` \
accessor's doc comment has a ready-to-paste starting point\"\n\
)]\n\
pub trait TryToSbe<Wire> {\n\
type Error: core::fmt::Debug + core::fmt::Display;\n\
fn try_to_sbe(&self) -> Result<Wire, Self::Error>;\n\
}\n\n",
);
}
fn bool_impl_tokens(bt_ident: &syn::Ident) -> proc_macro2::TokenStream {
quote::quote! {
impl TryFromSbe<#bt_ident> for bool {
type Error = &'static str;
#[inline]
fn try_from_sbe(wire: #bt_ident) -> Result<Self, Self::Error> {
wire.as_bool().ok_or("null or unknown boolean discriminant")
}
}
impl TryToSbe<#bt_ident> for bool {
type Error = &'static str;
#[inline]
fn try_to_sbe(&self) -> Result<#bt_ident, Self::Error> {
Ok(#bt_ident::from(*self))
}
}
}
}
fn decimal_impl_tokens(
dec_ident: &syn::Ident,
exponent_is_constant: bool,
mantissa_is_optional: bool,
) -> proc_macro2::TokenStream {
let dec_new_call: proc_macro2::TokenStream = if exponent_is_constant {
quote::quote! { #dec_ident::new(mantissa) }
} else {
quote::quote! { #dec_ident::new(mantissa, -(self.scale() as i8)) }
};
let mantissa_expr: proc_macro2::TokenStream = if mantissa_is_optional {
quote::quote! { wire.mantissa().ok_or("null Decimal mantissa")? as i128 }
} else {
quote::quote! { wire.mantissa() as i128 }
};
quote::quote! {
impl TryFromSbe<#dec_ident> for rust_decimal::Decimal {
type Error = &'static str;
#[inline]
fn try_from_sbe(wire: #dec_ident) -> Result<Self, Self::Error> {
let mantissa = #mantissa_expr;
let exponent = wire.exponent() as i32;
let (mantissa, scale) = if exponent < 0 {
let scale = exponent.unsigned_abs();
(mantissa, scale)
} else {
let pow = 10i128.checked_pow(exponent as u32)
.ok_or("Decimal exponent overflow")?;
let scaled = mantissa.checked_mul(pow)
.ok_or("Decimal mantissa overflow")?;
(scaled, 0)
};
rust_decimal::Decimal::from_i128_with_scale(mantissa, scale)
.try_into()
.map_err(|_| "Decimal overflow")
}
}
impl TryToSbe<#dec_ident> for rust_decimal::Decimal {
type Error = &'static str;
#[inline]
fn try_to_sbe(&self) -> Result<#dec_ident, Self::Error> {
let mantissa: i64 = self.mantissa()
.try_into()
.map_err(|_| "Decimal mantissa overflow i64")?;
Ok(#dec_new_call)
}
}
}
}
fn chrono_impl_tokens() -> proc_macro2::TokenStream {
quote::quote! {
impl TryFromSbe<u64> for chrono::DateTime<chrono::Utc> {
type Error = &'static str;
#[inline]
fn try_from_sbe(wire: u64) -> Result<Self, Self::Error> {
let secs = (wire / 1_000_000_000) as i64;
let nsec = (wire % 1_000_000_000) as u32;
chrono::DateTime::from_timestamp(secs, nsec)
.ok_or("timestamp out of range for DateTime<Utc>")
}
}
impl TryToSbe<u64> for chrono::DateTime<chrono::Utc> {
type Error = &'static str;
#[inline]
fn try_to_sbe(&self) -> Result<u64, Self::Error> {
let total_nanos = self.timestamp_nanos_opt()
.ok_or("timestamp_nanos overflow")?;
u64::try_from(total_nanos)
.map_err(|_| "timestamp out of u64 range")
}
}
}
}
fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
syn::parse_str::<syn::File>(&ts.to_string())
.map(|file| prettyplease::unparse(&file))
.unwrap_or_else(|_| ts.to_string())
}
pub(crate) fn generate_conversion_impl_blocks(
elements: &SchemaElements,
_conversions: &[crate::ConversionSelector],
domain_types: &[(crate::ConversionSelector, String)],
manual_impl_selectors: &[crate::ConversionSelector],
) -> String {
let mut out = String::new();
let span = proc_macro2::Span::call_site();
for (sel, ty) in domain_types {
if ty != "bool" || manual_impl_selectors.contains(sel) {
continue;
}
let bt_name = match sel {
crate::ConversionSelector::NamedType(n) => to_pascal_case(n),
_ => continue,
};
let bt_ident = syn::Ident::new(&bt_name, span);
out.push_str(&bool_impl_tokens(&bt_ident).to_string());
}
let has_chrono_conv = domain_types.iter().any(|(sel, _)| {
matches!(sel, crate::ConversionSelector::SemanticType(st) if st == "UTCTimestamp")
&& !manual_impl_selectors.contains(sel)
});
for (sel, ty) in domain_types {
if ty != "rust_decimal::Decimal" || manual_impl_selectors.contains(sel) {
continue;
}
let comp_name = match sel {
crate::ConversionSelector::NamedType(n) => n.as_str(),
_ => continue,
};
let Some(dec_composite) = elements.composites.iter().find(|c| c[0].name == comp_name)
else {
continue;
};
let dec_ident = syn::Ident::new(&to_pascal_case(comp_name), span);
let exponent_is_constant = composite_exponent_is_constant(dec_composite);
let mantissa_is_optional = composite_mantissa_is_optional(dec_composite);
let ts = decimal_impl_tokens(&dec_ident, exponent_is_constant, mantissa_is_optional);
out.push_str(&ts.to_string());
}
if has_chrono_conv {
out.push_str(&chrono_impl_tokens().to_string());
}
out
}
fn composite_exponent_is_constant(dec_composite: &[crate::ir::Token]) -> bool {
dec_composite
.iter()
.find(|t| t.name == "exponent")
.map(|t| t.encoding.presence == crate::ir::Presence::Constant)
.unwrap_or(false)
}
fn composite_mantissa_is_optional(dec_composite: &[crate::ir::Token]) -> bool {
dec_composite
.iter()
.find(|t| t.name == "mantissa")
.map(|t| t.encoding.presence == crate::ir::Presence::Optional)
.unwrap_or(false)
}
pub(crate) fn generate_manual_impl_snippets(
elements: &SchemaElements,
domain_types: &[(crate::ConversionSelector, String)],
manual_impl_selectors: &[crate::ConversionSelector],
) -> Vec<(crate::ConversionSelector, String)> {
let span = proc_macro2::Span::call_site();
let mut out = Vec::new();
for (sel, ty) in domain_types {
if !manual_impl_selectors.contains(sel) {
continue;
}
let ts = match ty.as_str() {
"bool" => {
let crate::ConversionSelector::NamedType(n) = sel else {
continue;
};
let bt_ident = syn::Ident::new(&to_pascal_case(n), span);
bool_impl_tokens(&bt_ident)
}
"rust_decimal::Decimal" => {
let crate::ConversionSelector::NamedType(comp_name) = sel else {
continue;
};
let Some(dec_composite) =
elements.composites.iter().find(|c| &c[0].name == comp_name)
else {
continue;
};
let dec_ident = syn::Ident::new(&to_pascal_case(comp_name), span);
decimal_impl_tokens(
&dec_ident,
composite_exponent_is_constant(dec_composite),
composite_mantissa_is_optional(dec_composite),
)
}
"chrono::DateTime<chrono::Utc>" if matches!(sel, crate::ConversionSelector::SemanticType(st) if st == "UTCTimestamp") => {
chrono_impl_tokens()
}
_ => continue,
};
out.push((sel.clone(), pretty_print(&ts)));
}
out
}