From 72c8ee7287652eb42acb8698fa14364fcc1ec786 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 23 Nov 2023 12:24:12 +0900 Subject: [PATCH] fixes #180 --- macros/src/data_type_from/mod.rs | 2 +- macros/src/type/enum.rs | 2 +- macros/src/type/struct.rs | 2 +- src/datatype/enum.rs | 7 ++++++- src/datatype/mod.rs | 1 + src/datatype/struct.rs | 4 +++- src/internal.rs | 4 ++++ src/lang/ts/context.rs | 5 +++++ src/lang/ts/mod.rs | 17 ++++++++++++++--- src/type/impls.rs | 6 +++++- src/type/map.rs | 11 +++++++++-- tests/remote_impls.rs | 19 +++++++++++++++++-- tests/reserved_keywords.rs | 6 +++--- tests/ts.rs | 4 ++-- 14 files changed, 72 insertions(+), 18 deletions(-) diff --git a/macros/src/data_type_from/mod.rs b/macros/src/data_type_from/mod.rs index 2011977..301bef7 100644 --- a/macros/src/data_type_from/mod.rs +++ b/macros/src/data_type_from/mod.rs @@ -65,7 +65,7 @@ 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(#struct_name.into(), vec![], #crate_ref::internal::construct::struct_named(vec![#(#fields),*], None)) + #crate_ref::internal::construct::r#struct(#struct_name.into(), None, vec![], #crate_ref::internal::construct::struct_named(vec![#(#fields),*], None)) } } diff --git a/macros/src/type/enum.rs b/macros/src/type/enum.rs index 815152b..3cd492a 100644 --- a/macros/src/type/enum.rs +++ b/macros/src/type/enum.rs @@ -243,7 +243,7 @@ pub fn parse_enum( let skip_bigint_checs = enum_attrs.unstable_skip_bigint_checks; Ok(( - quote!(#crate_ref::DataType::Enum(#crate_ref::internal::construct::r#enum(#name.into(), #repr, #skip_bigint_checs, vec![#(#definition_generics),*], vec![#(#variant_types),*]))), + quote!(#crate_ref::DataType::Enum(#crate_ref::internal::construct::r#enum(#name.into(), SID, #repr, #skip_bigint_checs, vec![#(#definition_generics),*], vec![#(#variant_types),*]))), quote!({ let generics = vec![#(#reference_generics),*]; #crate_ref::reference::reference::(opts, #crate_ref::internal::construct::data_type_reference( diff --git a/macros/src/type/struct.rs b/macros/src/type/struct.rs index 163769c..0989915 100644 --- a/macros/src/type/struct.rs +++ b/macros/src/type/struct.rs @@ -280,7 +280,7 @@ pub fn parse_struct( 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))) + quote!(#crate_ref::DataType::Struct(#crate_ref::internal::construct::r#struct(#name.into(), Some(SID), vec![#(#definition_generics),*], #fields))) }; let category = if container_attrs.inline { diff --git a/src/datatype/enum.rs b/src/datatype/enum.rs index ce2c80f..2f4fe55 100644 --- a/src/datatype/enum.rs +++ b/src/datatype/enum.rs @@ -1,7 +1,8 @@ use std::borrow::Cow; use crate::{ - datatype::DataType, DeprecatedType, GenericType, NamedDataType, NamedFields, UnnamedFields, + datatype::DataType, DeprecatedType, GenericType, NamedDataType, NamedFields, SpectaID, + UnnamedFields, }; /// Enum type which dictates how the enum is represented. @@ -13,6 +14,10 @@ use crate::{ #[derive(Debug, Clone, PartialEq)] pub struct EnumType { pub(crate) name: Cow<'static, str>, + // Associating a SpectaID will allow exporter to lookup more detailed information about the type to provide better errors. + pub(crate) sid: Option, + // This is used to allow `serde_json::Number` and `toml::Value` to contain BigInt numbers without an error. + // I don't know if we should block bigints in these any types. Really I think we should but we need a good DX around overriding it on a per-type basis. pub(crate) skip_bigint_checks: bool, pub(crate) repr: EnumRepr, pub(crate) generics: Vec, diff --git a/src/datatype/mod.rs b/src/datatype/mod.rs index 0165fe4..6b45440 100644 --- a/src/datatype/mod.rs +++ b/src/datatype/mod.rs @@ -161,6 +161,7 @@ impl + 'static> From> for DataType { fn from(t: Vec) -> Self { DataType::Enum(EnumType { name: "Vec".into(), + sid: None, repr: EnumRepr::Untagged, skip_bigint_checks: false, variants: t diff --git a/src/datatype/struct.rs b/src/datatype/struct.rs index e5979cb..ee03faf 100644 --- a/src/datatype/struct.rs +++ b/src/datatype/struct.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::{DataType, GenericType, NamedDataType, NamedFields, UnnamedFields}; +use crate::{DataType, GenericType, NamedDataType, NamedFields, SpectaID, UnnamedFields}; #[derive(Debug, Clone, PartialEq)] pub enum StructFields { @@ -21,6 +21,8 @@ pub enum StructFields { #[derive(Debug, Clone, PartialEq)] pub struct StructType { pub(crate) name: Cow<'static, str>, + // Associating a SpectaID will allow exporter to lookup more detailed information about the type to provide better errors. + pub(crate) sid: Option, pub(crate) generics: Vec, pub(crate) fields: StructFields, } diff --git a/src/internal.rs b/src/internal.rs index 6093eea..65b61a3 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -40,11 +40,13 @@ pub mod construct { pub const fn r#struct( name: Cow<'static, str>, + sid: Option, generics: Vec, fields: StructFields, ) -> StructType { StructType { name, + sid, generics, fields, } @@ -67,6 +69,7 @@ pub mod construct { pub const fn r#enum( name: Cow<'static, str>, + sid: SpectaID, repr: EnumRepr, skip_bigint_checks: bool, generics: Vec, @@ -74,6 +77,7 @@ pub mod construct { ) -> EnumType { EnumType { name, + sid: Some(sid), repr, skip_bigint_checks, generics, diff --git a/src/lang/ts/context.rs b/src/lang/ts/context.rs index d417571..fa5251f 100644 --- a/src/lang/ts/context.rs +++ b/src/lang/ts/context.rs @@ -1,10 +1,13 @@ use std::{borrow::Cow, fmt}; +use crate::ImplLocation; + use super::ExportConfig; #[derive(Clone, Debug)] pub(crate) enum PathItem { Type(Cow<'static, str>), + TypeExtended(Cow<'static, str>, ImplLocation), Field(Cow<'static, str>), Variant(Cow<'static, str>), } @@ -41,6 +44,7 @@ impl ExportPath { while let Some(item) = path.next() { s.push_str(match item { PathItem::Type(v) => v, + PathItem::TypeExtended(_, loc) => loc.as_str(), PathItem::Field(v) => v, PathItem::Variant(v) => v, }); @@ -48,6 +52,7 @@ impl ExportPath { if let Some(next) = path.peek() { s.push_str(match next { PathItem::Type(_) => " -> ", + PathItem::TypeExtended(_, _) => " -> ", PathItem::Field(_) => ".", PathItem::Variant(_) => "::", }); diff --git a/src/lang/ts/mod.rs b/src/lang/ts/mod.rs index cd0f395..511caa1 100644 --- a/src/lang/ts/mod.rs +++ b/src/lang/ts/mod.rs @@ -130,6 +130,7 @@ fn export_datatype_inner( ctx: ExportContext, typ @ NamedDataType { name, + ext, docs, deprecated, inner: item, @@ -137,7 +138,11 @@ fn export_datatype_inner( }: &NamedDataType, type_map: &TypeMap, ) -> Output { - let ctx = ctx.with(PathItem::Type(name.clone())); + let ctx = ctx.with( + ext.clone() + .map(|v| PathItem::TypeExtended(name.clone(), v.impl_location)) + .unwrap_or_else(|| PathItem::Type(name.clone())), + ); let name = sanitise_type_name(ctx.clone(), NamedLocation::Type, name)?; let generics = item @@ -187,7 +192,7 @@ pub(crate) fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &Type BigIntExportBehavior::Number => NUMBER.into(), BigIntExportBehavior::BigInt => BIGINT.into(), BigIntExportBehavior::Fail => { - return Err(ExportError::BigIntForbidden(ctx.export_path())) + return Err(ExportError::BigIntForbidden(ctx.export_path())); } BigIntExportBehavior::FailWithReason(reason) => { return Err(ExportError::Other(ctx.export_path(), reason.to_owned())) @@ -241,7 +246,13 @@ pub(crate) fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &Type } } DataType::Struct(item) => struct_datatype( - ctx.with(PathItem::Type(item.name().clone())), + ctx.with( + item.sid + .and_then(|sid| type_map.get(sid)) + .and_then(|v| v.ext()) + .map(|v| PathItem::TypeExtended(item.name().clone(), v.impl_location)) + .unwrap_or_else(|| PathItem::Type(item.name().clone())), + ), item.name(), item, type_map, diff --git a/src/type/impls.rs b/src/type/impls.rs index 08105e4..8770eb2 100644 --- a/src/type/impls.rs +++ b/src/type/impls.rs @@ -213,6 +213,7 @@ impl Type for std::ops::Range { let ty = Some(T::definition(opts)); DataType::Struct(StructType { name: "Range".into(), + sid: None, generics: vec![], fields: StructFields::Named(NamedFields { fields: vec![ @@ -299,6 +300,7 @@ const _: () = { fn inline(_: DefOpts, _: &[DataType]) -> DataType { DataType::Enum(EnumType { name: "Number".into(), + sid: None, repr: EnumRepr::Untagged, skip_bigint_checks: true, variants: vec![ @@ -396,6 +398,7 @@ const _: () = { fn inline(_: DefOpts, _: &[DataType]) -> DataType { DataType::Enum(EnumType { name: "Number".into(), + sid: None, repr: EnumRepr::Untagged, skip_bigint_checks: true, variants: vec![ @@ -669,6 +672,7 @@ impl Type for either::Either { fn inline(opts: DefOpts, generics: &[DataType]) -> DataType { DataType::Enum(EnumType { name: "Either".into(), + sid: None, repr: EnumRepr::Untagged, skip_bigint_checks: false, variants: vec![ @@ -727,7 +731,7 @@ impl Type for either::Either { #[cfg(feature = "bevy_ecs")] const _: () = { #[derive(Type)] - #[specta(rename = "bevy_ecs::entity::Entity", remote = bevy_ecs::entity::Entity, crate = crate, export = false)] + #[specta(rename = "Entity", remote = bevy_ecs::entity::Entity, crate = crate, export = false)] #[allow(dead_code)] struct EntityDef(u64); }; diff --git a/src/type/map.rs b/src/type/map.rs index a622c3a..0e593fb 100644 --- a/src/type/map.rs +++ b/src/type/map.rs @@ -1,15 +1,22 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt}; use crate::{NamedDataType, SpectaID}; /// A map used to store the types "discovered" while exporting a type. -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Default, Clone, PartialEq)] pub struct TypeMap { // `None` indicates that the entry is a placeholder. It was reference and we are currently working out it's definition. pub(crate) map: BTreeMap>, + // A stack of types that are currently being flattened. This is used to detect cycles. pub(crate) flatten_stack: Vec, } +impl fmt::Debug for TypeMap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("TypeMap").field(&self.map).finish() + } +} + impl TypeMap { #[track_caller] pub fn get(&self, sid: SpectaID) -> Option<&NamedDataType> { diff --git a/tests/remote_impls.rs b/tests/remote_impls.rs index 8267bf5..0ca6659 100644 --- a/tests/remote_impls.rs +++ b/tests/remote_impls.rs @@ -35,9 +35,24 @@ fn typescript_types_glam() { #[test] #[cfg(feature = "bevy_ecs")] fn typescript_types_bevy_ecs() { - use specta::ts::ExportPath; + use specta::ts::{self, BigIntExportBehavior, ExportConfig, ExportPath}; use crate::ts::assert_ts; - assert_ts!(error; bevy_ecs::entity::Entity, specta::ts::ExportError::BigIntForbidden(ExportPath::new_unsafe("bevy_ecs::entity::Entity -> u64"))); + assert_eq!( + ts::inline::( + &ExportConfig::default().bigint(BigIntExportBehavior::Number) + ), + Ok("number".into()) + ); + // TODO: As we inline `Entity` never ends up in the type map so it falls back to "Entity" in the error instead of the path to the type. Is this what we want or not? + assert_ts!(error; bevy_ecs::entity::Entity, specta::ts::ExportError::BigIntForbidden(ExportPath::new_unsafe("Entity -> u64"))); + + // https://github.com/oscartbeaumont/specta/issues/161#issuecomment-1822735951 + assert_eq!( + ts::export::( + &ExportConfig::default().bigint(BigIntExportBehavior::Number) + ), + Ok("export Entity = number;".into()) + ); } diff --git a/tests/reserved_keywords.rs b/tests/reserved_keywords.rs index e4564d5..5d91111 100644 --- a/tests/reserved_keywords.rs +++ b/tests/reserved_keywords.rs @@ -43,7 +43,7 @@ fn test_ts_reserved_keyworks() { specta::ts::export::(&ExportConfig::default()), Err(ExportError::ForbiddenName( NamedLocation::Type, - ExportPath::new_unsafe("enum"), + ExportPath::new_unsafe("tests/reserved_keywords.rs:10:14"), "enum" )) ); @@ -51,7 +51,7 @@ fn test_ts_reserved_keyworks() { specta::ts::export::(&ExportConfig::default()), Err(ExportError::ForbiddenName( NamedLocation::Type, - ExportPath::new_unsafe("enum"), + ExportPath::new_unsafe("tests/reserved_keywords.rs:22:14"), "enum" )) ); @@ -59,7 +59,7 @@ fn test_ts_reserved_keyworks() { specta::ts::export::(&ExportConfig::default()), Err(ExportError::ForbiddenName( NamedLocation::Type, - ExportPath::new_unsafe("enum"), + ExportPath::new_unsafe("tests/reserved_keywords.rs:32:14"), "enum" )) ); diff --git a/tests/ts.rs b/tests/ts.rs index e4698ac..5084793 100644 --- a/tests/ts.rs +++ b/tests/ts.rs @@ -270,12 +270,12 @@ fn typescript_types() { assert_ts_export!( error; RenameWithWeirdCharsStruct, - ExportError::InvalidName(NamedLocation::Type, ExportPath::new_unsafe("@odata.context"), r#"@odata.context"#.to_string()) + ExportError::InvalidName(NamedLocation::Type, ExportPath::new_unsafe("tests/ts.rs:599:10"), r#"@odata.context"#.to_string()) ); assert_ts_export!( error; RenameWithWeirdCharsEnum, - ExportError::InvalidName(NamedLocation::Type, ExportPath::new_unsafe("@odata.context"), r#"@odata.context"#.to_string()) + ExportError::InvalidName(NamedLocation::Type, ExportPath::new_unsafe("tests/ts.rs:603:10"), r#"@odata.context"#.to_string()) ); // https://github.com/oscartbeaumont/specta/issues/156