Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions specta-zod/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use std::{borrow::Cow, fmt};

use crate::{export_config::ExportConfig, ImplLocation};
use crate::Zod;

#[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) cfg: &'a Zod,
pub(crate) path: Vec<PathItem>,
// `false` when inline'ing and `true` when exporting as named.
pub(crate) is_export: bool,
Expand Down Expand Up @@ -42,15 +41,13 @@ 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,
});

if let Some(next) = path.peek() {
s.push_str(match next {
PathItem::Type(_) => " -> ",
PathItem::TypeExtended(_, _) => " -> ",
PathItem::Field(_) => ".",
PathItem::Variant(_) => "::",
});
Expand Down
27 changes: 13 additions & 14 deletions specta-zod/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use core::fmt;
use std::borrow::Cow;
use std::{borrow::Cow, panic::Location};

use specta_serde::SerdeError;
use thiserror::Error;

use crate::{context::ExportPath, ImplLocation};
use crate::context::ExportPath;

/// Describes where an error occurred.
#[derive(Error, Debug, PartialEq)]
Expand All @@ -24,16 +23,14 @@ impl fmt::Display for NamedLocation {
}
}

/// The error type for the TypeScript exporter.
/// The error type for the Zod exporter.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ExportError {
pub enum Error {
#[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),
Serde(#[from] specta_serde::Error),
#[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")]
Expand All @@ -42,21 +39,23 @@ pub enum ExportError {
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("Unable to export type named '{name}' from multiple locations")]
DuplicateTypeName {
name: Cow<'static, str>,
types: (Location<'static>, Location<'static>),
},
#[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 {
impl PartialEq for Error {
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
}
Expand All @@ -68,8 +67,8 @@ impl PartialEq for ExportError {
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::DuplicateTypeName { name: l0, types: l1 }, Self::DuplicateTypeName { name: r0, types: r1 }) => {
l0 == r0 && l1 == r1
}
(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,
Expand Down
95 changes: 7 additions & 88 deletions specta-zod/src/export_config.rs
Original file line number Diff line number Diff line change
@@ -1,103 +1,22 @@
use std::{borrow::Cow, io, path::PathBuf};

use specta_typescript::{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).
/// Allows you to configure how Specta's Zod 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)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BigIntExportBehavior {
/// Export BigInt as a Typescript `string`
/// Export BigInt as a Zod `z.string()`
///
/// Doing this in serde is [pretty simple](https://github.com/serde-rs/json/issues/329#issuecomment-305608405).
String,
/// Export BigInt as a Typescript `number`.
/// Export BigInt as a Zod `z.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`.
/// Export BigInt as a Zod `z.bigint()`.
///
/// You must ensure your deserializer is able to support this.
BigInt,
/// Abort the export with an error.
///
Expand Down
Loading
Loading