From dc2848820839c59590c16d5aca843988ee059054 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sat, 19 Aug 2023 11:22:15 +0800 Subject: [PATCH] wip --- examples/demo.rs | 15 +++ examples/demo2.rs | 11 ++ src/json.rs | 274 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 + src/static_types.rs | 42 ++++++- tests/json.rs | 20 ++++ tests/lib.rs | 1 + tests/ts.rs | 2 +- 8 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 examples/demo.rs create mode 100644 examples/demo2.rs create mode 100644 src/json.rs create mode 100644 tests/json.rs diff --git a/examples/demo.rs b/examples/demo.rs new file mode 100644 index 00000000..a17eaafc --- /dev/null +++ b/examples/demo.rs @@ -0,0 +1,15 @@ +use specta::ts::inline_ref; + +fn main() { + // let v = specta::json!(42u32); + + // println!("{:?}", serde_json::to_string(&v)); + // println!("{:?}", inline_ref(&v, &Default::default()).unwrap()); + + let v = specta::json!({ + "hello": "world" + }); + + println!("{:?}", serde_json::to_string(&v)); + println!("{:?}", inline_ref(&v, &Default::default()).unwrap()); +} diff --git a/examples/demo2.rs b/examples/demo2.rs new file mode 100644 index 00000000..86200d8e --- /dev/null +++ b/examples/demo2.rs @@ -0,0 +1,11 @@ +// macro_rules! demo { +// () => { +// a: (), +// }; +// } + +// pub struct Demo { +// // demo!() +// } + +fn main() {} diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 00000000..e4c8be0e --- /dev/null +++ b/src/json.rs @@ -0,0 +1,274 @@ +// TODO: Should this be called `specta::type` with an alias for `specta::json` cause it's not tied to JSON. + +// TODO: Remove dependency on `serde_json` + +#[macro_export(local_inner_macros)] +macro_rules! json { + // Hide distracting implementation details from the generated rustdoc. + ($($json:tt)+) => { + json_internal!($($json)+) + }; +} + +#[macro_export(local_inner_macros)] +#[doc(hidden)] +macro_rules! json_internal { + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an array [...]. Produces a vec![...] + // of the elements. + // + // Must be invoked as: json_internal!(@array [] $($tt)*) + ////////////////////////////////////////////////////////////////////////// + + // Done with trailing comma. + (@array [$($elems:expr,)*]) => { + json_internal_vec![$($elems,)*] + }; + + // Done without trailing comma. + (@array [$($elems:expr),*]) => { + json_internal_vec![$($elems),*] + }; + + // Next element is `null`. + (@array [$($elems:expr,)*] null $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*) + }; + + // Next element is `true`. + (@array [$($elems:expr,)*] true $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*) + }; + + // Next element is `false`. + (@array [$($elems:expr,)*] false $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*) + }; + + // Next element is an array. + (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*) + }; + + // Next element is a map. + (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*) + }; + + // Next element is an expression followed by comma. + (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*) + }; + + // Last element is an expression with no trailing comma. + (@array [$($elems:expr,)*] $last:expr) => { + json_internal!(@array [$($elems,)* json_internal!($last)]) + }; + + // Comma after the most recent element. + (@array [$($elems:expr),*] , $($rest:tt)*) => { + json_internal!(@array [$($elems,)*] $($rest)*) + }; + + // Unexpected token after most recent element. + (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => { + json_unexpected!($unexpected) + }; + + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an object {...}. Each entry is + // inserted into the given map variable. + // + // Must be invoked as: json_internal!(@object $map () ($($tt)*) ($($tt)*)) + // + // We require two copies of the input tokens so that we can match on one + // copy and trigger errors on the other copy. + ////////////////////////////////////////////////////////////////////////// + + // Done. + (@object $object:ident () () ()) => {}; + + // Insert the current entry followed by trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => { + let _ = $object.insert(($($key)+).into(), $value); + json_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Current entry followed by unexpected token. + (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => { + json_unexpected!($unexpected); + }; + + // Insert the last entry without trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr)) => { + let _ = $object.insert(($($key)+).into(), $value); + }; + + // Next value is `null`. + (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*); + }; + + // Next value is `true`. + (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*); + }; + + // Next value is `false`. + (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*); + }; + + // Next value is an array. + (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*); + }; + + // Next value is a map. + (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*); + }; + + // Next value is an expression followed by comma. + (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*); + }; + + // Last value is an expression with no trailing comma. + (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!($value))); + }; + + // Missing value for last entry. Trigger a reasonable error message. + (@object $object:ident ($($key:tt)+) (:) $copy:tt) => { + // "unexpected end of macro invocation" + json_internal!(); + }; + + // Missing colon and value for last entry. Trigger a reasonable error + // message. + (@object $object:ident ($($key:tt)+) () $copy:tt) => { + // "unexpected end of macro invocation" + json_internal!(); + }; + + // Misplaced colon. Trigger a reasonable error message. + (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => { + // Takes no arguments so "no rules expected the token `:`". + json_unexpected!($colon); + }; + + // Found a comma inside a key. Trigger a reasonable error message. + (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => { + // Takes no arguments so "no rules expected the token `,`". + json_unexpected!($comma); + }; + + // Key is fully parenthesized. This avoids clippy double_parens false + // positives because the parenthesization may be necessary here. + (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*)); + }; + + // Refuse to absorb colon token into key expression. + (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => { + json_expect_expr_comma!($($unexpected)+); + }; + + // Munch a token into the current key. + (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*)); + }; + + ////////////////////////////////////////////////////////////////////////// + // The main implementation. + // + // Must be invoked as: json_internal!($($json)+) + ////////////////////////////////////////////////////////////////////////// + (null) => { + () + }; + + (true) => { + $crate::True + }; + + (false) => { + $crate::False + }; + + ([]) => {{ + #[derive(serde::Serialize, $crate::Type)] + struct Anonymous(); + + Anonymous() + }}; + + ([ $($tt:tt)+ ]) => {{ + todo!(); + // $crate::Value::Array(json_internal!(@array [] $($tt)+))}; + }}; + + ({}) => {{ + #[derive(serde::Serialize, $crate::Type)] + struct Anonymous {}; + + Anonymous {} + }}; + + ({ $($tt:tt)+ }) => {{ + struct Anonymous; + + impl serde::Serialize for Anonymous { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serde_json::Value::Object({ + let mut object = serde_json::Map::new(); + json_internal!(@object object () ($($tt)+) ($($tt)+)); + object + }).serialize(serializer) + } + } + + impl $crate::Type for Anonymous { + fn inline( + opts: $crate::DefOpts, + generics: &[$crate::DataType], + ) -> Result<$crate::DataType, $crate::ExportError> { + <() as $crate::Type>::inline(opts, generics) + } + } + + + Anonymous {} + }}; + + // Any Serialize type: numbers, strings, struct literals, variables etc. + // Must be below every other rule. + ($other:expr) => {{ + serde_json::to_value(&$other).unwrap() + }}; + + // Any Serialize type: numbers, strings, struct literals, variables etc. + // Must be below every other rule. + (@ty $ty:expr) => {{ + fn infer(t: &T) -> Result<$crate::DataType, $crate::ExportError> { + T::inline(Default::default(), &[]); + } + + infer(&$ty) + }}; + + // TODO: Remove + ($($tt:tt)+) => {{ + todo!(); + }}; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! json_unexpected { + () => {}; +} diff --git a/src/lib.rs b/src/lib.rs index 590bb5fb..84c751d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,7 @@ pub mod export; #[cfg(feature = "functions")] #[cfg_attr(docsrs, doc(cfg(feature = "functions")))] pub mod functions; +mod json; mod lang; mod selection; mod static_types; @@ -81,6 +82,10 @@ pub use r#type::*; pub use selection::*; pub use static_types::*; +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +pub use json::*; + /// Implements [`Type`] for a given struct or enum. /// /// ## Example diff --git a/src/static_types.rs b/src/static_types.rs index f24132f6..269e4a8a 100644 --- a/src/static_types.rs +++ b/src/static_types.rs @@ -1,6 +1,6 @@ -use crate::{DataType, DefOpts, ExportError, Type}; +use crate::{DataType, DefOpts, ExportError, LiteralType, Type}; -/// A type that is unconstructable but is typed as `any` in TypeScript. +/// A type that is typed as `any` in TypeScript. /// /// This can be use like the following: /// ```rust @@ -13,10 +13,46 @@ use crate::{DataType, DefOpts, ExportError, Type}; /// pub field: String, /// } /// ``` -pub enum Any {} +pub struct Any {} impl Type for Any { fn inline(_: DefOpts, _: &[DataType]) -> Result { Ok(DataType::Any) } } + +pub struct True; + +impl Type for True { + fn inline(_: DefOpts, _: &[DataType]) -> Result { + Ok(DataType::Literal(LiteralType::bool(true))) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for True { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + true.serialize(serializer) + } +} + +pub struct False; + +impl Type for False { + fn inline(_: DefOpts, _: &[DataType]) -> Result { + Ok(DataType::Literal(LiteralType::bool(false))) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for False { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + false.serialize(serializer) + } +} diff --git a/tests/json.rs b/tests/json.rs new file mode 100644 index 00000000..f13d654f --- /dev/null +++ b/tests/json.rs @@ -0,0 +1,20 @@ +use crate::ts::assert_ts; + +#[test] +#[cfg(feature = "serde")] +fn test_json() { + use specta::{json, True}; + + assert_ts!(() => json!(null), "null"); + assert_ts!(() => json!(true), "true"); + assert_ts!(() => json!(false), "false"); + + assert_ts!(() => json!({}), "Record"); + assert_ts!(() => json!({ "hello": "world" }), "{ hello: string }"); + // assert_ts!(() => json!({ + // "hello": "world", + // }), "{ hello: string }"); + + assert_ts!(() => json!([]), "[]"); + // assert_ts!(() => json!(["a", "b", "c"]), "string[]"); +} diff --git a/tests/lib.rs b/tests/lib.rs index 6b3356ac..3661fa04 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,6 +6,7 @@ mod datatype; mod duplicate_ty_name; mod export; mod functions; +mod json; mod macro_decls; mod reserved_keywords; mod selection; diff --git a/tests/ts.rs b/tests/ts.rs index b70b91ca..8b1175df 100644 --- a/tests/ts.rs +++ b/tests/ts.rs @@ -22,7 +22,7 @@ macro_rules! assert_ts { (() => $expr:expr, $e:expr) => { let _: () = { - fn assert_ty_eq(_t: T) { + fn assert_ty_eq(_t: T) { assert_eq!(specta::ts::inline::(&Default::default()), Ok($e.into())); } assert_ty_eq($expr);