-
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This was moved from mattrax/Mattrax repository and was originally written by Brendan. Co-authored-by: Brendan Allan <[email protected]>
- Loading branch information
1 parent
fadef54
commit d1bfae8
Showing
6 changed files
with
1,770 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "specta-zod" | ||
description = "Export your Rust types to Zod schemas for TypeScript" | ||
version = "0.0.1" | ||
authors = ["Oscar Beaumont <[email protected]>"] | ||
edition = "2021" | ||
license = "MIT" | ||
repository = "https://github.com/oscartbeaumont/specta" | ||
documentation = "https://docs.rs/specta-zod/latest/specta-zod" | ||
keywords = ["async", "specta", "rspc", "typescript", "typesafe"] | ||
categories = ["web-programming", "asynchronous"] | ||
|
||
[dependencies] | ||
specta = { path = "../../", features = ["typescript"] } | ||
thiserror = "1.0.60" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
use std::{borrow::Cow, fmt}; | ||
|
||
use crate::{export_config::ExportConfig, ImplLocation}; | ||
|
||
#[derive(Clone, Debug)] | ||
pub(crate) enum PathItem { | ||
Type(Cow<'static, str>), | ||
TypeExtended(Cow<'static, str>, ImplLocation), | ||
Field(Cow<'static, str>), | ||
Variant(Cow<'static, str>), | ||
} | ||
|
||
#[derive(Clone)] | ||
pub(crate) struct ExportContext<'a> { | ||
pub(crate) cfg: &'a ExportConfig, | ||
pub(crate) path: Vec<PathItem>, | ||
// `false` when inline'ing and `true` when exporting as named. | ||
pub(crate) is_export: bool, | ||
} | ||
|
||
impl ExportContext<'_> { | ||
pub(crate) fn with(&self, item: PathItem) -> Self { | ||
Self { | ||
path: self.path.iter().cloned().chain([item]).collect(), | ||
..*self | ||
} | ||
} | ||
|
||
pub(crate) fn export_path(&self) -> ExportPath { | ||
ExportPath::new(&self.path) | ||
} | ||
} | ||
|
||
/// Represents the path of an error in the export tree. | ||
/// This is designed to be opaque, meaning it's internal format and `Display` impl are subject to change at will. | ||
pub struct ExportPath(String); | ||
|
||
impl ExportPath { | ||
pub(crate) fn new(path: &[PathItem]) -> Self { | ||
let mut s = String::new(); | ||
let mut path = path.iter().peekable(); | ||
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, | ||
}); | ||
|
||
if let Some(next) = path.peek() { | ||
s.push_str(match next { | ||
PathItem::Type(_) => " -> ", | ||
PathItem::TypeExtended(_, _) => " -> ", | ||
PathItem::Field(_) => ".", | ||
PathItem::Variant(_) => "::", | ||
}); | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
Self(s) | ||
} | ||
|
||
#[doc(hidden)] | ||
pub fn new_unsafe(path: &str) -> Self { | ||
Self(path.to_string()) | ||
} | ||
} | ||
|
||
impl PartialEq for ExportPath { | ||
fn eq(&self, other: &Self) -> bool { | ||
self.0 == other.0 | ||
} | ||
} | ||
|
||
impl fmt::Debug for ExportPath { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
f.write_str(&self.0) | ||
} | ||
} | ||
|
||
impl fmt::Display for ExportPath { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", self.0) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
use core::fmt; | ||
use std::borrow::Cow; | ||
|
||
use thiserror::Error; | ||
|
||
use crate::{context::ExportPath, ImplLocation, SerdeError}; | ||
|
||
/// Describes where an error occurred. | ||
#[derive(Error, Debug, PartialEq)] | ||
pub enum NamedLocation { | ||
Type, | ||
Field, | ||
Variant, | ||
} | ||
|
||
impl fmt::Display for NamedLocation { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
Self::Type => write!(f, "type"), | ||
Self::Field => write!(f, "field"), | ||
Self::Variant => write!(f, "variant"), | ||
} | ||
} | ||
} | ||
|
||
/// The error type for the TypeScript exporter. | ||
#[derive(Error, Debug)] | ||
#[non_exhaustive] | ||
pub enum ExportError { | ||
#[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("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 '{1}' but was unable to due to {0} name '{2}' containing an invalid character")] | ||
InvalidName(NamedLocation, ExportPath, String), | ||
#[error("Attempted to export '{0}' with tagging but the type is not tagged.")] | ||
InvalidTagging(ExportPath), | ||
#[error("Attempted to export '{0}' with internal tagging but the variant is a tuple struct.")] | ||
InvalidTaggedVariantContainingTupleStruct(ExportPath), | ||
#[error("Unable to export type named '{0}' from locations '{:?}' '{:?}'", .1.as_str(), .2.as_str())] | ||
DuplicateTypeName(Cow<'static, str>, ImplLocation, ImplLocation), | ||
#[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 ExportError { | ||
fn eq(&self, other: &Self) -> bool { | ||
match (self, other) { | ||
(Self::BigIntForbidden(l0), Self::BigIntForbidden(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 | ||
} | ||
(Self::InvalidName(l0, l1, l2), Self::InvalidName(r0, r1, r2)) => { | ||
l0 == r0 && l1 == r1 && l2 == r2 | ||
} | ||
(Self::InvalidTagging(l0), Self::InvalidTagging(r0)) => l0 == r0, | ||
( | ||
Self::InvalidTaggedVariantContainingTupleStruct(l0), | ||
Self::InvalidTaggedVariantContainingTupleStruct(r0), | ||
) => l0 == r0, | ||
(Self::DuplicateTypeName(l0, l1, l2), Self::DuplicateTypeName(r0, r1, r2)) => { | ||
l0 == r0 && l1 == r1 && l2 == r2 | ||
} | ||
(Self::Io(l0), Self::Io(r0)) => l0.to_string() == r0.to_string(), // This is a bit hacky but it will be fine for usage in unit tests! | ||
(Self::Other(l0, l1), Self::Other(r0, r1)) => l0 == r0 && l1 == r1, | ||
_ => false, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
use std::{borrow::Cow, io, path::PathBuf}; | ||
|
||
use specta::ts::{comments, CommentFormatterFn}; | ||
|
||
use crate::DeprecatedType; | ||
|
||
#[derive(Debug)] | ||
#[non_exhaustive] | ||
pub struct CommentFormatterArgs<'a> { | ||
pub docs: &'a Cow<'static, str>, | ||
pub deprecated: Option<&'a DeprecatedType>, | ||
} | ||
|
||
/// The signature for a function responsible for exporting Typescript comments. | ||
// pub type CommentFormatterFn = fn(CommentFormatterArgs) -> String; // TODO: Returning `Cow`??? | ||
|
||
/// The signature for a function responsible for formatter a Typescript file. | ||
pub type FormatterFn = fn(PathBuf) -> io::Result<()>; | ||
|
||
/// Options for controlling the behavior of the Typescript exporter. | ||
#[derive(Debug, Clone)] | ||
pub struct ExportConfig { | ||
/// How BigInts should be exported. | ||
pub(crate) bigint: BigIntExportBehavior, | ||
/// How comments should be rendered. | ||
pub(crate) comment_exporter: Option<CommentFormatterFn>, | ||
/// How the resulting file should be formatted. | ||
pub(crate) formatter: Option<FormatterFn>, | ||
} | ||
|
||
impl ExportConfig { | ||
/// Construct a new `ExportConfiguration` | ||
pub fn new() -> Self { | ||
Default::default() | ||
} | ||
|
||
/// Configure the BigInt handling behaviour | ||
pub fn bigint(mut self, bigint: BigIntExportBehavior) -> Self { | ||
self.bigint = bigint; | ||
self | ||
} | ||
|
||
/// Configure a function which is responsible for styling the comments to be exported | ||
/// | ||
/// Implementations: | ||
/// - [`js_doc`](crate::lang::ts::js_doc) | ||
/// | ||
/// 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<CommentFormatterFn>) -> Self { | ||
self.comment_exporter = exporter; | ||
self | ||
} | ||
|
||
/// Configure a function which is responsible for formatting the result file or files | ||
/// | ||
/// | ||
/// Implementations: | ||
/// - [`prettier`](crate::lang::ts::prettier) | ||
/// - [`ESLint`](crate::lang::ts::eslint) | ||
pub fn formatter(mut self, formatter: FormatterFn) -> Self { | ||
self.formatter = Some(formatter); | ||
self | ||
} | ||
|
||
/// Run the specified formatter on the given path. | ||
pub fn run_format(&self, path: PathBuf) -> io::Result<()> { | ||
if let Some(formatter) = self.formatter { | ||
formatter(path)?; | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl Default for ExportConfig { | ||
fn default() -> Self { | ||
Self { | ||
bigint: Default::default(), | ||
comment_exporter: Some(comments::js_doc), | ||
formatter: None, | ||
} | ||
} | ||
} | ||
|
||
/// Allows you to configure how Specta's Typescript exporter will deal with BigInt types ([i64], [i128] etc). | ||
/// | ||
/// WARNING: None of these settings affect how your data is actually ser/deserialized. | ||
/// It's up to you to adjust your ser/deserialize settings. | ||
#[derive(Debug, Clone, Default)] | ||
pub enum BigIntExportBehavior { | ||
/// Export BigInt as a Typescript `string` | ||
/// | ||
/// Doing this is serde is [pretty simple](https://github.com/serde-rs/json/issues/329#issuecomment-305608405). | ||
String, | ||
/// Export BigInt as a Typescript `number`. | ||
/// | ||
/// WARNING: `JSON.parse` in JS will truncate your number resulting in data loss so ensure your deserializer supports large numbers. | ||
Number, | ||
/// Export BigInt as a Typescript `BigInt`. | ||
BigInt, | ||
/// Abort the export with an error. | ||
/// | ||
/// This is the default behavior because without integration from your serializer and deserializer we can't guarantee data loss won't occur. | ||
#[default] | ||
Fail, | ||
/// Same as `Self::Fail` but it allows a library to configure the message shown to the end user. | ||
#[doc(hidden)] | ||
FailWithReason(&'static str), | ||
} |
Oops, something went wrong.