diff --git a/macros/src/data_type_from/mod.rs b/macros/src/data_type_from/mod.rs index 628ad86..f8d702a 100644 --- a/macros/src/data_type_from/mod.rs +++ b/macros/src/data_type_from/mod.rs @@ -9,7 +9,11 @@ use crate::utils::parse_attrs; pub fn derive(input: proc_macro::TokenStream) -> syn::Result { let DeriveInput { - ident, data, attrs, .. + ident, + data, + attrs, + generics, + .. } = &parse_macro_input::parse::(input)?; let mut attrs = parse_attrs(attrs)?; @@ -17,6 +21,13 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result 0 { + return Err(syn::Error::new_spanned( + generics, + "DataTypeFrom does not support generics", + )); + } + Ok(match data { Data::Struct(data) => match &data.fields { Fields::Named(_) => { @@ -42,53 +53,55 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result for #crate_ref::StructType { fn from(t: #ident) -> #crate_ref::StructType { - #crate_ref::internal::construct::r#struct(vec![], vec![#(#fields),*], None) + #crate_ref::internal::construct::r#struct(#struct_name.into(), vec![], #crate_ref::internal::construct::struct_named(vec![#(#fields),*], None)) } } #[automatically_derived] impl From<#ident> for #crate_ref::DataType { fn from(t: #ident) -> #crate_ref::DataType { - #crate_ref::DataType::Struct(t.into()) + Self::Struct(t.into()) } } } } Fields::Unnamed(_) => { - let fields = data.fields.iter().enumerate().map(|(i, _)| { - let i = proc_macro2::Literal::usize_unsuffixed(i); - quote!(t.#i.into()) - }); + let fields = data + .fields + .iter() + .enumerate() + .map(|(i, _)| { + let i = proc_macro2::Literal::usize_unsuffixed(i); + quote!(t.#i.into()) + }) + .collect::>(); quote! { #[automatically_derived] impl From<#ident> for #crate_ref::TupleType { fn from(t: #ident) -> #crate_ref::TupleType { - #crate_ref::TupleType::Named { - generics: vec![], - fields: vec![#(#fields),*] - } + #crate_ref::internal::construct::tuple( + vec![#(#fields),*] + ) } } #[automatically_derived] impl From<#ident> for #crate_ref::DataType { fn from(t: #ident) -> #crate_ref::DataType { - #crate_ref::DataType::Tuple(t.into()) + Self::Tuple(t.into()) } } } diff --git a/macros/src/type/attr/container.rs b/macros/src/type/attr/container.rs index 4cd8456..b0e4c85 100644 --- a/macros/src/type/attr/container.rs +++ b/macros/src/type/attr/container.rs @@ -15,6 +15,9 @@ pub struct ContainerAttr { pub export: Option, pub doc: Vec, pub deprecated: Option, + + // Struct ony (we pass it anyway so enums get nice errors) + pub transparent: bool, } impl_parse! { @@ -53,6 +56,7 @@ impl_parse! { out.deprecated = out.deprecated.take().or(Some(attr.parse_string()?)); } }, + "transparent" => out.transparent = attr.parse_bool().unwrap_or(true) } } @@ -62,6 +66,7 @@ impl ContainerAttr { Self::try_from_attrs("specta", attrs, &mut result)?; #[cfg(feature = "serde")] Self::try_from_attrs("serde", attrs, &mut result)?; + Self::try_from_attrs("repr", attrs, &mut result)?; // To handle `#[repr(transparent)]` Self::try_from_attrs("doc", attrs, &mut result)?; Ok(result) } diff --git a/macros/src/type/attr/field.rs b/macros/src/type/attr/field.rs index 3db4748..ad60dc7 100644 --- a/macros/src/type/attr/field.rs +++ b/macros/src/type/attr/field.rs @@ -39,6 +39,7 @@ impl_parse! { "skip_serializing" => out.skip = true, "skip_deserializing" => out.skip = true, "skip_serializing_if" => out.optional = attr.parse_string()? == *"Option::is_none", + // Specta only attribute "optional" => out.optional = attr.parse_bool().unwrap_or(true), "default" => out.optional = attr.parse_bool().unwrap_or(true), "flatten" => out.flatten = attr.parse_bool().unwrap_or(true) diff --git a/macros/src/type/attr/mod.rs b/macros/src/type/attr/mod.rs index e211baf..11690bf 100644 --- a/macros/src/type/attr/mod.rs +++ b/macros/src/type/attr/mod.rs @@ -1,11 +1,9 @@ pub use container::*; pub use field::*; pub use r#enum::*; -pub use r#struct::*; pub use variant::*; mod container; mod r#enum; mod field; -mod r#struct; mod variant; diff --git a/macros/src/type/attr/struct.rs b/macros/src/type/attr/struct.rs deleted file mode 100644 index 1c918be..0000000 --- a/macros/src/type/attr/struct.rs +++ /dev/null @@ -1,25 +0,0 @@ -use syn::Result; - -use crate::utils::Attribute; - -#[derive(Default)] -pub struct StructAttr { - pub transparent: bool, -} - -impl_parse! { - StructAttr(attr, out) { - "transparent" => out.transparent = attr.parse_bool().unwrap_or(true) - } -} - -impl StructAttr { - pub fn from_attrs(attrs: &mut Vec) -> Result { - let mut result = Self::default(); - Self::try_from_attrs("specta", attrs, &mut result)?; - #[cfg(feature = "serde")] - Self::try_from_attrs("serde", attrs, &mut result)?; - Self::try_from_attrs("repr", attrs, &mut result)?; // To handle `#[repr(transparent)]` - Ok(result) - } -} diff --git a/macros/src/type/enum.rs b/macros/src/type/enum.rs index 2fa4b05..b8b23cf 100644 --- a/macros/src/type/enum.rs +++ b/macros/src/type/enum.rs @@ -1,10 +1,8 @@ -use super::{ - attr::*, generics::construct_datatype, named_data_type_wrapper, r#struct::decode_field_attrs, -}; +use super::{attr::*, generics::construct_datatype, r#struct::decode_field_attrs}; use crate::utils::*; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{DataEnum, Fields, GenericParam, Generics}; +use syn::{spanned::Spanned, DataEnum, Fields, GenericParam, Generics}; pub fn parse_enum( name: &TokenStream, @@ -14,6 +12,13 @@ pub fn parse_enum( crate_ref: &TokenStream, data: &DataEnum, ) -> syn::Result<(TokenStream, TokenStream, bool)> { + if container_attrs.transparent { + return Err(syn::Error::new( + data.enum_token.span(), + "#[specta(transparent)] is not allowed on an enum", + )); + } + let generic_idents = generics .params .iter() @@ -40,100 +45,60 @@ pub fn parse_enum( generics .get(#i) .cloned() - .map_or_else(|| <#ident as #crate_ref::Type>::reference( + .unwrap_or_else(|| <#ident as #crate_ref::Type>::reference( #crate_ref::DefOpts { parent_inline: #parent_inline, type_map: opts.type_map, }, &[], - ), Ok)? + ).inner) } }); let repr = enum_attrs.tagged()?; - let (variant_names, variant_types): (Vec<_>, Vec<_>) = data - .variants - .iter() - .map(|v| { - // We pass all the attributes at the start and when decoding them pop them off the list. - // This means at the end we can check for any that weren't consumed and throw an error. - let mut attrs = parse_attrs(&v.attrs)?; - let variant_attrs = VariantAttr::from_attrs(&mut attrs)?; - - attrs - .iter() - .find(|attr| attr.root_ident == "specta") - .map_or(Ok(()), |attr| { - Err(syn::Error::new( - attr.key.span(), - format!("specta: Found unsupported enum attribute '{}'", attr.key), - )) - })?; - - Ok((v, variant_attrs)) - }) - .collect::>>()? - .into_iter() - .filter(|(_, attrs)| !attrs.skip) - .map(|(variant, attrs)| { - let variant_ident_str = unraw_raw_ident(&variant.ident); - - let variant_name_str = match (attrs.rename, container_attrs.rename_all) { - (Some(name), _) => name, - (_, Some(inflection)) => inflection.apply(&variant_ident_str), - (_, _) => variant_ident_str, - }; - - let generic_idents = generic_idents.clone().collect::>(); - - Ok(( - variant_name_str, - match &variant.fields { - Fields::Unit => { - quote!(#crate_ref::EnumVariant::Unit) - } + let variant_types = + data.variants + .iter() + .map(|v| { + // We pass all the attributes at the start and when decoding them pop them off the list. + // This means at the end we can check for any that weren't consumed and throw an error. + let mut attrs = parse_attrs(&v.attrs)?; + let variant_attrs = VariantAttr::from_attrs(&mut attrs)?; + + attrs + .iter() + .find(|attr| attr.root_ident == "specta") + .map_or(Ok(()), |attr| { + Err(syn::Error::new( + attr.key.span(), + format!("specta: Found unsupported enum attribute '{}'", attr.key), + )) + })?; + + Ok((v, variant_attrs)) + }) + .collect::>>()? + .into_iter() + .filter(|(_, attrs)| !attrs.skip) + .map(|(variant, attrs)| { + let variant_ident_str = unraw_raw_ident(&variant.ident); + + let variant_name_str = match (attrs.rename, container_attrs.rename_all) { + (Some(name), _) => name, + (_, Some(inflection)) => inflection.apply(&variant_ident_str), + (_, _) => variant_ident_str, + }; + + let generic_idents = generic_idents.clone().collect::>(); + + let inner = match &variant.fields { + Fields::Unit => quote!(#crate_ref::internal::construct::enum_variant_unit()), Fields::Unnamed(fields) => { - let inner = if fields.unnamed.len() == 0 { - quote!(#crate_ref::TupleType::Unnamed) - } else { - let fields = fields - .unnamed - .iter() - .map(|field| { - let (field, field_attrs) = decode_field_attrs(field)?; - let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); - - let generic_vars = construct_datatype( - format_ident!("gen"), - field_ty, - &generic_idents, - crate_ref, - attrs.inline, - )?; - - Ok(quote!({ - #generic_vars - - gen - })) - }) - .collect::>>()?; - - quote!(#crate_ref::TupleType::Named { - fields: vec![#(#fields.into()),*], - generics: vec![] - }) - }; - - quote!(#crate_ref::EnumVariant::Unnamed(#inner)) - } - Fields::Named(fields) => { let fields = fields - .named + .unnamed .iter() .map(|field| { let (field, field_attrs) = decode_field_attrs(field)?; - let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); let generic_vars = construct_datatype( @@ -144,57 +109,80 @@ pub fn parse_enum( attrs.inline, )?; - let field_ident_str = - unraw_raw_ident(field.ident.as_ref().unwrap()); - - let field_name = match (field_attrs.rename, attrs.rename_all) { - (Some(name), _) => name, - (_, Some(inflection)) => { - let name = inflection.apply(&field_ident_str); - quote::quote!(#name) - } - (_, _) => quote::quote!(#field_ident_str), - }; - - Ok(quote!(#crate_ref::internal::construct::struct_field( - #field_name.into(), + Ok(quote!(#crate_ref::internal::construct::field( false, false, - { + { #generic_vars gen - }, + } ))) }) .collect::>>()?; - quote!(#crate_ref::EnumVariant::Named(#crate_ref::internal::construct::r#struct(vec![], vec![#(#fields),*], None))) + quote!(#crate_ref::internal::construct::enum_variant_unnamed( + vec![#(#fields),*], + )) } - }, - )) - }) - .collect::>>()? - .into_iter() - .unzip(); + Fields::Named(fields) => { + let fields = fields + .named + .iter() + .map(|field| { + let (field, field_attrs) = decode_field_attrs(field)?; + + let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); + + let generic_vars = construct_datatype( + format_ident!("gen"), + field_ty, + &generic_idents, + crate_ref, + attrs.inline, + )?; + + let field_ident_str = + unraw_raw_ident(field.ident.as_ref().unwrap()); + + let field_name = match (field_attrs.rename, attrs.rename_all) { + (Some(name), _) => name, + (_, Some(inflection)) => { + let name = inflection.apply(&field_ident_str); + quote::quote!(#name) + } + (_, _) => quote::quote!(#field_ident_str), + }; + + Ok(quote!((#field_name.into(), #crate_ref::internal::construct::field( + false, + false, + { + #generic_vars + + gen + }, + )))) + }) + .collect::>>()?; + + quote!(#crate_ref::internal::construct::enum_variant_named(vec![#(#fields),*], None)) + } + }; + + Ok(quote!((#variant_name_str.into(), #inner))) + }) + .collect::>>()?; - let (enum_impl, can_flatten) = match repr { + let (repr, can_flatten) = match repr { Tagged::Untagged => ( - quote! { - #crate_ref::internal::construct::untagged_enum(vec![#(#variant_types),*], vec![#(#definition_generics),*]) - }, + quote!(#crate_ref::EnumRepr::Untagged), data.variants .iter() .any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))), ), Tagged::Externally => ( - quote! { - #crate_ref::internal::construct::tagged_enum( - vec![#((#variant_names.into(), #variant_types)),*], - vec![#(#definition_generics),*], - #crate_ref::EnumRepr::External - ) - }, + quote!(#crate_ref::EnumRepr::External), data.variants.iter().any(|v| match &v.fields { Fields::Unnamed(f) if f.unnamed.len() == 1 => true, Fields::Named(_) => true, @@ -202,49 +190,27 @@ pub fn parse_enum( }), ), Tagged::Adjacently { tag, content } => ( - quote! { - #crate_ref::internal::construct::tagged_enum( - vec![#((#variant_names.into(), #variant_types)),*], - vec![#(#definition_generics),*], - #crate_ref::EnumRepr::Adjacent { tag: #tag.into(), content: #content.into() } - ) - }, + quote!(#crate_ref::EnumRepr::Adjacent { tag: #tag.into(), content: #content.into() }), true, ), Tagged::Internally { tag } => ( - quote! { - #crate_ref::internal::construct::tagged_enum( - vec![#((#variant_names.into(), #variant_types)),*], - vec![#(#definition_generics),*], - #crate_ref::EnumRepr::Internal { tag: #tag.into() }, - ) - }, + quote!(#crate_ref::EnumRepr::Internal { tag: #tag.into() }), data.variants .iter() .any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))), ), }; - let body = named_data_type_wrapper( - crate_ref, - container_attrs, - name, - quote! { - #crate_ref::NamedDataTypeItem::Enum( - #enum_impl - ) - }, - ); - Ok(( - body, - quote! { - #crate_ref::TypeCategory::Reference(#crate_ref::internal::construct::data_type_reference( + quote!(#crate_ref::DataType::Enum(#crate_ref::internal::construct::r#enum(#name.into(), #repr, vec![#(#definition_generics),*], vec![#(#variant_types),*]))), + quote!({ + let generics = vec![#(#reference_generics),*]; + #crate_ref::reference::reference::(opts, &generics, #crate_ref::internal::construct::data_type_reference( #name.into(), SID, - vec![#(#reference_generics),*], + generics.clone() // TODO: This `clone` is cringe )) - }, + }), can_flatten, )) } diff --git a/macros/src/type/generics.rs b/macros/src/type/generics.rs index 39308c2..4094680 100644 --- a/macros/src/type/generics.rs +++ b/macros/src/type/generics.rs @@ -87,9 +87,9 @@ pub fn construct_datatype( crate_ref: &TokenStream, inline: bool, ) -> syn::Result { - let method = match inline { - true => quote!(inline), - false => quote!(reference), + let (method, transform) = match inline { + true => (quote!(inline), quote!()), + false => (quote!(reference), quote!(.inner)), }; let parent_inline = inline.then(|| quote!(true)).unwrap_or(quote!(false)); @@ -126,7 +126,7 @@ pub fn construct_datatype( type_map: opts.type_map }, &[#(#generic_var_idents),*] - )?; + )#transform; }); } Type::Array(TypeArray { elem, .. }) | Type::Slice(TypeSlice { elem, .. }) => { @@ -148,7 +148,7 @@ pub fn construct_datatype( type_map: opts.type_map }, &[#elem_var_ident] - )?; + )#transform; }); } Type::Ptr(TypePtr { elem, .. }) | Type::Reference(TypeReference { elem, .. }) => { @@ -169,7 +169,7 @@ pub fn construct_datatype( type_map: opts.type_map }, &[] - )?; + )#transform; }); } ty => { @@ -190,7 +190,7 @@ pub fn construct_datatype( { let type_ident = type_ident.to_string(); return Ok(quote! { - let #var_ident = generics.get(#i).cloned().map_or_else( + let #var_ident = generics.get(#i).cloned().unwrap_or_else( || { <#generic_ident as #crate_ref::Type>::#method( #crate_ref::DefOpts { @@ -198,10 +198,9 @@ pub fn construct_datatype( type_map: opts.type_map }, &[#crate_ref::DataType::Generic(std::borrow::Cow::Borrowed(#type_ident).into())] - ) + )#transform }, - Ok, - )?; + ); }); } } @@ -251,6 +250,6 @@ pub fn construct_datatype( type_map: opts.type_map }, &[#(#generic_var_idents),*] - )?; + )#transform; }) } diff --git a/macros/src/type/mod.rs b/macros/src/type/mod.rs index 1d4c6a7..efc41d9 100644 --- a/macros/src/type/mod.rs +++ b/macros/src/type/mod.rs @@ -38,26 +38,20 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result parse_struct( - &name, - (&container_attrs, StructAttr::from_attrs(&mut attrs)?), - generics, - &crate_name, - data, - ), + let (inlines, reference, can_flatten) = match data { + Data::Struct(data) => parse_struct(&name, &container_attrs, generics, &crate_ref, data), Data::Enum(data) => parse_enum( &name, &EnumAttr::from_attrs(&container_attrs, &mut attrs)?, &container_attrs, generics, - &crate_name, + &crate_ref, data, ), Data::Union(data) => Err(syn::Error::new_spanned( @@ -86,16 +80,16 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result syn::Result>(); + #crate_ref::export::register_ty::<#ident<#(#generic_params),*>>(); } } }); + let comments = { + let comments = &container_attrs.doc; + quote!(vec![#(#comments.into()),*]) + }; + let should_export = match container_attrs.export { + Some(export) => quote!(Some(#export)), + None => quote!(None), + }; + let deprecated = match &container_attrs.deprecated { + Some(msg) => quote!(Some(#msg.into())), + None => quote!(None), + }; + Ok(quote! { const _: () = { - // We do this so `sid!()` is only called once, preventing the type ended up with multiple ids - const SID: #crate_name::SpectaID = #crate_name::sid!(@with_specta_path; #name; #crate_name); - const IMPL_LOCATION: #crate_name::ImplLocation = #crate_name::impl_location!(@with_specta_path; #crate_name); + // We do this so `sid!()` is only called once, as it does a hashing operation. + const SID: #crate_ref::SpectaID = #crate_ref::sid!(@with_specta_path; #name; #crate_ref); + const IMPL_LOCATION: #crate_ref::ImplLocation = #crate_ref::impl_location!(@with_specta_path; #crate_ref); - // We do this so `sid!()` is only called once, preventing the type ended up with multiple ids #[automatically_derived] #type_impl_heading { - fn inline(opts: #crate_name::DefOpts, generics: &[#crate_name::DataType]) -> std::result::Result<#crate_name::DataType, #crate_name::ExportError> { - Ok(#crate_name::DataType::Named(::named_data_type(opts, generics)?)) + fn inline(opts: #crate_ref::DefOpts, generics: &[#crate_ref::DataType]) -> #crate_ref::DataType { + #inlines } - fn category_impl(opts: #crate_name::DefOpts, generics: &[#crate_name::DataType]) -> std::result::Result<#crate_name::TypeCategory, #crate_name::ExportError> { - Ok(#category) + fn definition_generics() -> Vec<#crate_ref::GenericType> { + vec![#(#definition_generics),*] } - fn definition_generics() -> Vec<#crate_name::GenericType> { - vec![#(#definition_generics),*] + fn reference(opts: #crate_ref::DefOpts, generics: &[#crate_ref::DataType]) -> #crate_ref::reference::Reference { + #reference } } #[automatically_derived] - impl #bounds #crate_name::NamedType for #ident #type_args #where_bound { - const SID: #crate_name::SpectaID = SID; - const IMPL_LOCATION: #crate_name::ImplLocation = IMPL_LOCATION; - - fn named_data_type(opts: #crate_name::DefOpts, generics: &[#crate_name::DataType]) -> std::result::Result<#crate_name::NamedDataType, #crate_name::ExportError> { - Ok(#inlines) + impl #bounds #crate_ref::NamedType for #ident #type_args #where_bound { + const SID: #crate_ref::SpectaID = SID; + const IMPL_LOCATION: #crate_ref::ImplLocation = IMPL_LOCATION; + + fn named_data_type(opts: #crate_ref::DefOpts, generics: &[#crate_ref::DataType]) -> #crate_ref::NamedDataType { + #crate_ref::internal::construct::named_data_type( + #name.into(), + #comments, + #deprecated, + SID, + IMPL_LOCATION, + #should_export, + ::inline(opts, generics) + ) } } @@ -154,35 +168,3 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result TokenStream { - let comments = { - let comments = &container_attrs.doc; - quote!(vec![#(#comments.into()),*]) - }; - let should_export = match container_attrs.export { - Some(export) => quote!(Some(#export)), - None => quote!(None), - }; - let deprecated = match &container_attrs.deprecated { - Some(msg) => quote!(Some(#msg.into())), - None => quote!(None), - }; - - quote! { - #crate_ref::internal::construct::named_data_type( - #name.into(), - #comments, - #deprecated, - SID, - IMPL_LOCATION, - #should_export, - #t - ) - } -} diff --git a/macros/src/type/struct.rs b/macros/src/type/struct.rs index 95da73a..2c82481 100644 --- a/macros/src/type/struct.rs +++ b/macros/src/type/struct.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{spanned::Spanned, DataStruct, Field, Fields, GenericParam, Generics}; -use super::{attr::*, generics::construct_datatype, named_data_type_wrapper}; +use super::{attr::*, generics::construct_datatype}; pub fn decode_field_attrs(field: &Field) -> syn::Result<(&Field, FieldAttr)> { // We pass all the attributes at the start and when decoding them pop them off the list. @@ -26,7 +26,7 @@ pub fn decode_field_attrs(field: &Field) -> syn::Result<(&Field, FieldAttr)> { pub fn parse_struct( name: &TokenStream, - (container_attrs, struct_attrs): (&ContainerAttr, StructAttr), + container_attrs: &ContainerAttr, generics: &Generics, crate_ref: &TokenStream, data: &DataStruct, @@ -51,13 +51,13 @@ pub fn parse_struct( generics .get(#i) .cloned() - .map_or_else(|| <#ident as #crate_ref::Type>::reference( + .unwrap_or_else(|| <#ident as #crate_ref::Type>::reference( #crate_ref::DefOpts { parent_inline: #parent_inline, type_map: opts.type_map, }, &[], - ), Ok)? + ).inner) } }); @@ -66,224 +66,192 @@ pub fn parse_struct( quote!(std::borrow::Cow::Borrowed(#ident).into()) }); - let definition = match &data.fields { - Fields::Named(_) => { - let fields = data.fields.iter().map(decode_field_attrs) - .collect::>>()? - .iter() - .filter_map(|(field, field_attrs)| { - - if field_attrs.skip { - return None; - } - - Some((field, field_attrs)) - }).map(|(field, field_attrs)| { - let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); - - let ty = construct_datatype( - format_ident!("ty"), - field_ty, - &generic_idents, - crate_ref, - field_attrs.inline, - )?; - - let field_ident_str = unraw_raw_ident(field.ident.as_ref().unwrap()); - - let field_name = match (field_attrs.rename.clone(), container_attrs.rename_all) { - (Some(name), _) => name, - (_, Some(inflection)) => { - let name = inflection.apply(&field_ident_str); - quote::quote!(#name) - }, - (_, _) => quote::quote!(#field_ident_str), - }; - - let optional = field_attrs.optional; - let flatten = field_attrs.flatten; - - let parent_inline = container_attrs - .inline - .then(|| quote!(true)) - .unwrap_or(parent_inline.clone()); - - let ty = if field_attrs.flatten { - quote! { - #[allow(warnings)] - { - #ty - } - - fn validate_flatten() {} - validate_flatten::<#field_ty>(); - - let mut ty = <#field_ty as #crate_ref::Type>::inline(#crate_ref::DefOpts { - parent_inline: #parent_inline, - type_map: opts.type_map - }, &generics)?; - - match &mut ty { - #crate_ref::DataType::Enum(item) => { - item.make_flattenable(IMPL_LOCATION)?; - } - #crate_ref::DataType::Named(#crate_ref::NamedDataType { item: #crate_ref::NamedDataTypeItem::Enum(item), .. }) => { - item.make_flattenable(IMPL_LOCATION)?; - } - _ => {} - } + let definition = if container_attrs.transparent { + if let Fields::Unit = data.fields { + return Err(syn::Error::new( + data.fields.span(), + "specta: unit structs cannot be transparent", + )); + } else if data.fields.len() != 1 { + return Err(syn::Error::new( + data.fields.span(), + "specta: transparent structs must have exactly one field", + )); + } - ty - } - } else { - quote! { - #ty + let (field, field_attrs) = decode_field_attrs( + data.fields + .iter() + .next() + .expect("unreachable: we just checked this!"), + )?; - ty - } - }; + let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); - Ok(quote!(#crate_ref::internal::construct::struct_field( - #field_name.into(), - #optional, - #flatten, - { - #ty - } - ))) - }).collect::>>()?; + let ty = construct_datatype( + format_ident!("ty"), + &field_ty, + &generic_idents, + crate_ref, + field_attrs.inline, + )?; - let tag = container_attrs - .tag - .as_ref() - .map(|t| quote!(Some(#t.into()))) - .unwrap_or(quote!(None)); + quote!({ + #ty - named_data_type_wrapper( - crate_ref, - container_attrs, - name, - quote! { - #crate_ref::NamedDataTypeItem::Struct(#crate_ref::internal::construct::r#struct(vec![#(#definition_generics),*], vec![#(#fields),*], #tag)) - }, - ) - } - Fields::Unnamed(fields) => { - let inner = match (fields.unnamed.len(), struct_attrs.transparent) { - (0, _) => { - quote!(#crate_ref::NamedDataTypeItem::Tuple(#crate_ref::TupleType::Unnamed),) - } - (_, true) => { - if data.fields.len() != 1 { - return Err(syn::Error::new( - data.fields.span(), - "specta: transparent structs must have exactly one field", - )); + ty + }) + } else { + let fields = match &data.fields { + Fields::Named(_) => { + let fields = data.fields.iter().map(decode_field_attrs) + .collect::>>()? + .iter() + .filter_map(|(field, field_attrs)| { + + if field_attrs.skip { + return None; } - - let (field, field_attrs) = decode_field_attrs( - data.fields - .iter() - .next() - .expect("Unreachable: we just checked this!"), - )?; - + + Some((field, field_attrs)) + }).map(|(field, field_attrs)| { let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); - + let ty = construct_datatype( format_ident!("ty"), - &field_ty, + field_ty, &generic_idents, crate_ref, field_attrs.inline, )?; - - quote! { - #crate_ref::NamedDataTypeItem::Tuple(#crate_ref::TupleType::Named { - generics: vec![#(#definition_generics),*], - fields: vec![ - { - #ty - - ty - } - ] - }), - } - } - (_, false) => { - let fields = data - .fields - .iter() - .map(decode_field_attrs) - .collect::>>()? - .iter() - .filter_map(|(field, field_attrs)| { - if field_attrs.skip { - return None; - } - - Some((field, field_attrs)) - }) - .map(|(field, field_attrs)| { - let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); - - let generic_vars = construct_datatype( - format_ident!("gen"), - field_ty, - &generic_idents, - crate_ref, - field_attrs.inline, - )?; - - Ok(quote! {{ - #generic_vars - - gen - }}) - }) - .collect::>>()?; - - quote! { - #crate_ref::NamedDataTypeItem::Tuple( - #crate_ref::TupleType::Named { - generics: vec![#(#definition_generics),*], - fields: vec![#(#fields),*], + + let field_ident_str = unraw_raw_ident(field.ident.as_ref().unwrap()); + + let field_name = match (field_attrs.rename.clone(), container_attrs.rename_all) { + (Some(name), _) => name, + (_, Some(inflection)) => { + let name = inflection.apply(&field_ident_str); + quote::quote!(#name) + }, + (_, _) => quote::quote!(#field_ident_str), + }; + + let optional = field_attrs.optional; + let flatten = field_attrs.flatten; + + let parent_inline = container_attrs + .inline + .then(|| quote!(true)) + .unwrap_or(parent_inline.clone()); + + let ty = if field_attrs.flatten { + quote! { + #[allow(warnings)] + { + #ty } - ) - } - } - }; - - named_data_type_wrapper(crate_ref, container_attrs, name, inner) - } - Fields::Unit => named_data_type_wrapper( - crate_ref, - container_attrs, - name, - quote! { - #crate_ref::NamedDataTypeItem::Tuple(#crate_ref::TupleType::Named { - generics: vec![], - fields: vec![], - }) - }, - ), + + fn validate_flatten() {} + validate_flatten::<#field_ty>(); + + let mut ty = <#field_ty as #crate_ref::Type>::inline(#crate_ref::DefOpts { + parent_inline: #parent_inline, + type_map: opts.type_map + }, &generics); + + ty + } + } else { + quote! { + #ty + + ty + } + }; + + Ok(quote!((#field_name.into(), #crate_ref::internal::construct::field( + #optional, + #flatten, + { + #ty + } + )))) + }).collect::>>()?; + + let tag = container_attrs + .tag + .as_ref() + .map(|t| quote!(Some(#t.into()))) + .unwrap_or(quote!(None)); + + + quote!(#crate_ref::internal::construct::struct_named(vec![#(#fields),*], #tag)) + } + Fields::Unnamed(_) => { + let fields = data + .fields + .iter() + .map(decode_field_attrs) + .collect::>>()? + .iter() + .filter_map(|(field, field_attrs)| { + if field_attrs.skip { + return None; + } + + Some((field, field_attrs)) + }) + .map(|(field, field_attrs)| { + let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty); + + let generic_vars = construct_datatype( + format_ident!("gen"), + field_ty, + &generic_idents, + crate_ref, + field_attrs.inline, + )?; + + let optional = field_attrs.optional; + let flatten = field_attrs.flatten; + Ok(quote!({ + #generic_vars + + #crate_ref::internal::construct::field(#optional, #flatten, gen) + })) + }) + .collect::>>()?; + + + quote!(#crate_ref::internal::construct::struct_unnamed(vec![#(#fields),*])) + } + Fields::Unit => quote!(#crate_ref::internal::construct::struct_unit()), + }; + + quote!(#crate_ref::DataType::Struct(#crate_ref::internal::construct::r#struct(#name.into(), vec![#(#definition_generics),*], #fields))) }; + + let category = if container_attrs.inline { - quote!(#crate_ref::TypeCategory::Inline({ + quote!({ let generics = &[#(#reference_generics),*]; - ::inline(opts, generics)? - })) + #crate_ref::reference::inline::(opts, generics) + }) } else { - quote! { - #crate_ref::TypeCategory::Reference(#crate_ref::internal::construct::data_type_reference( + quote!({ + let generics = vec![#(#reference_generics),*]; + #crate_ref::reference::reference::(opts, &generics, #crate_ref::internal::construct::data_type_reference( #name.into(), SID, - vec![#(#reference_generics),*], + generics.clone() // TODO: This `clone` is cringe )) - } + }) }; - Ok((definition, category, true)) + Ok(( + definition, + category, + true, + )) } diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 4076d4b..f1b7fcd 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -142,7 +142,7 @@ pub fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result> { .expect("Attribute path must be an ident") .clone(); - if !(ident == "specta" || ident == "serde" || ident == "doc") { + if !(ident == "specta" || ident == "serde" || ident == "doc" || ident == "repr") { return Ok(vec![]); } diff --git a/src/datatype/enum.rs b/src/datatype/enum.rs index a0b7d52..cedc147 100644 --- a/src/datatype/enum.rs +++ b/src/datatype/enum.rs @@ -1,69 +1,55 @@ use std::borrow::Cow; -use crate::{ - datatype::{DataType, StructType, TupleType}, - ExportError, GenericType, ImplLocation, -}; +use crate::{datatype::DataType, GenericType, NamedDataType, NamedFields, UnnamedFields}; +/// Enum type which dictates how the enum is represented. +/// +/// The tagging refers to the [Serde concept](https://serde.rs/enum-representations.html). +/// +/// [`Untagged`](EnumType::Untagged) is here rather than in [`EnumRepr`] as it is the only enum representation that does not have tags on its variants. +/// Separating it allows for better typesafety since `variants` doesn't have to be a [`Vec`] of tuples. #[derive(Debug, Clone, PartialEq)] -pub struct UntaggedEnum { - pub(crate) variants: Vec, +pub struct EnumType { + pub(crate) name: Cow<'static, str>, + pub(crate) repr: EnumRepr, pub(crate) generics: Vec, + pub(crate) variants: Vec<(Cow<'static, str>, EnumVariant)>, } -impl UntaggedEnum { - pub fn variants(&self) -> impl Iterator { - self.variants.iter() - } - - pub fn generics(&self) -> impl Iterator { - self.generics.iter() - } -} - -impl Into for UntaggedEnum { - fn into(self) -> EnumType { - EnumType::Untagged(self) +impl EnumType { + /// Convert a [`EnumType`] to an anonymous [`DataType`]. + pub fn to_anonymous(self) -> DataType { + DataType::Enum(self) } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct TaggedEnum { - pub(crate) variants: Vec<(Cow<'static, str>, EnumVariant)>, - pub(crate) generics: Vec, - pub(crate) repr: EnumRepr, -} -impl TaggedEnum { - pub fn variants(&self) -> impl Iterator, EnumVariant)> { - self.variants.iter() + /// Convert a [`EnumType`] to a named [`NamedDataType`]. + /// + /// This can easily be converted to a [`DataType`] by putting it inside the [DataType::Named] variant. + pub fn to_named(self, name: impl Into>) -> NamedDataType { + NamedDataType { + name: name.into(), + comments: vec![], + deprecated: None, + ext: None, + inner: DataType::Enum(self), + } } - pub fn generics(&self) -> impl Iterator { - self.generics.iter() + pub fn name(&self) -> &Cow<'static, str> { + &self.name } pub fn repr(&self) -> &EnumRepr { &self.repr } -} -impl Into for TaggedEnum { - fn into(self) -> EnumType { - EnumType::Tagged(self) + pub fn variants(&self) -> &Vec<(Cow<'static, str>, EnumVariant)> { + &self.variants } -} -/// Enum type which dictates how the enum is represented. -/// -/// The tagging refers to the [Serde concept](https://serde.rs/enum-representations.html). -/// -/// [`Untagged`](EnumType::Untagged) is here rather than in [`EnumRepr`] as it is the only enum representation that does not have tags on its variants. -/// Separating it allows for better typesafety since `variants` doesn't have to be a [`Vec`] of tuples. -#[derive(Debug, Clone, PartialEq)] -pub enum EnumType { - Untagged(UntaggedEnum), - Tagged(TaggedEnum), + pub fn generics(&self) -> &Vec { + &self.generics + } } impl From for DataType { @@ -72,77 +58,10 @@ impl From for DataType { } } -impl EnumType { - pub(crate) fn generics(&self) -> &Vec { - match self { - Self::Untagged(UntaggedEnum { generics, .. }) => generics, - Self::Tagged(TaggedEnum { generics, .. }) => generics, - } - } - - pub(crate) fn variants_len(&self) -> usize { - match self { - Self::Untagged(UntaggedEnum { variants, .. }) => variants.len(), - Self::Tagged(TaggedEnum { variants, .. }) => variants.len(), - } - } - - /// An enum may contain variants which are invalid and will cause a runtime errors during serialize/deserialization. - /// This function will filter them out so types can be exported for valid variants. - pub fn make_flattenable(&mut self, impl_location: ImplLocation) -> Result<(), ExportError> { - match self { - Self::Untagged(UntaggedEnum { variants, .. }) => { - variants.iter().try_for_each(|v| match v { - EnumVariant::Unit => Ok(()), - EnumVariant::Named(_) => Ok(()), - EnumVariant::Unnamed(_) => Err(ExportError::InvalidType( - impl_location, - "`EnumRepr::Untagged` with `EnumVariant::Unnamed` is invalid!", - )), - })?; - } - Self::Tagged(TaggedEnum { variants, repr, .. }) => { - variants.iter().try_for_each(|(_, v)| { - match repr { - EnumRepr::External => match v { - EnumVariant::Unit => Err(ExportError::InvalidType( - impl_location, - "`EnumRepr::External` with ` EnumVariant::Unit` is invalid!", - )), - EnumVariant::Unnamed(v) => match v { - TupleType::Unnamed => Ok(()), - TupleType::Named { fields, .. } if fields.len() == 1 => Ok(()), - TupleType::Named { .. } => Err(ExportError::InvalidType( - impl_location, - "`EnumRepr::External` with `EnumVariant::Unnamed` containing more than a single field is invalid!", - )), - }, - EnumVariant::Named(_) => Ok(()), - }, - EnumRepr::Adjacent { .. } => Ok(()), - EnumRepr::Internal { .. } => match v { - EnumVariant::Unit => Ok(()), - EnumVariant::Named(_) => Ok(()), - EnumVariant::Unnamed(_) => Err(ExportError::InvalidType( - impl_location, - "`EnumRepr::Internal` with `EnumVariant::Unnamed` is invalid!", - )), - }, - } - })?; - } - } - - Ok(()) - } -} - /// Serde representation of an enum. -/// -/// Does not contain [`Untagged`](EnumType::Untagged) as that is handled by [`EnumType`]. #[derive(Debug, Clone, PartialEq)] -#[allow(missing_docs)] pub enum EnumRepr { + Untagged, External, Internal { tag: Cow<'static, str>, @@ -155,20 +74,14 @@ pub enum EnumRepr { /// Type of an [`EnumType`] variant. #[derive(Debug, Clone, PartialEq)] -#[allow(missing_docs)] pub enum EnumVariant { + /// A unit enum variant + /// Eg. `Variant` Unit, - Named(StructType), - Unnamed(TupleType), -} - -impl EnumVariant { - /// Get the [`DataType`](crate::DataType) of the variant. - pub fn data_type(&self) -> DataType { - match self { - Self::Unit => unreachable!("Unit enum variants have no type!"), // TODO: Remove unreachable in type system + avoid following clones - Self::Unnamed(tuple_type) => tuple_type.clone().into(), - Self::Named(object_type) => object_type.clone().into(), - } - } + /// The enum variant contains named fields. + /// Eg. `Variant { a: u32 }` + Named(NamedFields), + /// The enum variant contains unnamed fields. + /// Eg. `Variant(u32, String)` + Unnamed(UnnamedFields), } diff --git a/src/datatype/fields.rs b/src/datatype/fields.rs new file mode 100644 index 0000000..aaa8350 --- /dev/null +++ b/src/datatype/fields.rs @@ -0,0 +1,69 @@ +//! Field types are used by both enums and structs. + +use std::borrow::Cow; + +use crate::DataType; + +#[derive(Debug, Clone, PartialEq)] +pub struct Field { + pub(crate) optional: bool, + pub(crate) flatten: bool, + pub(crate) ty: DataType, +} + +impl Field { + pub fn optional(&self) -> bool { + self.optional + } + + pub fn flatten(&self) -> bool { + self.flatten + } + + pub fn ty(&self) -> &DataType { + &self.ty + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct UnnamedFields { + pub(crate) fields: Vec, +} + +impl UnnamedFields { + /// A list of fields for the current type. + pub fn fields(&self) -> &Vec { + &self.fields + } +} + +/// The fields for a [StructType] or the anonymous struct declaration in an [EnumVariant]. +/// +/// Eg. +/// ```rust +/// // This whole thing is a [StructFields::Named] +/// pub struct Demo { +/// a: String +/// } +/// +/// pub enum Demo2 { +/// A { a: String } // This variant is a [EnumVariant::Named] +/// } +/// ``` +#[derive(Debug, Clone, PartialEq)] +pub struct NamedFields { + pub(crate) fields: Vec<(Cow<'static, str>, Field)>, + pub(crate) tag: Option>, +} + +impl NamedFields { + /// A list of fields in the format (name, [StructField]). + pub fn fields(&self) -> &Vec<(Cow<'static, str>, Field)> { + &self.fields + } + + /// Serde tag for the current field. + pub fn tag(&self) -> &Option> { + &self.tag + } +} diff --git a/src/datatype/literal.rs b/src/datatype/literal.rs index 81150f8..4d45ab4 100644 --- a/src/datatype/literal.rs +++ b/src/datatype/literal.rs @@ -8,7 +8,6 @@ use crate::DataType; /// it's more for library authors. #[derive(Debug, Clone, PartialEq)] #[allow(non_camel_case_types)] -#[allow(missing_docs)] #[non_exhaustive] pub enum LiteralType { i8(i8), @@ -21,6 +20,7 @@ pub enum LiteralType { f64(f64), bool(bool), String(String), + char(char), /// Standalone `null` without a known type None, } diff --git a/src/datatype/mod.rs b/src/datatype/mod.rs index baa946c..c147681 100644 --- a/src/datatype/mod.rs +++ b/src/datatype/mod.rs @@ -4,12 +4,14 @@ use std::{ }; mod r#enum; +mod fields; mod literal; mod named; mod primitive; mod r#struct; mod tuple; +pub use fields::*; pub use literal::*; pub use named::*; pub use primitive::*; @@ -37,7 +39,6 @@ pub struct DefOpts<'a> { /// /// A language exporter takes this general format and converts it into a language specific syntax. #[derive(Debug, Clone, PartialEq)] -#[allow(missing_docs)] pub enum DataType { // Always inlined Any, @@ -47,8 +48,6 @@ pub enum DataType { List(Box), Nullable(Box), Map(Box<(DataType, DataType)>), - // Named reference types - Named(NamedDataType), // Anonymous Reference types Struct(StructType), Enum(EnumType), @@ -60,6 +59,16 @@ pub enum DataType { Generic(GenericType), } +impl DataType { + pub fn generics(&self) -> Option<&Vec> { + match self { + Self::Struct(s) => Some(s.generics()), + Self::Enum(e) => Some(e.generics()), + _ => None, + } + } +} + /// A reference to a [`DataType`] that can be used before a type is resolved in order to /// support recursive types without causing an infinite loop. /// @@ -85,8 +94,8 @@ impl DataTypeReference { self.sid } - pub fn generics(&self) -> impl Iterator { - self.generics.iter() + pub fn generics(&self) -> &Vec { + &self.generics } } @@ -118,21 +127,32 @@ impl From for DataType { impl + 'static> From> for DataType { fn from(t: Vec) -> Self { - DataType::Enum( - UntaggedEnum { - variants: t - .into_iter() - .map(|t| { - EnumVariant::Unnamed(TupleType::Named { - fields: vec![t.into()], - generics: vec![], - }) - }) - .collect(), - generics: vec![], - } - .into(), - ) + DataType::Enum(EnumType { + name: "Vec".into(), + repr: EnumRepr::Untagged, + variants: t + .into_iter() + .map(|t| { + let ty: DataType = t.into(); + ( + match &ty { + DataType::Struct(s) => s.name.clone(), + DataType::Enum(e) => e.name().clone(), + // TODO: This is probs gonna cause problems so we should try and remove the need for this entire impl block if we can. + _ => "".into(), + }, + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty, + }], + }), + ) + }) + .collect(), + generics: vec![], + }) } } diff --git a/src/datatype/named.rs b/src/datatype/named.rs index 7e770d7..a6d19d2 100644 --- a/src/datatype/named.rs +++ b/src/datatype/named.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::{DataType, EnumType, GenericType, ImplLocation, SpectaID, StructType, TupleType}; +use crate::{DataType, ImplLocation, SpectaID}; /// A NamedDataTypeImpl includes extra information which is only available for [NamedDataType]'s that come from a real Rust type. #[derive(Debug, Clone, PartialEq)] @@ -42,8 +42,8 @@ pub struct NamedDataType { /// This will be `None` when constructing [NamedDataType] using `StructType::to_named` or `TupleType::to_named` since those types do not correspond to actual Rust types. pub(crate) ext: Option, /// the actual type definition. - // This field is public because we match on it in flattening code. // TODO: Review if this can be fixed when reviewing the flattening logic/error handling - pub item: NamedDataTypeItem, + // This field is public because we match on it in flattening code. // TODO: Review if this can be made private when reviewing the flattening logic/error handling + pub inner: DataType, } impl NamedDataType { @@ -63,64 +63,3 @@ impl NamedDataType { self.ext.as_ref() } } - -impl From for DataType { - fn from(t: NamedDataType) -> Self { - Self::Named(t) - } -} - -/// The possible types for a [`NamedDataType`]. -/// -/// This type will model the type of the Rust type that is being exported but be aware of the following: -/// ```rust -/// #[derive(serde::Serialize)] -/// struct Demo {} -/// // is: NamedDataTypeItem::Struct -/// // typescript: `{}` -/// -/// #[derive(serde::Serialize)] -/// struct Demo2(); -/// // is: NamedDataTypeItem::Tuple(TupleType::Unnamed) -/// // typescript: `[]` -/// -/// #[derive(specta::Type)] -/// struct Demo3; -///// is: NamedDataTypeItem::Tuple(TupleType::Named(_)) -/// // typescript: `null` -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub enum NamedDataTypeItem { - /// Represents an Rust struct with named fields - Struct(StructType), - /// Represents an Rust enum - Enum(EnumType), - /// Represents an Rust struct with unnamed fields - Tuple(TupleType), -} - -impl NamedDataTypeItem { - /// Converts a [`NamedDataTypeItem`] into a [`DataType`] - pub fn datatype(self) -> DataType { - match self { - Self::Struct(o) => o.into(), - Self::Enum(e) => e.into(), - Self::Tuple(t) => t.into(), - } - } - - /// Returns the generics arguments for the type - pub fn generics(&self) -> Vec { - match self { - // Named struct - Self::Struct(StructType { generics, .. }) => generics.clone(), - // Enum - Self::Enum(e) => e.generics().clone(), - // Struct with unnamed fields - Self::Tuple(tuple) => match tuple { - TupleType::Unnamed => vec![], - TupleType::Named { generics, .. } => generics.clone(), - }, - } - } -} diff --git a/src/datatype/struct.rs b/src/datatype/struct.rs index 329aa69..0a36e43 100644 --- a/src/datatype/struct.rs +++ b/src/datatype/struct.rs @@ -1,41 +1,28 @@ use std::borrow::Cow; -use crate::{DataType, GenericType, NamedDataType, NamedDataTypeItem}; +use crate::{DataType, GenericType, NamedDataType, NamedFields, UnnamedFields}; -/// A field in an [`StructType`]. #[derive(Debug, Clone, PartialEq)] -pub struct StructField { - pub(crate) key: Cow<'static, str>, - pub(crate) optional: bool, - pub(crate) flatten: bool, - pub(crate) ty: DataType, -} - -impl StructField { - pub fn key(&self) -> &Cow<'static, str> { - &self.key - } - - pub fn optional(&self) -> bool { - self.optional - } - - pub fn flatten(&self) -> bool { - self.flatten - } - - pub fn ty(&self) -> &DataType { - &self.ty - } +pub enum StructFields { + /// A unit struct. + /// + /// Represented in Rust as `pub struct Unit;` and in TypeScript as `null`. + Unit, + /// A struct with unnamed fields. + /// + /// Represented in Rust as `pub struct Unit();` and in TypeScript as `[]`. + Unnamed(UnnamedFields), + /// A struct with named fields. + /// + /// Represented in Rust as `pub struct Unit {}` and in TypeScript as `{}`. + Named(NamedFields), } -/// Type of a struct. -/// Could be from a struct or named enum variant. #[derive(Debug, Clone, PartialEq)] pub struct StructType { + pub(crate) name: Cow<'static, str>, pub(crate) generics: Vec, - pub(crate) fields: Vec, - pub(crate) tag: Option>, + pub(crate) fields: StructFields, } impl StructType { @@ -53,20 +40,28 @@ impl StructType { comments: vec![], deprecated: None, ext: None, - item: NamedDataTypeItem::Struct(self), + inner: DataType::Struct(self), } } - pub fn generics(&self) -> impl Iterator { - self.generics.iter() + pub fn name(&self) -> &Cow<'static, str> { + &self.name } - pub fn fields(&self) -> impl Iterator { - self.fields.iter() + pub fn generics(&self) -> &Vec { + &self.generics + } + + pub fn fields(&self) -> &StructFields { + &self.fields } pub fn tag(&self) -> Option<&Cow<'static, str>> { - self.tag.as_ref() + match &self.fields { + StructFields::Unit => None, + StructFields::Unnamed(_) => None, + StructFields::Named(named) => named.tag.as_ref(), + } } } diff --git a/src/datatype/tuple.rs b/src/datatype/tuple.rs index fcc364e..5326bf7 100644 --- a/src/datatype/tuple.rs +++ b/src/datatype/tuple.rs @@ -1,23 +1,14 @@ use std::borrow::Cow; -use crate::{DataType, GenericType, NamedDataType, NamedDataTypeItem}; +use crate::{DataType, NamedDataType}; -/// Type of a tuple. -/// Could be from an actual tuple or unnamed struct. +/// A regular tuple +/// +/// Represented in Rust as `(...)` and in TypeScript as `[...]`. +/// Be aware `()` is treated specially as `null` in Typescript. #[derive(Debug, Clone, PartialEq)] -pub enum TupleType { - /// An unnamed tuple. - /// - /// Represented in Rust as `pub struct Unit();` and in TypeScript as `[]`. - Unnamed, - /// A regular tuple - /// - /// Represented in Rust as `(...)` and in TypeScript as `[...]`. - /// Be aware `()` is treated specially as `null` in Typescript. - Named { - fields: Vec, - generics: Vec, - }, +pub struct TupleType { + pub(crate) fields: Vec, } impl TupleType { @@ -35,9 +26,13 @@ impl TupleType { comments: vec![], deprecated: None, ext: None, - item: NamedDataTypeItem::Tuple(self), + inner: DataType::Tuple(self), } } + + pub fn fields(&self) -> &Vec { + &self.fields + } } impl From for DataType { diff --git a/src/export/export.rs b/src/export/export.rs index 09ae5da..3f9ffc7 100644 --- a/src/export/export.rs +++ b/src/export/export.rs @@ -1,13 +1,9 @@ -use crate::ts::TsExportError; use crate::*; use once_cell::sync::Lazy; -use std::collections::BTreeSet; use std::sync::{PoisonError, RwLock, RwLockReadGuard}; -type InnerTy = (TypeMap, BTreeSet); - // Global type store for collecting custom types to export. -static TYPES: Lazy> = Lazy::new(Default::default); +static TYPES: Lazy> = Lazy::new(Default::default); /// A lock type for iterating over the internal type map. /// @@ -15,14 +11,14 @@ static TYPES: Lazy> = Lazy::new(Default::default); /// pub struct TypesIter { index: usize, - lock: RwLockReadGuard<'static, InnerTy>, + lock: RwLockReadGuard<'static, TypeMap>, } impl Iterator for TypesIter { type Item = (SpectaID, Option); fn next(&mut self) -> Option { - let (k, v) = self.lock.0.iter().nth(self.index)?; + let (k, v) = self.lock.iter().nth(self.index)?; self.index += 1; // We have to clone, because we can't invent a lifetime Some((*k, v.clone())) @@ -30,32 +26,26 @@ impl Iterator for TypesIter { } /// Get the global type store for collecting custom types to export. -pub fn get_types() -> Result { +pub fn get_types() -> TypesIter { let types = TYPES.read().unwrap_or_else(PoisonError::into_inner); - // TODO: Return all errors at once? - if let Some(err) = types.1.iter().next() { - return Err(err.clone().into()); - } - - Ok(TypesIter { + TypesIter { index: 0, lock: types, - }) + } } // Called within ctor functions to register a type. #[doc(hidden)] -pub fn register_ty() -> () { - let (type_map, errors) = &mut *TYPES.write().unwrap_or_else(PoisonError::into_inner); +pub fn register_ty() { + let type_map = &mut *TYPES.write().unwrap_or_else(PoisonError::into_inner); - if let Err(err) = T::reference( + // We call this for it's side effects on the `type_map` + T::reference( DefOpts { parent_inline: false, type_map, }, &[], - ) { - errors.insert(err); - } + ); } diff --git a/src/export/ts.rs b/src/export/ts.rs index 8e04186..663048a 100644 --- a/src/export/ts.rs +++ b/src/export/ts.rs @@ -16,8 +16,7 @@ pub fn ts_with_cfg(path: &str, conf: &ExportConfig) -> Result<(), TsExportError> let export_by_default = conf.export_by_default.unwrap_or(true); // We sort by name to detect duplicate types BUT also to ensure the output is deterministic. The SID can change between builds so is not suitable for this. - let types = get_types()? - .into_iter() + let types = get_types() .filter(|(_, v)| match v { Some(v) => v .ext() diff --git a/src/functions/arg.rs b/src/functions/arg.rs index 505ce48..c88989e 100644 --- a/src/functions/arg.rs +++ b/src/functions/arg.rs @@ -1,5 +1,5 @@ mod private { - use crate::{DataType, DefOpts, ExportError, Type}; + use crate::{DataType, DefOpts, Type}; /// Implemented by types that can be used as an argument in a function annotated with /// [`specta`](crate::specta). @@ -8,14 +8,14 @@ mod private { /// /// Some argument types should be ignored (eg Tauri command State), /// so the value is optional. - fn to_datatype(opts: DefOpts) -> Result, ExportError>; + fn to_datatype(opts: DefOpts) -> Option; } pub enum FunctionArgMarker {} impl SpectaFunctionArg for T { - fn to_datatype(opts: DefOpts) -> Result, ExportError> { - T::reference(opts, &[]).map(Some) + fn to_datatype(opts: DefOpts) -> Option { + Some(T::reference(opts, &[]).inner) } } @@ -24,22 +24,22 @@ mod private { pub enum FunctionArgTauriMarker {} impl SpectaFunctionArg for tauri::Window { - fn to_datatype(_: DefOpts) -> Result, ExportError> { - Ok(None) + fn to_datatype(_: DefOpts) -> Option { + None } } impl<'r, T: Send + Sync + 'static> SpectaFunctionArg for tauri::State<'r, T> { - fn to_datatype(_: DefOpts) -> Result, ExportError> { - Ok(None) + fn to_datatype(_: DefOpts) -> Option { + None } } impl SpectaFunctionArg for tauri::AppHandle { - fn to_datatype(_: DefOpts) -> Result, ExportError> { - Ok(None) + fn to_datatype(_: DefOpts) -> Option { + None } } }; diff --git a/src/functions/mod.rs b/src/functions/mod.rs index fca2d45..9b8b435 100644 --- a/src/functions/mod.rs +++ b/src/functions/mod.rs @@ -22,7 +22,7 @@ use crate::*; /// } /// /// fn main() { -/// let typ = fn_datatype!(some_function).unwrap(); +/// let typ = fn_datatype!(some_function); /// /// assert_eq!(typ.name, "some_function"); /// assert_eq!(typ.args.len(), 2); @@ -68,7 +68,7 @@ pub trait SpectaFunction { type_map: &mut TypeMap, fields: &[Cow<'static, str>], docs: Vec>, - ) -> Result; + ) -> FunctionDataType; } impl> SpectaFunction @@ -80,18 +80,17 @@ impl> SpectaFunction type_map: &mut TypeMap, _fields: &[Cow<'static, str>], docs: Vec>, - ) -> Result { - TResult::to_datatype(DefOpts { - parent_inline: false, - type_map, - }) - .map(|result| FunctionDataType { + ) -> FunctionDataType { + FunctionDataType { asyncness, name, args: vec![], - result, + result: TResult::to_datatype(DefOpts { + parent_inline: false, + type_map, + }), docs, - }) + } } } @@ -105,7 +104,7 @@ pub fn get_datatype_internal>( type_map: &mut TypeMap, fields: &[Cow<'static, str>], docs: Vec>, -) -> Result { +) -> FunctionDataType { T::to_datatype(asyncness, name, type_map, fields, docs) } @@ -124,10 +123,10 @@ macro_rules! impl_typed_command { type_map: &mut TypeMap, fields: &[Cow<'static, str>], docs: Vec>, - ) -> Result { + ) -> FunctionDataType { let mut fields = fields.into_iter(); - Ok(FunctionDataType { + FunctionDataType { asyncness, name, docs, @@ -135,21 +134,21 @@ macro_rules! impl_typed_command { fields .next() .map_or_else( - || Ok(None), + || None, |field| $i::to_datatype(DefOpts { parent_inline: false, type_map, - }).map(|v| v.map(|ty| (field.clone(), ty))) - )? + }).map(|ty| (field.clone(), ty)) + ) ),*,] - .into_iter() - .filter_map(|v| v) - .collect(), + .into_iter() + .filter_map(|v| v) + .collect::>(), result: TResult::to_datatype(DefOpts { parent_inline: false, type_map, - })?, - }) + }), + } } } } @@ -180,7 +179,7 @@ impl_typed_command!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); /// /// fn main() { /// // `type_defs` is created internally -/// let (functions, type_defs) = functions::collect_functions![some_function].unwrap(); +/// let (functions, type_defs) = functions::collect_functions![some_function]; /// /// let custom_type_defs = TypeMap::default(); /// @@ -189,18 +188,16 @@ impl_typed_command!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); /// let (functions, custom_type_defs) = functions::collect_functions![ /// custom_type_defs; // You can provide a custom map to collect the types into /// some_function -/// ].unwrap(); +/// ]; /// } /// ```` #[macro_export] macro_rules! collect_functions { ($type_map:ident; $($command:path),* $(,)?) => {{ let mut type_map: $crate::TypeMap = $type_map; - - [$($crate::fn_datatype!(type_map; $command)),*] - .into_iter() - .collect::<::std::result::Result, _>>() - .map(|v| (v, type_map)) + ([$($crate::fn_datatype!(type_map; $command)),*] + .into_iter() + .collect::>(), type_map) }}; ($($command:path),* $(,)?) => {{ let mut type_map = $crate::TypeMap::default(); diff --git a/src/functions/result.rs b/src/functions/result.rs index 5072b73..281d189 100644 --- a/src/functions/result.rs +++ b/src/functions/result.rs @@ -1,19 +1,19 @@ mod private { use std::future::Future; - use crate::{DataType, DefOpts, ExportError, Type}; + use crate::{DataType, DefOpts, Type}; /// Implemented by types that can be returned from a function annotated with /// [`specta`](crate::specta). pub trait SpectaFunctionResult { /// Gets the type of the result as a [`DataType`]. - fn to_datatype(opts: DefOpts) -> Result; + fn to_datatype(opts: DefOpts) -> DataType; } pub enum SpectaFunctionResultMarker {} impl SpectaFunctionResult for T { - fn to_datatype(opts: DefOpts) -> Result { - T::reference(opts, &[]) + fn to_datatype(opts: DefOpts) -> DataType { + T::reference(opts, &[]).inner } } @@ -23,8 +23,8 @@ mod private { F: Future, F::Output: Type, { - fn to_datatype(opts: DefOpts) -> Result { - F::Output::reference(opts, &[]) + fn to_datatype(opts: DefOpts) -> DataType { + F::Output::reference(opts, &[]).inner } } } diff --git a/src/internal.rs b/src/internal.rs index 54e9ccf..571e85e 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -12,37 +12,76 @@ pub use specta_macros::fn_datatype; /// Functions used to construct `crate::datatype` types (they have private fields so can't be constructed directly). /// We intentionally keep their fields private so we can modify them without a major version bump. +/// As this module is `#[doc(hidden)]` we allowed to make breaking changes within a minor version as it's only used by the macros. pub mod construct { use std::borrow::Cow; use crate::{datatype::*, ImplLocation, SpectaID}; + pub const fn field(optional: bool, flatten: bool, ty: DataType) -> Field { + Field { + optional, + flatten, + ty, + } + } + pub const fn r#struct( + name: Cow<'static, str>, generics: Vec, - fields: Vec, - tag: Option>, + fields: StructFields, ) -> StructType { StructType { + name, generics, fields, - tag, } } - pub const fn struct_field( - key: Cow<'static, str>, - optional: bool, - flatten: bool, - ty: DataType, - ) -> StructField { - StructField { - key, - optional, - flatten, - ty, + pub const fn struct_unit() -> StructFields { + StructFields::Unit + } + + pub const fn struct_unnamed(fields: Vec) -> StructFields { + StructFields::Unnamed(UnnamedFields { fields }) + } + + pub const fn struct_named( + fields: Vec<(Cow<'static, str>, Field)>, + tag: Option>, + ) -> StructFields { + StructFields::Named(NamedFields { fields, tag }) + } + + pub const fn r#enum( + name: Cow<'static, str>, + repr: EnumRepr, + generics: Vec, + variants: Vec<(Cow<'static, str>, EnumVariant)>, + ) -> EnumType { + EnumType { + name, + repr, + generics, + variants, } } + pub const fn enum_variant_unit() -> EnumVariant { + EnumVariant::Unit + } + + pub const fn enum_variant_unnamed(fields: Vec) -> EnumVariant { + EnumVariant::Unnamed(UnnamedFields { fields }) + } + + pub const fn enum_variant_named( + fields: Vec<(Cow<'static, str>, Field)>, + tag: Option>, + ) -> EnumVariant { + EnumVariant::Named(NamedFields { fields, tag }) + } + pub const fn named_data_type( name: Cow<'static, str>, comments: Vec>, @@ -50,7 +89,7 @@ pub mod construct { sid: SpectaID, impl_location: ImplLocation, export: Option, - item: NamedDataTypeItem, + inner: DataType, ) -> NamedDataType { NamedDataType { name, @@ -61,7 +100,7 @@ pub mod construct { impl_location, export, }), - item, + inner, } } @@ -77,19 +116,7 @@ pub mod construct { } } - pub const fn untagged_enum(variants: Vec, generics: Vec) -> EnumType { - EnumType::Untagged(UntaggedEnum { variants, generics }) - } - - pub const fn tagged_enum( - variants: Vec<(Cow<'static, str>, EnumVariant)>, - generics: Vec, - repr: EnumRepr, - ) -> EnumType { - EnumType::Tagged(TaggedEnum { - variants, - generics, - repr, - }) + pub const fn tuple(fields: Vec) -> TupleType { + TupleType { fields } } } diff --git a/src/lang/ts/error.rs b/src/lang/ts/error.rs index 935dae4..cca3f42 100644 --- a/src/lang/ts/error.rs +++ b/src/lang/ts/error.rs @@ -3,13 +3,12 @@ use std::borrow::Cow; use thiserror::Error; -use crate::{ExportError, ImplLocation}; +use crate::{ImplLocation, SerdeError}; use super::ExportPath; /// Describes where an error occurred. #[derive(Error, Debug, PartialEq)] -#[allow(missing_docs)] pub enum NamedLocation { Type, Field, @@ -32,27 +31,29 @@ impl fmt::Display for NamedLocation { pub enum TsExportError { #[error("Attempted to export '{0}' but Specta configuration forbids exporting BigInt types (i64, u64, i128, u128) because we don't know if your se/deserializer supports it. You can change this behavior by editing your `ExportConfiguration`!")] BigIntForbidden(ExportPath), - #[error("Attempted to export '{0}' but was unable to export a tagged type which is unnamed")] - UnableToTagUnnamedType(ExportPath), + #[error("Serde error: {0}")] + Serde(#[from] SerdeError), + // #[error("Attempted to export '{0}' but was unable to export a tagged type which is unnamed")] + // UnableToTagUnnamedType(ExportPath), #[error("Attempted to export '{1}' but was unable to due to {0} name '{2}' conflicting with a reserved keyword in Typescript. Try renaming it or using `#[specta(rename = \"new name\")]`")] ForbiddenName(NamedLocation, ExportPath, &'static str), #[error("Attempted to export '{0}' with tagging but the type is not tagged.")] InvalidTagging(ExportPath), #[error("Unable to export type named '{0}' from locations '{:?}' '{:?}'", .1.as_str(), .2.as_str())] DuplicateTypeName(Cow<'static, str>, ImplLocation, ImplLocation), - #[error("{0}")] - SpectaExportError(#[from] ExportError), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Failed to export '{0}' due to error: {1}")] Other(ExportPath, String), } +// TODO: This `impl` is cringe impl PartialEq for TsExportError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::BigIntForbidden(l0), Self::BigIntForbidden(r0)) => l0 == r0, - (Self::UnableToTagUnnamedType(l0), Self::UnableToTagUnnamedType(r0)) => l0 == r0, + (Self::Serde(l0), Self::Serde(r0)) => l0 == r0, + // (Self::UnableToTagUnnamedType(l0), Self::UnableToTagUnnamedType(r0)) => l0 == r0, (Self::ForbiddenName(l0, l1, l2), Self::ForbiddenName(r0, r1, r2)) => { l0 == r0 && l1 == r1 && l2 == r2 } diff --git a/src/lang/ts/export_config.rs b/src/lang/ts/export_config.rs index feec0fc..e10775d 100644 --- a/src/lang/ts/export_config.rs +++ b/src/lang/ts/export_config.rs @@ -39,8 +39,12 @@ impl ExportConfig { /// /// Implementations: /// - [`js_doc`](crate::lang::ts::js_doc) - pub fn comment_style(mut self, exporter: CommentFormatterFn) -> Self { - self.comment_exporter = Some(exporter); + /// + /// Not calling this method will default to the [`js_doc`](crate::lang::ts::js_doc) exporter. + /// `None` will disable comment exporting. + /// `Some(exporter)` will enable comment exporting using the provided exporter. + pub fn comment_style(mut self, exporter: Option) -> Self { + self.comment_exporter = exporter; self } diff --git a/src/lang/ts/mod.rs b/src/lang/ts/mod.rs index 26e2094..223d2ce 100644 --- a/src/lang/ts/mod.rs +++ b/src/lang/ts/mod.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + mod comments; mod context; mod error; @@ -5,8 +7,6 @@ mod export_config; mod formatter; mod reserved_terms; -use std::borrow::Cow; - pub use comments::*; pub use context::*; pub use error::*; @@ -18,6 +18,7 @@ use crate::*; #[allow(missing_docs)] pub type Result = std::result::Result; + type Output = Result; /// Convert a type which implements [`Type`](crate::Type) to a TypeScript string with an export. @@ -35,7 +36,8 @@ pub fn export(conf: &ExportConfig) -> Output { let named_data_type = T::definition_named_data_type(DefOpts { parent_inline: false, type_map: &mut type_map, - })?; + }); + is_valid_ty(&named_data_type.inner, &type_map)?; let result = export_named_datatype(conf, &named_data_type, &type_map); if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() { @@ -57,17 +59,15 @@ pub fn inline_ref(_: &T, conf: &ExportConfig) -> Output { /// Eg. `{ demo: string; };` pub fn inline(conf: &ExportConfig) -> Output { let mut type_map = TypeMap::default(); - let result = datatype( - conf, - &T::inline( - DefOpts { - parent_inline: false, - type_map: &mut type_map, - }, - &[], - )?, - &type_map, + let ty = T::inline( + DefOpts { + parent_inline: false, + type_map: &mut type_map, + }, + &[], ); + is_valid_ty(&ty, &type_map)?; + let result = datatype(conf, &ty, &type_map); if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() { return Err(TsExportError::DuplicateTypeName(ty_name, l0, l1)); @@ -86,6 +86,7 @@ pub fn export_named_datatype( ) -> Output { // TODO: Duplicate type name detection? + is_valid_ty(&typ.inner, type_map)?; export_datatype_inner(ExportContext { conf, path: vec![] }, typ, type_map) } @@ -94,7 +95,7 @@ fn export_datatype_inner( typ @ NamedDataType { name, comments, - item, + inner: item, .. }: &NamedDataType, type_map: &TypeMap, @@ -102,9 +103,10 @@ fn export_datatype_inner( let ctx = ctx.with(PathItem::Type(name.clone())); let name = sanitise_type_name(ctx.clone(), NamedLocation::Type, name)?; - let inline_ts = named_datatype_inner(ctx.clone(), typ, type_map)?; + let inline_ts = datatype_inner(ctx.clone(), &typ.inner, type_map, "null")?; - let generics = Some(item.generics()) + let generics = item + .generics() .filter(|generics| !generics.is_empty()) .map(|generics| format!("<{}>", generics.join(", "))) .unwrap_or_default(); @@ -120,40 +122,21 @@ fn export_datatype_inner( )) } -/// Convert a NamedDataType to a TypeScript string -/// -/// Eg. `{ scalar_field: number, generc_field: T }` -pub fn named_datatype(conf: &ExportConfig, typ: &NamedDataType, type_map: &TypeMap) -> Output { - named_datatype_inner( - ExportContext { - conf, - path: vec![PathItem::Type(typ.name.clone())], - }, - typ, - type_map, - ) -} - -fn named_datatype_inner(ctx: ExportContext, typ: &NamedDataType, type_map: &TypeMap) -> Output { - let name = Some(&typ.name); - - match &typ.item { - NamedDataTypeItem::Struct(o) => object_datatype(ctx, name, o, type_map), - NamedDataTypeItem::Enum(e) => enum_datatype(ctx, name, e, type_map), - NamedDataTypeItem::Tuple(t) => tuple_datatype(ctx, t, type_map), - } -} - /// Convert a DataType to a TypeScript string /// /// Eg. `{ demo: string; }` pub fn datatype(conf: &ExportConfig, typ: &DataType, type_map: &TypeMap) -> Output { // TODO: Duplicate type name detection? - datatype_inner(ExportContext { conf, path: vec![] }, typ, type_map) + datatype_inner(ExportContext { conf, path: vec![] }, typ, type_map, "null") } -fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &TypeMap) -> Output { +fn datatype_inner( + ctx: ExportContext, + typ: &DataType, + type_map: &TypeMap, + empty_tuple_fallback: &'static str, +) -> Output { Ok(match &typ { DataType::Any => "any".into(), DataType::Primitive(p) => { @@ -177,7 +160,7 @@ fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &TypeMap) -> Out } DataType::Literal(literal) => literal.to_ts(), DataType::Nullable(def) => { - let dt = datatype_inner(ctx, def, type_map)?; + let dt = datatype_inner(ctx, def, type_map, "null")?; if dt.ends_with(" | null") { dt @@ -186,49 +169,38 @@ fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &TypeMap) -> Out } } DataType::Map(def) => { - let is_enum = match &def.0 { - DataType::Enum(_) => true, - DataType::Named(dt) => matches!(dt.item, NamedDataTypeItem::Enum(_)), - DataType::Reference(r) => { - let typ = type_map - .get(&r.sid()) - .unwrap_or_else(|| panic!("Type {} not found!", r.name())) - .as_ref() - .unwrap_or_else(|| panic!("Type {} has no value!", r.name())); - - matches!(typ.item, NamedDataTypeItem::Enum(_)) - } - _ => false, - }; - - let divider = if is_enum { " in" } else { ":" }; - format!( // We use this isn't of `Record` to avoid issues with circular references. - "{{ [key{divider} {}]: {} }}", - datatype_inner(ctx.clone(), &def.0, type_map)?, - datatype_inner(ctx, &def.1, type_map)? + "{{ [key in {}]: {} }}", + datatype_inner(ctx.clone(), &def.0, type_map, "null")?, + datatype_inner(ctx, &def.1, type_map, "null")? ) } // We use `T[]` instead of `Array` to avoid issues with circular references. DataType::List(def) => { - let dt = datatype_inner(ctx, def, type_map)?; + let dt = datatype_inner(ctx, def, type_map, "null")?; if dt.contains(' ') && !dt.ends_with('}') { format!("({dt})[]") } else { format!("{dt}[]") } } - DataType::Struct(item) => object_datatype(ctx, None, item, type_map)?, - DataType::Enum(item) => enum_datatype(ctx, None, item, type_map)?, - DataType::Tuple(tuple) => tuple_datatype(ctx, tuple, type_map)?, - DataType::Named(typ) => { - named_datatype_inner(ctx.with(PathItem::Type(typ.name.clone())), typ, type_map)? - } + DataType::Struct(item) => struct_datatype( + ctx.with(PathItem::Type(item.name().clone())), + item.name(), + item, + type_map, + )?, + DataType::Enum(item) => enum_datatype( + ctx.with(PathItem::Variant(item.name.clone())), + item, + type_map, + )?, + DataType::Tuple(tuple) => tuple_datatype(ctx, tuple, type_map, empty_tuple_fallback)?, DataType::Result(result) => { let mut variants = vec![ - datatype_inner(ctx.clone(), &result.0, type_map)?, - datatype_inner(ctx, &result.1, type_map)?, + datatype_inner(ctx.clone(), &result.0, type_map, "null")?, + datatype_inner(ctx, &result.1, type_map, "null")?, ]; variants.dedup(); variants.join(" | ") @@ -238,7 +210,14 @@ fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &TypeMap) -> Out generics => { let generics = generics .iter() - .map(|v| datatype_inner(ctx.with(PathItem::Type(name.clone())), v, type_map)) + .map(|v| { + datatype_inner( + ctx.with(PathItem::Type(name.clone())), + v, + type_map, + empty_tuple_fallback, + ) + }) .collect::>>()? .join(", "); @@ -249,56 +228,85 @@ fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &TypeMap) -> Out }) } -fn tuple_datatype(ctx: ExportContext, tuple: &TupleType, type_map: &TypeMap) -> Output { - match tuple { - TupleType::Unnamed => Ok("[]".to_string()), - TupleType::Named { fields, .. } => match &fields[..] { - [] => Ok("null".to_string()), - [ty] => datatype_inner(ctx, ty, type_map), - tys => Ok(format!( - "[{}]", - tys.iter() - .map(|v| datatype_inner(ctx.clone(), v, type_map)) - .collect::>>()? - .join(", ") - )), - }, +// Can be used with `StructUnnamedFields.fields` or `EnumNamedFields.fields` +fn unnamed_fields_datatype( + ctx: ExportContext, + fields: &[Field], + type_map: &TypeMap, + empty_tuple_fallback: &'static str, +) -> Output { + match fields { + [] => Ok(empty_tuple_fallback.to_string()), + [field] => datatype_inner(ctx, &field.ty, type_map, "null"), + fields => Ok(format!( + "[{}]", + fields + .iter() + .map(|field| datatype_inner(ctx.clone(), &field.ty, type_map, "null")) + .collect::>>()? + .join(", ") + )), } } -fn object_datatype( +fn tuple_datatype( ctx: ExportContext, - name: Option<&Cow<'static, str>>, - StructType { fields, tag, .. }: &StructType, + tuple: &TupleType, type_map: &TypeMap, + empty_tuple_fallback: &'static str, ) -> Output { - match &fields[..] { - [] => Ok("Record".to_string()), - fields => { - let mut field_sections = fields - .iter() - .filter(|f| f.flatten) - .map(|field| { + match &tuple.fields[..] { + [] => Ok(empty_tuple_fallback.to_string()), + [ty] => datatype_inner(ctx, ty, type_map, "null"), + tys => Ok(format!( + "[{}]", + tys.iter() + .map(|v| datatype_inner(ctx.clone(), v, type_map, "null")) + .collect::>>()? + .join(", ") + )), + } +} + +fn struct_datatype(ctx: ExportContext, key: &str, s: &StructType, type_map: &TypeMap) -> Output { + match &s.fields { + StructFields::Unit => Ok("null".into()), + StructFields::Unnamed(s) => unnamed_fields_datatype(ctx, &s.fields, type_map, "[]"), + StructFields::Named(s) => { + if s.fields.is_empty() { + return Ok("Record".into()); + } + + let (flattened, non_flattened): (Vec<_>, Vec<_>) = + s.fields.iter().partition(|(_, f)| f.flatten); + + let mut field_sections = flattened + .into_iter() + .map(|(key, field)| { datatype_inner( - ctx.with(PathItem::Field(field.key.clone())), + ctx.with(PathItem::Field(key.clone())), &field.ty, type_map, + "[]", ) .map(|type_str| format!("({type_str})")) }) .collect::>>()?; - let mut unflattened_fields = fields - .iter() - .filter(|f| !f.flatten) - .map(|f| object_field_to_ts(ctx.with(PathItem::Field(f.key.clone())), f, type_map)) + let mut unflattened_fields = non_flattened + .into_iter() + .map(|(key, f)| { + object_field_to_ts( + ctx.with(PathItem::Field(key.clone())), + key.clone(), + f, + type_map, + ) + }) .collect::>>()?; - if let Some(tag) = tag { - unflattened_fields.push(format!( - "{tag}: \"{}\"", - name.ok_or_else(|| TsExportError::UnableToTagUnnamedType(ctx.export_path()))? - )); + if let Some(tag) = &s.tag { + unflattened_fields.push(format!("{tag}: \"{key}\"")); } if !unflattened_fields.is_empty() { @@ -310,42 +318,126 @@ fn object_datatype( } } -fn enum_datatype( +fn enum_variant_datatype( ctx: ExportContext, - _ty_name: Option<&Cow<'static, str>>, - e: &EnumType, type_map: &TypeMap, + name: Cow<'static, str>, + variant: &EnumVariant, ) -> Output { - if e.variants_len() == 0 { + match variant { + // TODO: Remove unreachable in type system + EnumVariant::Unit => unreachable!("Unit enum variants have no type!"), + EnumVariant::Named(obj) => { + let mut fields = if let Some(tag) = &obj.tag { + let sanitised_name = sanitise_key(name, true); + vec![format!("{tag}: {sanitised_name}")] + } else { + vec![] + }; + + fields.extend( + obj.fields + .iter() + .map(|(name, field)| { + object_field_to_ts( + ctx.with(PathItem::Field(name.clone())), + name.clone(), + field, + type_map, + ) + }) + .collect::>>()?, + ); + + Ok(match &fields[..] { + [] => "Record".to_string(), + fields => format!("{{ {} }}", fields.join("; ")), + }) + } + EnumVariant::Unnamed(obj) => { + let fields = obj + .fields + .iter() + .map(|field| datatype_inner(ctx.clone(), &field.ty, type_map, "[]")) + .collect::>>()?; + + Ok(match &fields[..] { + [] => "[]".to_string(), + [field] => field.to_string(), + fields => format!("[{}]", fields.join(", ")), + }) + } + } +} + +fn enum_datatype(ctx: ExportContext, e: &EnumType, type_map: &TypeMap) -> Output { + if e.variants().is_empty() { return Ok("never".to_string()); } - Ok(match e { - EnumType::Tagged(TaggedEnum { variants, repr, .. }) => { - let mut variants = variants + Ok(match &e.repr { + EnumRepr::Untagged => { + let mut variants = e + .variants + .iter() + .map(|(name, variant)| { + Ok(match variant { + EnumVariant::Unit => "null".to_string(), + v => enum_variant_datatype( + ctx.with(PathItem::Variant(name.clone())), + type_map, + name.clone(), + v, + )?, + }) + }) + .collect::>>()?; + variants.dedup(); + variants.join(" | ") + } + repr => { + let mut variants = e + .variants .iter() .map(|(variant_name, variant)| { - let ctx = ctx.with(PathItem::Variant(variant_name.clone())); - let sanitised_name = sanitise_key(variant_name, true); + let sanitised_name = sanitise_key(variant_name.clone(), true); Ok(match (repr, variant) { + (EnumRepr::Untagged, _) => unreachable!(), (EnumRepr::Internal { tag }, EnumVariant::Unit) => { format!("{{ {tag}: {sanitised_name} }}") } (EnumRepr::Internal { tag }, EnumVariant::Unnamed(tuple)) => { - let typ = - datatype_inner(ctx, &DataType::Tuple(tuple.clone()), type_map)?; - format!("({{ {tag}: {sanitised_name} }} & {typ})") + let mut typ = unnamed_fields_datatype( + ctx.clone(), + &tuple.fields, + type_map, + "[]", + )?; + + // TODO: This `null` check is a bad fix for an internally tagged type with a `null` variant being exported as `{ type: "A" } & null` (which is `never` in TS) + // TODO: Move this check into the macros so it can apply to any language cause it should (it's just hard to do in the macros) + if typ == "null" { + format!("({{ {tag}: {sanitised_name} }})") + } else { + // We wanna be sure `... & ... | ...` becomes `... & (... | ...)` + if typ.contains('|') { + typ = format!("({typ})"); + } + format!("({{ {tag}: {sanitised_name} }} & {typ})") + } } (EnumRepr::Internal { tag }, EnumVariant::Named(obj)) => { let mut fields = vec![format!("{tag}: {sanitised_name}")]; fields.extend( - obj.fields() - .map(|v| { + obj.fields + .iter() + .map(|(name, field)| { object_field_to_ts( - ctx.with(PathItem::Field(v.key.clone())), - v, + ctx.with(PathItem::Field(name.clone())), + name.clone(), + field, type_map, ) }) @@ -357,8 +449,13 @@ fn enum_datatype( (EnumRepr::External, EnumVariant::Unit) => sanitised_name.to_string(), (EnumRepr::External, v) => { - let ts_values = datatype_inner(ctx.clone(), &v.data_type(), type_map)?; - let sanitised_name = sanitise_key(variant_name, false); + let ts_values = enum_variant_datatype( + ctx.with(PathItem::Variant(variant_name.clone())), + type_map, + variant_name.clone(), + v, + )?; + let sanitised_name = sanitise_key(variant_name.clone(), false); format!("{{ {sanitised_name}: {ts_values} }}") } @@ -366,7 +463,12 @@ fn enum_datatype( format!("{{ {tag}: {sanitised_name} }}") } (EnumRepr::Adjacent { tag, content }, v) => { - let ts_values = datatype_inner(ctx, &v.data_type(), type_map)?; + let ts_values = enum_variant_datatype( + ctx.with(PathItem::Variant(variant_name.clone())), + type_map, + variant_name.clone(), + v, + )?; format!("{{ {tag}: {sanitised_name}; {content}: {ts_values} }}") } @@ -376,19 +478,6 @@ fn enum_datatype( variants.dedup(); variants.join(" | ") } - EnumType::Untagged(UntaggedEnum { variants, .. }) => { - let mut variants = variants - .iter() - .map(|variant| { - Ok(match variant { - EnumVariant::Unit => "null".to_string(), - v => datatype_inner(ctx.clone(), &v.data_type(), type_map)?, - }) - }) - .collect::>>()?; - variants.dedup(); - variants.join(" | ") - } }) } @@ -405,26 +494,35 @@ impl LiteralType { Self::f64(v) => v.to_string(), Self::bool(v) => v.to_string(), Self::String(v) => format!(r#""{v}""#), + Self::char(v) => format!(r#""{v}""#), Self::None => "null".to_string(), } } } /// convert an object field into a Typescript string -fn object_field_to_ts(ctx: ExportContext, field: &StructField, type_map: &TypeMap) -> Output { - let field_name_safe = sanitise_key(&field.key, false); +fn object_field_to_ts( + ctx: ExportContext, + key: Cow<'static, str>, + field: &Field, + type_map: &TypeMap, +) -> Output { + let field_name_safe = sanitise_key(key, false); // https://github.com/oscartbeaumont/rspc/issues/100#issuecomment-1373092211 let (key, ty) = match field.optional { - true => (format!("{field_name_safe}?"), &field.ty), + true => (format!("{field_name_safe}?").into(), &field.ty), false => (field_name_safe, &field.ty), }; - Ok(format!("{key}: {}", datatype_inner(ctx, ty, type_map)?)) + Ok(format!( + "{key}: {}", + datatype_inner(ctx, ty, type_map, "null")? + )) } /// sanitise a string to be a valid Typescript key -fn sanitise_key(field_name: &str, force_string: bool) -> String { +fn sanitise_key<'a>(field_name: Cow<'static, str>, force_string: bool) -> Cow<'a, str> { let valid = field_name .chars() .all(|c| c.is_alphanumeric() || c == '_' || c == '$') @@ -435,9 +533,9 @@ fn sanitise_key(field_name: &str, force_string: bool) -> String { .unwrap_or(true); if force_string || !valid { - format!(r#""{field_name}""#) + format!(r#""{field_name}""#).into() } else { - field_name.to_string() + field_name } } diff --git a/src/lib.rs b/src/lib.rs index 20826a4..0e4b849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,8 @@ //! This results in a loss of information and lack of compatability with types from other crates. //! #![forbid(unsafe_code)] -#![warn(clippy::all, clippy::unwrap_used, clippy::panic, missing_docs)] +#![warn(clippy::all, clippy::unwrap_used, clippy::panic)] // TODO: missing_docs +#![allow(clippy::module_inception)] #![cfg_attr(docsrs, feature(doc_cfg))] #[doc(hidden)] @@ -73,6 +74,7 @@ pub mod export; pub mod functions; mod lang; mod selection; +mod serde; mod static_types; /// Contains [`Type`] and everything related to it, including implementations and helper macros pub mod r#type; @@ -82,6 +84,7 @@ pub use datatype::*; pub use lang::*; pub use r#type::*; pub use selection::*; +pub use serde::*; pub use static_types::*; /// Implements [`Type`] for a given struct or enum. diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..086919c --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,229 @@ +use thiserror::Error; + +use crate::{ + DataType, EnumRepr, EnumType, EnumVariant, LiteralType, PrimitiveType, StructFields, TypeMap, +}; + +// TODO: The error should show a path to the type causing the issue like the BigInt error reporting. + +#[derive(Error, Debug, PartialEq)] +pub enum SerdeError { + #[error("A map key must be a 'string' or 'number' type")] + InvalidMapKey, + #[error("#[specta(tag = \"...\")] cannot be used with tuple variants")] + InvalidInternallyTaggedEnum, +} + +/// Check that a [DataType] is a valid for Serde. +/// +/// This can be used by exporters which wanna do export-time checks that all types are compatible with Serde formats. +pub(crate) fn is_valid_ty(dt: &DataType, type_map: &TypeMap) -> Result<(), SerdeError> { + match dt { + DataType::Nullable(ty) => is_valid_ty(ty, type_map)?, + DataType::Map(ty) => { + is_valid_map_key(&ty.0, type_map)?; + is_valid_ty(&ty.1, type_map)?; + } + DataType::Struct(ty) => match ty.fields() { + StructFields::Unit => {} + StructFields::Unnamed(ty) => { + for field in ty.fields().iter() { + is_valid_ty(&field.ty, type_map)?; + } + } + StructFields::Named(ty) => { + for (_field_name, field) in ty.fields().iter() { + is_valid_ty(&field.ty, type_map)?; + } + } + }, + DataType::Enum(ty) => { + validate_enum(ty, type_map)?; + + for (_variant_name, variant) in ty.variants().iter() { + match variant { + EnumVariant::Unit => {} + EnumVariant::Named(variant) => { + for (_field_name, field) in variant.fields.iter() { + is_valid_ty(&field.ty, type_map)?; + } + } + EnumVariant::Unnamed(variant) => { + for field in variant.fields.iter() { + is_valid_ty(&field.ty, type_map)?; + } + } + } + } + } + DataType::Tuple(ty) => { + for field in ty.fields.iter() { + is_valid_ty(field, type_map)?; + } + } + DataType::Result(ty) => { + is_valid_ty(&ty.0, type_map)?; + is_valid_ty(&ty.1, type_map)?; + } + DataType::Reference(ty) => { + for generic in &ty.generics { + is_valid_ty(generic, type_map)?; + } + + let ty = type_map + .get(&ty.sid) + .as_ref() + .expect("Reference type not found") + .as_ref() + .expect("Type was never populated"); // TODO: Error properly + + is_valid_ty(&ty.inner, type_map)?; + } + _ => {} + } + + Ok(()) +} + +// Typescript: Must be assignable to `string | number | symbol` says Typescript. +fn is_valid_map_key(key_ty: &DataType, type_map: &TypeMap) -> Result<(), SerdeError> { + match key_ty { + DataType::Any => Ok(()), + DataType::Primitive(ty) => match ty { + PrimitiveType::i8 + | PrimitiveType::i16 + | PrimitiveType::i32 + | PrimitiveType::i64 + | PrimitiveType::i128 + | PrimitiveType::isize + | PrimitiveType::u8 + | PrimitiveType::u16 + | PrimitiveType::u32 + | PrimitiveType::u64 + | PrimitiveType::u128 + | PrimitiveType::usize + | PrimitiveType::f32 + | PrimitiveType::f64 + | PrimitiveType::String + | PrimitiveType::char => Ok(()), + _ => Err(SerdeError::InvalidMapKey), + }, + DataType::Literal(ty) => match ty { + LiteralType::i8(_) + | LiteralType::i16(_) + | LiteralType::i32(_) + | LiteralType::u8(_) + | LiteralType::u16(_) + | LiteralType::u32(_) + | LiteralType::f32(_) + | LiteralType::f64(_) + | LiteralType::String(_) + | LiteralType::char(_) => Ok(()), + _ => Err(SerdeError::InvalidMapKey), + }, + // Enum of other valid types are also valid Eg. `"A" | "B"` or `"A" | 5` are valid + DataType::Enum(ty) => { + for (_variant_name, variant) in &ty.variants { + match variant { + EnumVariant::Unit => {} + EnumVariant::Unnamed(item) => { + if item.fields.len() > 1 { + return Err(SerdeError::InvalidMapKey); + } + + if ty.repr != EnumRepr::Untagged { + return Err(SerdeError::InvalidMapKey); + } + } + _ => return Err(SerdeError::InvalidMapKey), + } + } + + Ok(()) + } + DataType::Reference(ty) => { + let ty = type_map + .get(&ty.sid) + .as_ref() + .expect("Reference type not found") + .as_ref() + .expect("Type was never populated"); // TODO: Error properly + + is_valid_map_key(&ty.inner, type_map) + } + _ => Err(SerdeError::InvalidMapKey), + } +} + +// Serde does not allow serializing a variant of certain types of enum's. +fn validate_enum(e: &EnumType, type_map: &TypeMap) -> Result<(), SerdeError> { + // Only internally tagged enums can be invalid. + if let EnumRepr::Internal { .. } = e.repr() { + validate_internally_tag_enum(e, type_map)?; + } + + Ok(()) +} + +// Checks for specially internally tagged enums. +fn validate_internally_tag_enum(e: &EnumType, type_map: &TypeMap) -> Result<(), SerdeError> { + for (_variant_name, variant) in &e.variants { + match variant { + EnumVariant::Unit => {} + EnumVariant::Named(_) => {} + EnumVariant::Unnamed(item) => { + let fields = item.fields(); + if fields.len() > 1 { + return Err(SerdeError::InvalidInternallyTaggedEnum); + } + + validate_internally_tag_enum_datatype(&fields[0].ty, type_map)?; + } + } + } + + Ok(()) +} + +// Internally tagged enums require map-type's (with a couple of exceptions like `null`) +// Which makes sense when you can't represent `{ "type": "A" } & string` in a single JSON value. +fn validate_internally_tag_enum_datatype( + ty: &DataType, + type_map: &TypeMap, +) -> Result<(), SerdeError> { + match ty { + // `serde_json::Any` can be *technically* be either valid or invalid based on the actual data but we are being strict and reject it. + DataType::Any => return Err(SerdeError::InvalidInternallyTaggedEnum), + DataType::Map(_) => {} + // Structs's are always map-types unless they are transparent then it depends on inner type. However, transparent passes through when calling `Type::inline` so we don't need to specially check that case. + DataType::Struct(_) => {} + DataType::Enum(ty) => match ty.repr { + // Is only valid if the enum itself is also valid. + EnumRepr::Untagged => validate_internally_tag_enum(ty, type_map)?, + // Eg. `{ "Variant": "value" }` is a map-type so valid. + EnumRepr::External => {} + // Eg. `{ "type": "variant", "field": "value" }` is a map-type so valid. + EnumRepr::Internal { .. } => {} + // Eg. `{ "type": "variant", "c": {} }` is a map-type so valid. + EnumRepr::Adjacent { .. } => {} + }, + // `()` is `null` and is valid + DataType::Tuple(ty) if ty.fields.is_empty() => {} + // Are valid as they are serialized as an map-type. Eg. `"Ok": 5` or `"Error": "todo"` + DataType::Result(_) => {} + // References need to be checked against the same rules. + DataType::Reference(ty) => { + let ty = type_map + .get(&ty.sid) + .as_ref() + .expect("Reference type not found") + .as_ref() + .expect("Type was never populated"); // TODO: Error properly + + validate_internally_tag_enum_datatype(&ty.inner, type_map)?; + } + _ => return Err(SerdeError::InvalidInternallyTaggedEnum), + } + + Ok(()) +} diff --git a/src/static_types.rs b/src/static_types.rs index f24132f..252d510 100644 --- a/src/static_types.rs +++ b/src/static_types.rs @@ -1,4 +1,4 @@ -use crate::{DataType, DefOpts, ExportError, Type}; +use crate::{DataType, DefOpts, Type}; /// A type that is unconstructable but is typed as `any` in TypeScript. /// @@ -16,7 +16,7 @@ use crate::{DataType, DefOpts, ExportError, Type}; pub enum Any {} impl Type for Any { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } diff --git a/src/type/impls.rs b/src/type/impls.rs index 371b8c3..704c49e 100644 --- a/src/type/impls.rs +++ b/src/type/impls.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{reference::Reference, *}; impl_primitives!( i8 i16 i32 i64 i128 isize @@ -22,25 +22,25 @@ const _: () = { }; impl<'a> Type for &'a str { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { String::inline(opts, generics) } } impl<'a, T: Type + 'static> Type for &'a T { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { T::inline(opts, generics) } } impl Type for [T] { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { T::inline(opts, generics) } } impl<'a, T: ?Sized + ToOwned + Type + 'static> Type for std::borrow::Cow<'a, T> { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { T::inline(opts, generics) } } @@ -113,103 +113,94 @@ impl_for_list!( ); impl<'a, T: Type> Type for &'a [T] { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { >::inline(opts, generics) } - - fn category_impl(opts: DefOpts, generics: &[DataType]) -> Result { - >::category_impl(opts, generics) - } } impl Type for [T; N] { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { >::inline(opts, generics) } - - fn category_impl(opts: DefOpts, generics: &[DataType]) -> Result { - >::category_impl(opts, generics) - } } impl Type for Option { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::Nullable(Box::new( - generics - .get(0) - .cloned() - .map_or_else(|| T::inline(opts, generics), Ok)?, - ))) - } - - fn category_impl(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(TypeCategory::Inline(DataType::Nullable(Box::new( + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + DataType::Nullable(Box::new( generics .get(0) .cloned() - .map_or_else(|| T::reference(opts, generics), Ok)?, - )))) + .unwrap_or_else(|| T::inline(opts, generics)), + )) } } -impl Type for Result { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::Result(Box::new(( +impl Type for std::result::Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + DataType::Result(Box::new(( T::inline( DefOpts { parent_inline: opts.parent_inline, type_map: opts.type_map, }, generics, - )?, + ), E::inline( DefOpts { parent_inline: opts.parent_inline, type_map: opts.type_map, }, generics, - )?, - )))) + ), + ))) } } impl Type for std::marker::PhantomData { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Literal(LiteralType::None)) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Literal(LiteralType::None) } } +// Serde does no support `Infallible` as it can't be constructed so a `&self` method is uncallable on it. #[allow(unused)] #[derive(Type)] #[specta(remote = std::convert::Infallible, crate = crate)] pub enum Infallible {} impl Type for std::ops::Range { - fn inline(opts: DefOpts, _generics: &[DataType]) -> Result { - let ty = T::definition(opts)?; - Ok(DataType::Struct(StructType { + fn inline(opts: DefOpts, _generics: &[DataType]) -> DataType { + let ty = T::definition(opts); + DataType::Struct(StructType { + name: "Range".into(), generics: vec![], - fields: vec![ - StructField { - key: "start".into(), - optional: false, - flatten: false, - ty: ty.clone(), - }, - StructField { - key: "end".into(), - optional: false, - flatten: false, - ty, - }, - ], - tag: None, - })) + fields: StructFields::Named(NamedFields { + fields: vec![ + ( + "start".into(), + Field { + optional: false, + flatten: false, + ty: ty.clone(), + }, + ), + ( + "end".into(), + Field { + optional: false, + flatten: false, + ty, + }, + ), + ], + tag: None, + }), + }) } } impl Type for std::ops::RangeInclusive { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { std::ops::Range::::inline(opts, generics) // Yeah Serde are cringe } } @@ -248,33 +239,50 @@ const _: () = { impl Flatten for serde_json::Map {} impl Type for serde_json::Value { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } impl Type for serde_json::Number { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Enum( - UntaggedEnum { - variants: vec![ - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::f64)], - generics: vec![], + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Enum(EnumType { + name: "Number".into(), + repr: EnumRepr::Untagged, + variants: vec![ + ( + "f64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::f64), + }], }), - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::i64)], - generics: vec![], + ), + ( + "i64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::i64), + }], }), - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::u64)], - generics: vec![], + ), + ( + "u64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::u64), + }], }), - ], - generics: vec![], - } - .into(), - )) + ), + ], + generics: vec![], + }) } } }; @@ -282,45 +290,62 @@ const _: () = { #[cfg(feature = "serde_yaml")] const _: () = { impl Type for serde_yaml::Value { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } impl Type for serde_yaml::Mapping { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } impl Type for serde_yaml::value::TaggedValue { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } impl Type for serde_yaml::Number { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Enum( - UntaggedEnum { - variants: vec![ - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::f64)], - generics: vec![], + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Enum(EnumType { + name: "Number".into(), + repr: EnumRepr::Untagged, + variants: vec![ + ( + "f64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::f64), + }], }), - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::i64)], - generics: vec![], + ), + ( + "i64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::i64), + }], }), - EnumVariant::Unnamed(TupleType::Named { - fields: vec![DataType::Primitive(PrimitiveType::u64)], - generics: vec![], + ), + ( + "u64".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: DataType::Primitive(PrimitiveType::u64), + }], }), - ], - generics: vec![], - } - .into(), - )) + ), + ], + generics: vec![], + }) } } }; @@ -331,8 +356,8 @@ const _: () = { impl Flatten for toml::map::Map {} impl Type for toml::Value { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Any) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Any } } @@ -391,14 +416,14 @@ const _: () = { ); impl Type for DateTime { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { String::inline(opts, generics) } } #[allow(deprecated)] impl Type for Date { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { String::inline(opts, generics) } } @@ -510,31 +535,45 @@ impl_as!(url::Url as String); #[cfg(feature = "either")] impl Type for either::Either { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::Enum(EnumType::Untagged(UntaggedEnum { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + DataType::Enum(EnumType { + name: "Either".into(), + repr: EnumRepr::Untagged, variants: vec![ - EnumVariant::Unnamed(TupleType::Named { - fields: vec![L::inline( - DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - }, - generics, - )?], - generics: vec![], - }), - EnumVariant::Unnamed(TupleType::Named { - fields: vec![R::inline( - DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - }, - generics, - )?], - generics: vec![], - }), + ( + "Left".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: L::inline( + DefOpts { + parent_inline: opts.parent_inline, + type_map: opts.type_map, + }, + generics, + ), + }], + }), + ), + ( + "Right".into(), + EnumVariant::Unnamed(UnnamedFields { + fields: vec![Field { + optional: false, + flatten: false, + ty: R::inline( + DefOpts { + parent_inline: opts.parent_inline, + type_map: opts.type_map, + }, + generics, + ), + }], + }), + ), ], generics: vec![], - }))) + }) } } diff --git a/src/type/macros.rs b/src/type/macros.rs index a6ab065..1eb3ef6 100644 --- a/src/type/macros.rs +++ b/src/type/macros.rs @@ -1,8 +1,8 @@ macro_rules! impl_primitives { ($($i:ident)+) => {$( impl Type for $i { - fn inline(_: DefOpts, _: &[DataType]) -> Result { - Ok(DataType::Primitive(datatype::PrimitiveType::$i)) + fn inline(_: DefOpts, _: &[DataType]) -> DataType { + DataType::Primitive(datatype::PrimitiveType::$i) } } )+}; @@ -16,10 +16,10 @@ macro_rules! impl_tuple { #[allow(non_snake_case)] impl<$($i: Type + 'static),*> Type for ($($i),*) { #[allow(unused)] - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { let mut _generics = generics.iter(); - $(let $i = _generics.next().map(Clone::clone).map_or_else( + $(let $i = _generics.next().map(Clone::clone).unwrap_or_else( || { $i::reference( DefOpts { @@ -27,15 +27,13 @@ macro_rules! impl_tuple { type_map: opts.type_map, }, generics, - ) + ).inner }, - Ok, - )?;)* + );)* - Ok(datatype::TupleType::Named { + datatype::TupleType { fields: vec![$($i),*], - generics: vec![] - }.to_anonymous()) + }.to_anonymous() } } }; @@ -49,28 +47,24 @@ macro_rules! impl_tuple { macro_rules! impl_containers { ($($container:ident)+) => {$( impl Type for $container { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - generics.get(0).cloned().map_or_else( + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + generics.get(0).cloned().unwrap_or_else( || { T::inline( opts, generics, ) }, - Ok, ) } - fn reference(opts: DefOpts, generics: &[DataType]) -> Result { - generics.get(0).cloned().map_or_else( - || { - T::reference( - opts, - generics, - ) - }, - Ok, - ) + fn reference(opts: DefOpts, generics: &[DataType]) -> Reference { + Reference { + inner: generics.get(0).cloned().unwrap_or_else( + || T::reference(opts, generics).inner, + ), + _priv: (), + } } } @@ -78,11 +72,11 @@ macro_rules! impl_containers { const SID: SpectaID = T::SID; const IMPL_LOCATION: ImplLocation = T::IMPL_LOCATION; - fn named_data_type(opts: DefOpts, generics: &[DataType]) -> Result { + fn named_data_type(opts: DefOpts, generics: &[DataType]) -> NamedDataType { T::named_data_type(opts, generics) } - fn definition_named_data_type(opts: DefOpts) -> Result { + fn definition_named_data_type(opts: DefOpts) -> NamedDataType { T::definition_named_data_type(opts) } } @@ -94,11 +88,11 @@ macro_rules! impl_containers { macro_rules! impl_as { ($($ty:path as $tty:ident)+) => {$( impl Type for $ty { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { <$tty as Type>::inline(opts, generics) } - fn reference(opts: DefOpts, generics: &[DataType]) -> Result { + fn reference(opts: DefOpts, generics: &[DataType]) -> Reference { <$tty as Type>::reference(opts, generics) } } @@ -108,23 +102,20 @@ macro_rules! impl_as { macro_rules! impl_for_list { ($($ty:path as $name:expr)+) => {$( impl Type for $ty { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::List(Box::new(generics.get(0).cloned().unwrap_or(T::inline( + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + DataType::List(Box::new(generics.get(0).cloned().unwrap_or_else(|| T::inline( opts, generics, - )?)))) + )))) } - fn reference(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::List(Box::new(generics.get(0).cloned().map_or_else( - || { - T::reference( - opts, - generics, - ) - }, - Ok, - )?))) + fn reference(opts: DefOpts, generics: &[DataType]) -> Reference { + Reference { + inner: DataType::List(Box::new(generics.get(0).cloned().unwrap_or_else( + || T::reference(opts, generics).inner, + ))), + _priv: (), + } } } )+}; @@ -133,39 +124,33 @@ macro_rules! impl_for_list { macro_rules! impl_for_map { ($ty:path as $name:expr) => { impl Type for $ty { - fn inline(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::Map(Box::new(( - generics.get(0).cloned().map_or_else( - || { - K::inline( - DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - }, - generics, - ) - }, - Ok, - )?, - generics.get(1).cloned().map_or_else( - || { - V::inline( - DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - }, - generics, - ) - }, - Ok, - )?, - )))) + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { + DataType::Map(Box::new(( + generics.get(0).cloned().unwrap_or_else(|| { + K::inline( + DefOpts { + parent_inline: opts.parent_inline, + type_map: opts.type_map, + }, + generics, + ) + }), + generics.get(1).cloned().unwrap_or_else(|| { + V::inline( + DefOpts { + parent_inline: opts.parent_inline, + type_map: opts.type_map, + }, + generics, + ) + }), + ))) } - fn reference(opts: DefOpts, generics: &[DataType]) -> Result { - Ok(DataType::Map(Box::new(( - generics.get(0).cloned().map_or_else( - || { + fn reference(opts: DefOpts, generics: &[DataType]) -> Reference { + Reference { + inner: DataType::Map(Box::new(( + generics.get(0).cloned().unwrap_or_else(|| { K::reference( DefOpts { parent_inline: opts.parent_inline, @@ -173,11 +158,9 @@ macro_rules! impl_for_map { }, generics, ) - }, - Ok, - )?, - generics.get(1).cloned().map_or_else( - || { + .inner + }), + generics.get(1).cloned().unwrap_or_else(|| { V::reference( DefOpts { parent_inline: opts.parent_inline, @@ -185,10 +168,11 @@ macro_rules! impl_for_map { }, generics, ) - }, - Ok, - )?, - )))) + .inner + }), + ))), + _priv: (), + } } } }; diff --git a/src/type/mod.rs b/src/type/mod.rs index b76235f..1da17f0 100644 --- a/src/type/mod.rs +++ b/src/type/mod.rs @@ -1,5 +1,3 @@ -use thiserror::Error; - use crate::*; #[macro_use] @@ -9,23 +7,7 @@ mod post_process; pub use post_process::*; -/// The category a type falls under. -/// Determines how references are generated for a given type. -pub enum TypeCategory { - /// No references should be created, instead just copies the inline representation of the type. - Inline(DataType), - /// The type should be properly referenced and stored in the type map to be defined outside of - /// where it is referenced. - Reference(DataTypeReference), -} - -/// Type exporting errors. -#[derive(Error, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum ExportError { - #[error("Atemmpted to export type defined at '{}' but encountered error: {1}", .0.as_str())] - InvalidType(ImplLocation, &'static str), -} +use self::reference::Reference; /// Provides runtime type information that can be fed into a language exporter to generate a type definition in another language. /// Avoid implementing this trait yourself where possible and use the [`Type`](derive@crate::Type) macro instead. @@ -35,7 +17,7 @@ pub trait Type { /// [`definition`](crate::Type::definition) and [`reference`](crate::Type::definition) /// /// Implemented internally or via the [`Type`](derive@crate::Type) macro - fn inline(opts: DefOpts, generics: &[DataType]) -> Result; + fn inline(opts: DefOpts, generics: &[DataType]) -> DataType; /// Returns the type parameter generics of a given type. /// Will usually be empty except for custom types. @@ -50,7 +32,7 @@ pub trait Type { /// as the value for the `generics` arg. /// /// Implemented internally - fn definition(opts: DefOpts) -> Result { + fn definition(opts: DefOpts) -> DataType { Self::inline( opts, &Self::definition_generics() @@ -60,68 +42,26 @@ pub trait Type { ) } - /// Defines which category this type falls into, determining how references to it are created. - /// See [`TypeCategory`] for more info. - /// - /// Implemented internally or via the [`Type`](derive@crate::Type) macro - fn category_impl(opts: DefOpts, generics: &[DataType]) -> Result { - Self::inline(opts, generics).map(TypeCategory::Inline) - } - /// Generates a datatype corresponding to a reference to this type, /// as determined by its category. Getting a reference to a type implies that /// it should belong in the type map (since it has to be referenced from somewhere), /// so the output of [`definition`](crate::Type::definition) will be put into the type map. - /// - /// Implemented internally - fn reference(opts: DefOpts, generics: &[DataType]) -> Result { - let category = Self::category_impl( - DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - }, - generics, - )?; - - Ok(match category { - TypeCategory::Inline(inline) => inline, - TypeCategory::Reference(def) => { - if opts.type_map.get(&def.sid()).is_none() { - opts.type_map.entry(def.sid()).or_default(); - - let definition = Self::definition(DefOpts { - parent_inline: opts.parent_inline, - type_map: opts.type_map, - })?; - - // TODO: It would be nice if we removed the `TypeCategory` and used the `NamedType` trait or something so this unreachable isn't needed. - let definition = match definition { - DataType::Named(definition) => definition, - _ => unreachable!(), - }; - - opts.type_map.insert(def.sid(), Some(definition)); - } - - DataType::Reference(def) - } - }) + fn reference(opts: DefOpts, generics: &[DataType]) -> Reference { + reference::inline::(opts, generics) } } /// NamedType represents a type that can be converted into [NamedDataType]. -/// This will be all types created by the derive macro. +/// This will be implemented for all types with the [Type] derive macro. pub trait NamedType: Type { const SID: SpectaID; const IMPL_LOCATION: ImplLocation; /// this is equivalent to [Type::inline] but returns a [NamedDataType] instead. - /// This is a compile-time guaranteed alternative to extracting the `DataType::Named` variant. - fn named_data_type(opts: DefOpts, generics: &[DataType]) -> Result; + fn named_data_type(opts: DefOpts, generics: &[DataType]) -> NamedDataType; /// this is equivalent to [Type::definition] but returns a [NamedDataType] instead. - /// This is a compile-time guaranteed alternative to extracting the `DataType::Named` variant. - fn definition_named_data_type(opts: DefOpts) -> Result { + fn definition_named_data_type(opts: DefOpts) -> NamedDataType { Self::named_data_type( opts, &Self::definition_generics() @@ -132,6 +72,57 @@ pub trait NamedType: Type { } } +/// Helpers for generating [Type::reference] implementations. +pub mod reference { + use super::*; + + /// A reference datatype. + /// + // This type exists to force the user to use [reference::inline] or [reference::reference] which provides some extra safety. + pub struct Reference { + pub inner: DataType, + pub(crate) _priv: (), + } + + pub fn inline(opts: DefOpts, generics: &[DataType]) -> Reference { + Reference { + inner: T::inline(opts, generics), + _priv: (), + } + } + + pub fn reference( + opts: DefOpts, + generics: &[DataType], + reference: DataTypeReference, + ) -> Reference { + if opts.type_map.get(&T::SID).is_none() { + // It's important we don't put `None` into the map here. By putting a *real* value we ensure that we don't stack overflow for recursive types when calling `named_data_type`. + opts.type_map.entry(T::SID).or_insert(Some(NamedDataType { + name: "placeholder".into(), + comments: vec![], + deprecated: None, + ext: None, + inner: DataType::Any, + })); + + let dt = T::named_data_type( + DefOpts { + parent_inline: true, + type_map: opts.type_map, + }, + generics, + ); + opts.type_map.insert(T::SID, Some(dt)); + } + + Reference { + inner: DataType::Reference(reference), + _priv: (), + } + } +} + /// A marker trait for compile-time validation of which types can be flattened. pub trait Flatten: Type {} diff --git a/tests/advanced_types.rs b/tests/advanced_types.rs index d6dcbc0..ad6d2ea 100644 --- a/tests/advanced_types.rs +++ b/tests/advanced_types.rs @@ -38,11 +38,11 @@ fn test_type_aliases() { assert_ts!(FullGeneric, "{ a: number; b: boolean }"); assert_ts!(Another, "{ a: number; b: boolean }"); - assert_ts!(MapA, "{ [key: string]: number }"); - assert_ts!(MapB, "{ [key: number]: string }"); + assert_ts!(MapA, "{ [key in string]: number }"); + assert_ts!(MapB, "{ [key in number]: string }"); assert_ts!( MapC, - "{ [key: string]: { field: Demo } }" + "{ [key in string]: { field: Demo } }" ); assert_ts!(Struct, "{ field: Demo }"); diff --git a/tests/bigints.rs b/tests/bigints.rs index 81eb675..9e5f431 100644 --- a/tests/bigints.rs +++ b/tests/bigints.rs @@ -52,6 +52,14 @@ pub enum EnumWithStructWithStructWithBigInt { A(StructWithStructWithBigInt), } +#[derive(Type)] +#[specta(export = false)] + +pub enum EnumWithInlineStructWithBigInt { + #[specta(inline)] + B { a: i128 }, +} + #[test] fn test_bigint_types() { for_bigint_types!(T -> |name| assert_eq!(specta::ts::inline::(&ExportConfig::default()), Err(TsExportError::BigIntForbidden(ExportPath::new_unsafe(name))))); @@ -101,4 +109,10 @@ fn test_bigint_types() { "EnumWithStructWithStructWithBigInt::A -> StructWithStructWithBigInt.abc -> StructWithBigInt.a -> i128" ))) ); + assert_eq!( + specta::ts::inline::(&ExportConfig::default()), + Err(TsExportError::BigIntForbidden(ExportPath::new_unsafe( + "EnumWithInlineStructWithBigInt::B.a -> i128" + ))) + ); } diff --git a/tests/datatype.rs b/tests/datatype.rs index 555fe52..31050b4 100644 --- a/tests/datatype.rs +++ b/tests/datatype.rs @@ -29,6 +29,12 @@ struct Procedures4 { #[derive(DataTypeFrom)] struct Procedures5(Vec); +#[derive(DataTypeFrom)] +struct Procedures7(); + +#[derive(DataTypeFrom)] +struct Procedures8 {} + #[test] fn test_datatype() { let val: TupleType = Procedures1(vec![ @@ -135,4 +141,24 @@ fn test_datatype() { ), Ok("{ queries: \"A\" | \"B\" }".into()) ); + + let val: TupleType = Procedures7().into(); + assert_eq!( + ts::datatype( + &Default::default(), + &val.clone().to_anonymous(), + &Default::default() + ), + Ok("null".into()) // This is equivalent of `()` Because this is a `TupleType` not an `EnumType`. + ); + + let val: StructType = Procedures8 {}.into(); + assert_eq!( + ts::datatype( + &Default::default(), + &val.clone().to_anonymous(), + &Default::default() + ), + Ok("Record".into()) + ); } diff --git a/tests/flatten_and_inline.rs b/tests/flatten_and_inline.rs new file mode 100644 index 0000000..b3d1fc3 --- /dev/null +++ b/tests/flatten_and_inline.rs @@ -0,0 +1,131 @@ +use std::{collections::HashMap, sync::Arc}; + +use specta::Type; + +use crate::ts::assert_ts; + +#[derive(Type)] +#[specta(export = false)] +pub struct A { + pub a: String, +} + +#[derive(Type)] +#[specta(export = false)] +pub struct AA { + pub a: i32, +} + +#[derive(Type)] +#[specta(export = false)] +pub struct B { + #[specta(flatten)] + pub a: A, + #[specta(flatten)] + pub b: HashMap, + #[specta(flatten)] + pub c: Arc, +} + +#[derive(Type)] +#[specta(export = false)] +pub struct C { + #[specta(flatten)] + pub a: A, + #[specta(inline)] + pub b: A, +} + +#[derive(Type)] +#[specta(export = false, tag = "type")] +pub struct D { + #[specta(flatten)] + pub a: A, + #[specta(inline)] + pub b: A, +} + +#[derive(Type)] +#[specta(export = false)] +#[serde(untagged)] +pub struct E { + #[specta(flatten)] + pub a: A, + #[specta(inline)] + pub b: A, +} + +// Flattening a struct multiple times +#[derive(Type)] +#[specta(export = false)] +pub struct F { + #[specta(flatten)] + pub a: A, + #[specta(flatten)] + pub b: A, +} + +// Two fields with the same name (`a`) but different types +#[derive(Type)] +#[specta(export = false)] +pub struct G { + #[specta(flatten)] + pub a: A, + #[specta(flatten)] + pub b: AA, +} + +// Serde can't serialize this +#[derive(Type)] +#[specta(export = false)] +pub enum H { + A(String), + B, +} + +// TODO: Invalid Serde type but unit test this at the datamodel level cause it might be valid in other langs. +// #[derive(Type)] +// #[specta(export = false, tag = "type")] +// pub enum I { +// A(String), +// B, +// #[specta(inline)] +// C(A), +// D(#[specta(flatten)] A), +// } + +#[derive(Type)] +#[specta(export = false, tag = "t", content = "c")] +pub enum J { + A(String), + B, + #[specta(inline)] + C(A), + D(A), +} + +#[derive(Type)] +#[specta(export = false, untagged)] +pub enum K { + A(String), + B, + #[specta(inline)] + C(A), + D(A), +} + +#[test] +fn serde() { + assert_ts!( + B, + "({ a: string }) & ({ [key in string]: string }) & ({ a: string })" + ); + assert_ts!(C, "({ a: string }) & { b: { a: string } }"); + assert_ts!(D, "({ a: string }) & { b: { a: string }; type: \"D\" }"); + assert_ts!(E, "({ a: string }) & { b: { a: string } }"); + assert_ts!(F, "({ a: string }) & ({ a: string })"); + assert_ts!(G, "({ a: string }) & ({ a: number })"); + assert_ts!(H, "{ A: string } | \"B\""); + assert_ts!(J, "{ t: \"A\"; c: string } | { t: \"B\" } | { t: \"C\"; c: { a: string } } | { t: \"D\"; c: A }"); + assert_ts!(K, "string | null | { a: string } | A"); +} diff --git a/tests/functions.rs b/tests/functions.rs index e63e68a..9b3ccb0 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -84,20 +84,19 @@ mod test { #[test] fn test_trailing_comma() { - functions::collect_functions![a].unwrap(); - functions::collect_functions![a,].unwrap(); - functions::collect_functions![a, b, c].unwrap(); - functions::collect_functions![a, b, c,].unwrap(); + functions::collect_functions![a]; + functions::collect_functions![a,]; + functions::collect_functions![a, b, c]; + functions::collect_functions![a, b, c,]; - let (functions, types) = - functions::collect_functions![a, b, c, d, e::, f, g, h, i, k].unwrap(); + let (functions, types) = functions::collect_functions![a, b, c, d, e::, f, g, h, i, k]; } #[test] fn test_function_exporting() { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; a).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; a); assert_eq!(def.asyncness, false); assert_eq!(def.name, "a"); assert_eq!(def.args.len(), 0); @@ -109,7 +108,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; b).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; b); assert_eq!(def.asyncness, false); assert_eq!(def.name, "b"); assert_eq!(def.args.len(), 1); @@ -125,7 +124,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; c).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; c); assert_eq!(def.asyncness, false); assert_eq!(def.name, "c"); assert_eq!(def.args.len(), 3); @@ -149,7 +148,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; d).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; d); assert_eq!(def.asyncness, false); assert_eq!(def.name, "d"); assert_eq!(def.args.len(), 1); @@ -165,8 +164,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = - specta::fn_datatype!(type_map; e::).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; e::); assert_eq!(def.asyncness, false); assert_eq!(def.name, "e"); assert_eq!(def.args.len(), 1); @@ -182,7 +180,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; f).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; f); assert_eq!(def.asyncness, false); assert_eq!(def.name, "f"); assert_eq!(def.args.len(), 1); @@ -198,7 +196,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; g).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; g); assert_eq!(def.asyncness, false); assert_eq!(def.name, "g"); assert_eq!(def.args.len(), 1); @@ -214,7 +212,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; h).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; h); assert_eq!(def.asyncness, false); assert_eq!(def.name, "h"); assert_eq!(def.args.len(), 1); @@ -230,7 +228,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; i).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; i); assert_eq!(def.asyncness, false); assert_eq!(def.name, "i"); assert_eq!(def.args.len(), 0); @@ -242,7 +240,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; k).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; k); assert_eq!(def.asyncness, false); assert_eq!(def.name, "k"); assert_eq!(def.args.len(), 0); @@ -254,7 +252,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; l).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; l); assert_eq!(def.asyncness, false); assert_eq!(def.name, "l"); assert_eq!(def.args.len(), 2); @@ -270,7 +268,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = specta::fn_datatype!(type_map; m).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; m); assert_eq!(def.asyncness, false); assert_eq!(def.name, "m"); assert_eq!(def.args.len(), 1); @@ -282,8 +280,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = - specta::fn_datatype!(type_map; async_fn).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; async_fn); assert_eq!(def.asyncness, true); assert_eq!(def.name, "async_fn"); assert_eq!(def.args.len(), 0); @@ -295,8 +292,7 @@ mod test { { let mut type_map = &mut specta::TypeMap::default(); - let def: functions::FunctionDataType = - specta::fn_datatype!(type_map; with_docs).unwrap(); + let def: functions::FunctionDataType = specta::fn_datatype!(type_map; with_docs); assert_eq!(def.asyncness, false); assert_eq!(def.name, "with_docs"); assert_eq!(def.args.len(), 0); diff --git a/tests/lib.rs b/tests/lib.rs index 6b3356a..d800c77 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,10 +5,15 @@ mod bigints; mod datatype; mod duplicate_ty_name; mod export; +mod flatten_and_inline; mod functions; mod macro_decls; +mod map_keys; +mod rename; mod reserved_keywords; mod selection; +mod serde; +mod transparent; pub mod ts; mod ts_rs; mod ty_override; diff --git a/tests/macro/compile_error.rs b/tests/macro/compile_error.rs index 51fb5c3..8269a71 100644 --- a/tests/macro/compile_error.rs +++ b/tests/macro/compile_error.rs @@ -91,4 +91,8 @@ struct InvalidAttrs4 { a: String, } +#[derive(Type)] +#[specta(transparent)] +pub enum TransparentEnum {} + // TODO: https://docs.rs/trybuild/latest/trybuild/#what-to-test diff --git a/tests/macro/compile_error.stderr b/tests/macro/compile_error.stderr index 62775cd..6d96397 100644 --- a/tests/macro/compile_error.stderr +++ b/tests/macro/compile_error.stderr @@ -34,11 +34,17 @@ error: specta: Found unsupported field attribute 'noshot' 90 | #[specta(noshot)] | ^^^^^^ +error: #[specta(transparent)] is not allowed on an enum + --> tests/macro/compile_error.rs:96:5 + | +96 | pub enum TransparentEnum {} + | ^^^^ + error[E0601]: `main` function not found in crate `$CRATE` - --> tests/macro/compile_error.rs:92:2 + --> tests/macro/compile_error.rs:96:28 | -92 | } - | ^ consider adding a `main` function to `$DIR/tests/macro/compile_error.rs` +96 | pub enum TransparentEnum {} + | ^ consider adding a `main` function to `$DIR/tests/macro/compile_error.rs` error[E0277]: the trait bound `UnitExternal: specta::Flatten` is not satisfied --> tests/macro/compile_error.rs:32:11 @@ -56,7 +62,7 @@ error[E0277]: the trait bound `UnitExternal: specta::Flatten` is not satisfied toml_datetime::datetime::Offset FlattenInternal and $N others -note: required by a bound in `_::::named_data_type::validate_flatten` +note: required by a bound in `_::::inline::validate_flatten` --> tests/macro/compile_error.rs:29:10 | 29 | #[derive(Type)] @@ -79,7 +85,7 @@ error[E0277]: the trait bound `UnnamedMultiExternal: specta::Flatten` is not sat toml_datetime::datetime::Offset FlattenInternal and $N others -note: required by a bound in `_::::named_data_type::validate_flatten` +note: required by a bound in `_::::inline::validate_flatten` --> tests/macro/compile_error.rs:29:10 | 29 | #[derive(Type)] @@ -102,7 +108,7 @@ error[E0277]: the trait bound `UnnamedUntagged: specta::Flatten` is not satisfie toml_datetime::datetime::Offset FlattenInternal and $N others -note: required by a bound in `_::::named_data_type::validate_flatten` +note: required by a bound in `_::::inline::validate_flatten` --> tests/macro/compile_error.rs:49:10 | 49 | #[derive(Type)] @@ -125,7 +131,7 @@ error[E0277]: the trait bound `UnnamedMultiUntagged: specta::Flatten` is not sat toml_datetime::datetime::Offset FlattenInternal and $N others -note: required by a bound in `_::::named_data_type::validate_flatten` +note: required by a bound in `_::::inline::validate_flatten` --> tests/macro/compile_error.rs:49:10 | 49 | #[derive(Type)] @@ -148,7 +154,7 @@ error[E0277]: the trait bound `UnnamedInternal: specta::Flatten` is not satisfie toml_datetime::datetime::Offset FlattenInternal and $N others -note: required by a bound in `_::::named_data_type::validate_flatten` +note: required by a bound in `_::::inline::validate_flatten` --> tests/macro/compile_error.rs:67:10 | 67 | #[derive(Type)] diff --git a/tests/map_keys.rs b/tests/map_keys.rs new file mode 100644 index 0000000..bd4059a --- /dev/null +++ b/tests/map_keys.rs @@ -0,0 +1,89 @@ +use std::{collections::HashMap, convert::Infallible}; + +use specta::{Any, SerdeError, Type}; + +use crate::ts::{assert_ts, assert_ts_export}; + +// Export needs a `NamedDataType` but uses `Type::reference` instead of `Type::inline` so we test it. +#[derive(Type)] +#[specta(export = false)] +struct Regular(HashMap); + +#[derive(Type)] +#[specta(export = false)] +struct RegularStruct { + a: String, +} + +#[derive(Type)] +#[specta(export = false, transparent)] +struct TransparentStruct(String); + +#[derive(Type)] +#[specta(export = false)] +enum UnitVariants { + A, + B, + C, +} + +#[derive(Type)] +#[specta(export = false, untagged)] +enum UntaggedVariants { + A(String), + B(i32), + C(u8), +} + +#[derive(Type)] +#[specta(export = false, untagged)] +enum InvalidUntaggedVariants { + A(String), + B(i32, String), + C(u8), +} + +#[derive(Type)] +#[specta(export = false)] +enum Variants { + A(String), + B(i32), + C(u8), +} + +#[derive(Type)] +#[specta(export = false, transparent)] +pub struct MaybeValidKey(T); + +#[derive(Type)] +#[specta(export = false, transparent)] +pub struct ValidMaybeValidKey(HashMap, ()>); + +#[derive(Type)] +#[specta(export = false, transparent)] +pub struct InvalidMaybeValidKey(HashMap, ()>); + +#[test] +fn map_keys() { + assert_ts!(HashMap, "{ [key in string]: null }"); + assert_ts_export!(Regular, "export type Regular = { [key in string]: null }"); + assert_ts!(HashMap, "{ [key in never]: null }"); + assert_ts!(HashMap, "{ [key in any]: null }"); + assert_ts!(HashMap, "{ [key in string]: null }"); + assert_ts!(HashMap, "{ [key in \"A\" | \"B\" | \"C\"]: null }"); + assert_ts!(HashMap, "{ [key in string | number]: null }"); + assert_ts!( + ValidMaybeValidKey, + "{ [key in MaybeValidKey]: null }" + ); + assert_ts_export!( + ValidMaybeValidKey, + "export type ValidMaybeValidKey = { [key in MaybeValidKey]: null }" + ); + + assert_ts!(error; HashMap<() /* `null` */, ()>, SerdeError::InvalidMapKey); + assert_ts!(error; HashMap, SerdeError::InvalidMapKey); + assert_ts!(error; HashMap, SerdeError::InvalidMapKey); + assert_ts!(error; InvalidMaybeValidKey, SerdeError::InvalidMapKey); + assert_ts_export!(error; InvalidMaybeValidKey, SerdeError::InvalidMapKey); +} diff --git a/tests/rename.rs b/tests/rename.rs new file mode 100644 index 0000000..bdca6e3 --- /dev/null +++ b/tests/rename.rs @@ -0,0 +1,57 @@ +use specta::Type; + +use crate::ts::{assert_ts, assert_ts_export}; + +#[derive(Type)] +#[specta(export = false, rename = "StructNew", tag = "t")] +pub struct Struct { + a: String, +} + +#[derive(Type)] +#[specta(export = false)] +pub struct Struct2 { + #[specta(rename = "b")] + a: String, +} + +#[derive(Type)] +#[specta(export = false, rename = "EnumNew", tag = "t")] +pub enum Enum { + A, + B, +} + +#[derive(Type)] +#[specta(export = false, rename = "EnumNew", tag = "t")] +pub enum Enum2 { + #[specta(rename = "C")] + A, + B, +} + +#[derive(Type)] +#[specta(export = false, rename = "EnumNew", tag = "t")] +pub enum Enum3 { + A { + #[specta(rename = "b")] + a: String, + }, +} + +#[test] +fn rename() { + assert_ts!(Struct, "{ a: string; t: \"StructNew\" }"); + assert_ts_export!( + Struct, + "export type StructNew = { a: string; t: \"StructNew\" }" + ); + + assert_ts!(Struct2, "{ b: string }"); + + assert_ts!(Enum, "{ t: \"A\" } | { t: \"B\" }"); + assert_ts_export!(Enum, "export type EnumNew = { t: \"A\" } | { t: \"B\" }"); + + assert_ts!(Enum2, "{ t: \"C\" } | { t: \"B\" }"); + assert_ts!(Enum3, "{ t: \"A\"; b: string }"); +} diff --git a/tests/serde/adjacently_tagged.rs b/tests/serde/adjacently_tagged.rs new file mode 100644 index 0000000..8289491 --- /dev/null +++ b/tests/serde/adjacently_tagged.rs @@ -0,0 +1,21 @@ +use specta::Type; + +use crate::ts::assert_ts; + +#[derive(Type)] +#[specta(export = false, tag = "t", content = "c")] +enum A { + A, + B { id: String, method: String }, + C(String), +} + +#[test] +fn adjacently_tagged() { + // There is not way to construct an invalid adjacently tagged type. + + assert_ts!( + A, + "{ t: \"A\" } | { t: \"B\"; c: { id: string; method: string } } | { t: \"C\"; c: string }" + ); +} diff --git a/tests/serde/externally_tagged.rs b/tests/serde/externally_tagged.rs new file mode 100644 index 0000000..51d71f1 --- /dev/null +++ b/tests/serde/externally_tagged.rs @@ -0,0 +1,21 @@ +use specta::Type; + +use crate::ts::assert_ts; + +#[derive(Type)] +#[specta(export = false)] +enum A { + A, + B { id: String, method: String }, + C(String), +} + +#[test] +fn externally_tagged() { + // There is not way to construct an invalid externally tagged type. + + assert_ts!( + A, + "\"A\" | { B: { id: string; method: string } } | { C: string }" + ); +} diff --git a/tests/serde/internally_tagged.rs b/tests/serde/internally_tagged.rs new file mode 100644 index 0000000..5b0c624 --- /dev/null +++ b/tests/serde/internally_tagged.rs @@ -0,0 +1,135 @@ +use std::collections::HashMap; + +use specta::{SerdeError, Type}; + +use crate::ts::assert_ts; + +// This type won't even compile with Serde macros +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum A { + // For internal tagging all variants must be a unit, named or *unnamed with a single variant*. + A(String, u32), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum B { + // Is not a map-type so invalid. + A(String), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum C { + // Is not a map-type so invalid. + A(Vec), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum D { + // Is a map type so valid. + A(HashMap), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum E { + // Null is valid (although it's not a map-type) + A(()), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum F { + // `FInner` is untagged so this is *only* valid if it is (which it is) + A(FInner), +} + +#[derive(Type)] +#[serde(export = false, untagged)] +pub enum FInner { + A(()), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum G { + // `GInner` is untagged so this is *only* valid if it is (which it is not) + A(GInner), +} + +#[derive(Type)] +#[serde(export = false, untagged)] +pub enum GInner { + A(String), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum H { + // `HInner` is transparent so this is *only* valid if it is (which it is) + A(HInner), +} + +#[derive(Type)] +#[serde(export = false, transparent)] +pub struct HInner(()); + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum I { + // `IInner` is transparent so this is *only* valid if it is (which it is not) + A(IInner), +} + +#[derive(Type)] +#[serde(export = false, transparent)] +pub struct IInner(String); + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum L { + // Internally tag enum with inlined field that is itself internally tagged + #[specta(inline)] + A(LInner), +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum LInner { + A, + B, +} + +#[derive(Type)] +#[serde(export = false, tag = "type")] +pub enum M { + // Internally tag enum with inlined field that is untagged + // `MInner` is `null` - Test `B` in `untagged.rs` + #[specta(inline)] + A(MInner), +} + +#[derive(Type)] +#[serde(export = false, untagged)] +pub enum MInner { + A, + B, +} + +#[test] +fn internally_tagged() { + assert_ts!(error; A, SerdeError::InvalidInternallyTaggedEnum); + assert_ts!(error; B, SerdeError::InvalidInternallyTaggedEnum); + assert_ts!(error; C, SerdeError::InvalidInternallyTaggedEnum); + assert_ts!(D, "({ type: \"A\" } & { [key in string]: string })"); + assert_ts!(E, "({ type: \"A\" })"); + assert_ts!(F, "({ type: \"A\" } & FInner)"); + assert_ts!(error; G, SerdeError::InvalidInternallyTaggedEnum); + assert_ts!(H, "({ type: \"A\" } & HInner)"); + assert_ts!(error; I, SerdeError::InvalidInternallyTaggedEnum); + assert_ts!(L, "({ type: \"A\" } & ({ type: \"A\" } | { type: \"B\" }))"); + assert_ts!(M, "({ type: \"A\" })"); +} diff --git a/tests/serde/mod.rs b/tests/serde/mod.rs new file mode 100644 index 0000000..5b72ed8 --- /dev/null +++ b/tests/serde/mod.rs @@ -0,0 +1,4 @@ +mod adjacently_tagged; +mod externally_tagged; +mod internally_tagged; +mod untagged; diff --git a/tests/serde/untagged.rs b/tests/serde/untagged.rs new file mode 100644 index 0000000..2daf954 --- /dev/null +++ b/tests/serde/untagged.rs @@ -0,0 +1,26 @@ +use specta::Type; + +use crate::ts::assert_ts; + +#[derive(Type)] +#[specta(export = false, untagged)] +enum A { + A { id: String }, + C(String), + D(String, String), +} + +#[derive(Type)] +#[serde(export = false, untagged)] +pub enum B { + A, + B, +} + +#[test] +fn untagged() { + // There is not way to construct an invalid untagged type. + + assert_ts!(A, "{ id: string } | string | [string, string]"); + assert_ts!(B, "null") +} diff --git a/tests/transparent.rs b/tests/transparent.rs new file mode 100644 index 0000000..40ace3e --- /dev/null +++ b/tests/transparent.rs @@ -0,0 +1,72 @@ +use specta::{DataType, DefOpts, PrimitiveType, Type}; + +use crate::ts::assert_ts; + +#[derive(Type)] +#[specta(export = false, transparent)] +struct TupleStruct(String); + +#[repr(transparent)] +#[derive(Type)] +#[specta(export = false)] +struct TupleStructWithRep(String); + +#[derive(Type)] +#[specta(export = false, transparent)] +struct GenericTupleStruct(T); + +#[derive(Type)] +#[specta(export = false, transparent)] +pub struct BracedStruct { + a: String, +} + +#[test] +fn transparent() { + // We check the datatype layer can TS can look correct but be wrong! + assert_eq!( + TupleStruct::inline( + DefOpts { + parent_inline: false, + type_map: &mut Default::default(), + }, + &[] + ), + DataType::Primitive(PrimitiveType::String) + ); + assert_eq!( + TupleStructWithRep::inline( + DefOpts { + parent_inline: false, + type_map: &mut Default::default(), + }, + &[] + ), + DataType::Primitive(PrimitiveType::String) + ); + assert_eq!( + GenericTupleStruct::::inline( + DefOpts { + parent_inline: false, + type_map: &mut Default::default(), + }, + &[] + ), + DataType::Primitive(PrimitiveType::String) + ); + assert_eq!( + BracedStruct::inline( + DefOpts { + parent_inline: false, + type_map: &mut Default::default(), + }, + &[] + ), + DataType::Primitive(PrimitiveType::String) + ); + + assert_ts!(TupleStruct, "string"); + assert_ts!(TupleStructWithRep, "string"); + assert_ts!(GenericTupleStruct::, "string"); + assert_ts!(BracedStruct, "string"); +} diff --git a/tests/ts.rs b/tests/ts.rs index b70b91c..9516147 100644 --- a/tests/ts.rs +++ b/tests/ts.rs @@ -16,6 +16,12 @@ use specta::{ }; macro_rules! assert_ts { + (error; $t:ty, $e:expr) => { + assert_eq!( + specta::ts::inline::<$t>(&Default::default()), + Err($e.into()) + ) + }; ($t:ty, $e:expr) => { assert_eq!(specta::ts::inline::<$t>(&Default::default()), Ok($e.into())) }; @@ -35,9 +41,18 @@ macro_rules! assert_ts_export { ($t:ty, $e:expr) => { assert_eq!(specta::ts::export::<$t>(&Default::default()), Ok($e.into())) }; + (error; $t:ty, $e:expr) => { + assert_eq!( + specta::ts::export::<$t>(&Default::default()), + Err($e.into()) + ) + }; ($t:ty, $e:expr; $cfg:expr) => { assert_eq!(specta::ts::export::<$t>($cfg), Ok($e.into())) }; + (error; $t:ty, $e:expr; $cfg:expr) => { + assert_eq!(specta::ts::export::<$t>($cfg), Err($e.into())) + }; } pub(crate) use assert_ts_export; @@ -122,7 +137,7 @@ fn typescript_types() { ); assert_ts!(OverridenStruct, "{ overriden_field: string }"); - assert_ts!(HasGenericAlias, r#"{ [key: number]: string }"#); + assert_ts!(HasGenericAlias, r#"{ [key in number]: string }"#); assert_ts!(SkipVariant, "{ A: string }"); assert_ts!(SkipVariant2, r#"{ tag: "A"; data: string }"#); @@ -152,7 +167,7 @@ fn typescript_types() { assert_ts!(Rename, r#""OneWord" | "Two words""#); - assert_ts!(TransparentType, r#"TransparentTypeInner"#); + assert_ts!(TransparentType, r#"TransparentTypeInner"#); // TODO: I don't think this is correct for `Type::inline` assert_ts!(TransparentType2, r#"null"#); assert_ts!(TransparentTypeWithOverride, r#"string"#); diff --git a/tests/ts_rs/generics.rs b/tests/ts_rs/generics.rs index eb3311d..416d1b7 100644 --- a/tests/ts_rs/generics.rs +++ b/tests/ts_rs/generics.rs @@ -56,7 +56,7 @@ fn test() { assert_ts_export!( Container1, - "export type Container1 = { foo: Generic1; bar: Generic1[]; baz: { [key: string]: Generic1 } }" + "export type Container1 = { foo: Generic1; bar: Generic1[]; baz: { [key in string]: Generic1 } }" ); } diff --git a/tests/ts_rs/indexmap.rs b/tests/ts_rs/indexmap.rs index ba3da68..31f6ed6 100644 --- a/tests/ts_rs/indexmap.rs +++ b/tests/ts_rs/indexmap.rs @@ -17,6 +17,6 @@ fn indexmap() { assert_ts!( Indexes, - "{ map: { [key: string]: string }; indexset: string[] }" + "{ map: { [key in string]: string }; indexset: string[] }" ); }