1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
use std::{
borrow::Cow,
io,
path::{Path, PathBuf},
};
use specta::{datatype::DeprecatedType, Language, TypeMap};
use specta_serde::is_valid_ty;
use crate::{comments, detect_duplicate_type_names, export_named_datatype, ExportError};
#[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(&Path) -> io::Result<()>;
/// 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),
}
/// Typescript language exporter.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Typescript {
/// The file's header
pub header: Cow<'static, str>,
/// Should we remove the default header?
pub remove_default_header: bool,
/// How BigInts should be exported.
pub bigint: BigIntExportBehavior,
/// How comments should be rendered.
pub comment_exporter: Option<CommentFormatterFn>,
/// How the resulting file should be formatted.
pub formatter: Option<FormatterFn>,
}
impl Default for Typescript {
fn default() -> Self {
Self {
header: Cow::Borrowed(""),
remove_default_header: false,
bigint: Default::default(),
comment_exporter: Some(comments::js_doc),
formatter: None,
}
}
}
impl Typescript {
/// Construct a new Typescript exporter with the default options configured.
pub fn new() -> Self {
Default::default()
}
/// Configure a header for the file.
///
/// This is perfect for configuring lint ignore rules or other file-level comments.
pub fn header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
self.header = header.into();
self
}
// TODO: Only keep this is TS stays responsible for exporting which it probs won't.
/// Removes the default Specta header from the output.
pub fn remove_default_header(mut self) -> Self {
self.remove_default_header = true;
self
}
/// 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`](specta_typescript::js_doc)
///
/// Not calling this method will default to the [`js_doc`](specta_typescript::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: CommentFormatterFn) -> Self {
self.comment_exporter = Some(exporter);
self
}
/// Configure a function which is responsible for formatting the result file or files
///
///
/// Built-in implementations:
/// - [`prettier`](crate:formatter:::prettier)
/// - [`ESLint`](crate::formatter::eslint)
/// - [`Biome`](crate::formatter::biome)e
pub fn formatter(mut self, formatter: FormatterFn) -> Self {
self.formatter = Some(formatter);
self
}
}
impl Language for Typescript {
type Error = ExportError;
fn export(&self, type_map: TypeMap) -> Result<String, Self::Error> {
let mut out = self.header.to_string();
if !self.remove_default_header {
out += "// This file has been generated by Specta. DO NOT EDIT.\n\n";
}
if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() {
return Err(ExportError::DuplicateTypeName(ty_name, l0, l1));
}
for (_, ty) in type_map.iter() {
is_valid_ty(&ty.inner, &type_map)?;
out += &export_named_datatype(self, ty, &type_map)?;
out += "\n\n";
}
Ok(out)
}
fn format(&self, path: &Path) -> Result<(), Self::Error> {
if let Some(formatter) = self.formatter {
formatter(path)?;
}
Ok(())
}
}