Skip to content

Commit

Permalink
jsdoc typedefs (#141)
Browse files Browse the repository at this point in the history
* jsdoc typedefs

* correct default features

* fix tests

* make datatype available to js and ts

* clippy

* Move `js_doc` back to `ts`
It makes the public API sooo cringe.
Also `js_doc` depending on `typescript` as a feature is expected.

* Do you have a moment for our lord Clippy?

* Rename feature from `js` to `js_doc`

* No default `serde` feature

* Taplo config + Drop `indoc` and `const_format` deps

* future me problem

* Let's be extra explicit

* why da fuk did I put `BigIntExportBehavior` in `comments.fs` + qualify `ts::comments`

* nit

* potentially maybe kinda possibly fix trybuild

* remove empty_tuple_fallback

* remove all empty_tuple_fallback

* fix

* trybuild

* fix js doc generics

* fix trybuild output
Rust version mismatch lol

---------

Co-authored-by: Oscar Beaumont <[email protected]>
  • Loading branch information
Brendonovich and oscartbeaumont authored Sep 21, 2023
1 parent 298c904 commit b474b8e
Show file tree
Hide file tree
Showing 19 changed files with 230 additions and 209 deletions.
2 changes: 2 additions & 0 deletions .taplo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[formatting]
column_width = 150
73 changes: 24 additions & 49 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ name = "export"
required-features = ["export", "typescript"]

[features]
default = ["serde", "typescript"]
default = []

##! Internal Features
## Support for exporting the types of Rust functions.
Expand All @@ -49,7 +49,10 @@ tauri = ["dep:tauri"]

#! Languages
## Support for [TypeScript](https://www.typescriptlang.org) language exporting
typescript = []
typescript = ["serde"] # TODO: Probally don't keep `serde` here for v2???
## Support for [JSDoc](https://jsdoc.app) exporting helpers.
## Also requires `typescript` feature to be enabled.
js_doc = []
# ## Support for [Rust](https://www.rust-lang.org) language exporting
# rust = []
# ## Support for [Swift](https://www.swift.org/) language exporting
Expand Down Expand Up @@ -94,64 +97,36 @@ tokio = ["dep:tokio"]
url = ["dep:url"]
## [either](https://docs.rs/either) crate
either = ["dep:either"]
## [thiserror](https://docs.rs/thiserror) crate
thiserror = [
] # We use `thiserror` internally regardless but this feature makes changing that non-breaking.

[dependencies]
specta-macros = { version = "2.0.0-rc.2", path = "./macros" }
serde = { version = "1.0.183", optional = true, default-features = false, features = [
"derive",
] }
serde_json = { version = "1.0.104", optional = true, default-features = false, features = [
"std",
] }
serde_yaml = { version = "0.9.25", optional = true, default-features = false, features = [
] }
serde = { version = "1.0.183", optional = true, default-features = false, features = ["derive"] }
serde_json = { version = "1.0.104", optional = true, default-features = false, features = ["std"] }
serde_yaml = { version = "0.9.25", optional = true, default-features = false, features = [] }
toml = { version = "0.7.6", optional = true, default-features = false }
uuid = { version = "1.4.1", optional = true, default-features = false, features = [
] }
chrono = { version = "0.4.26", optional = true, default-features = false, features = [
"clock",
] }
time = { version = "0.3.25", optional = true, default-features = false, features = [
] }
bigdecimal = { version = "0.4.1", optional = true, default-features = false, features = [
] }
rust_decimal = { version = "1.31.0", optional = true, default-features = false, features = [
] }
indexmap = { version = "2.0.0", optional = true, default-features = false, features = [
] }
ipnetwork = { version = "0.20.0", optional = true, default-features = false, features = [
] }
mac_address = { version = "1.1.5", optional = true, default-features = false, features = [
] }
bit-vec = { version = "0.6.3", optional = true, default-features = false, features = [
] }
bson = { version = "2.6.1", optional = true, default-features = false, features = [
] }
openapiv3 = { version = "1.0.2", optional = true, default-features = false, features = [
] }
uhlc = { version = "0.6.0", optional = true, default-features = false, features = [
] }
tauri = { version = "1.4.1", optional = true, default-features = false, features = [
] }
bytesize = { version = "1.2.0", optional = true, default-features = false, features = [
] }
glam = { version = "0.24", optional = true, default-features = false, features = [
"std",
] }
tokio = { version = "1.30", optional = true, default-features = false, features = [
"sync",
] }
uuid = { version = "1.4.1", optional = true, default-features = false, features = [] }
chrono = { version = "0.4.26", optional = true, default-features = false, features = ["clock"] }
time = { version = "0.3.25", optional = true, default-features = false, features = [] }
bigdecimal = { version = "0.4.1", optional = true, default-features = false, features = [] }
rust_decimal = { version = "1.31.0", optional = true, default-features = false, features = [] }
indexmap = { version = "2.0.0", optional = true, default-features = false, features = [] }
ipnetwork = { version = "0.20.0", optional = true, default-features = false, features = [] }
mac_address = { version = "1.1.5", optional = true, default-features = false, features = [] }
bit-vec = { version = "0.6.3", optional = true, default-features = false, features = [] }
bson = { version = "2.6.1", optional = true, default-features = false, features = [] }
openapiv3 = { version = "1.0.2", optional = true, default-features = false, features = [] }
uhlc = { version = "0.6.0", optional = true, default-features = false, features = [] }
tauri = { version = "1.4.1", optional = true, default-features = false, features = [] }
bytesize = { version = "1.2.0", optional = true, default-features = false, features = [] }
glam = { version = "0.24", optional = true, default-features = false, features = ["std"] }
tokio = { version = "1.30", optional = true, default-features = false, features = ["sync"] }
url = { version = "2.4.0", optional = true, default-features = false }
either = { version = "1.9.0", optional = true, default-features = false }
thiserror = "1.0.44"
paste = "1.0.14"
document-features = "0.2.7"
ctor = { version = "0.2.4", optional = true }
once_cell = "1.18.0"
indoc = "2.0.3"

[dev-dependencies]
doc-comment = "0.3.3"
Expand Down
7 changes: 7 additions & 0 deletions src/datatype/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
borrow::{Borrow, Cow},
collections::BTreeMap,
fmt::Display,
};

mod r#enum;
Expand Down Expand Up @@ -107,6 +108,12 @@ impl DataTypeReference {
#[derive(Debug, Clone, PartialEq)]
pub struct GenericType(pub(crate) Cow<'static, str>);

impl Display for GenericType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

impl Borrow<str> for GenericType {
fn borrow(&self) -> &str {
&self.0
Expand Down
8 changes: 4 additions & 4 deletions src/export/ts.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use std::collections::BTreeMap;

use crate::ts::{self, ExportConfig, TsExportError};
use crate::ts::{self, ExportConfig, ExportError};

use super::get_types;

/// Exports all types in the [`TYPES`](static@crate::export::TYPES) map to the provided TypeScript file.
pub fn ts(path: &str) -> Result<(), TsExportError> {
pub fn ts(path: &str) -> Result<(), ExportError> {
ts_with_cfg(path, &ExportConfig::default())
}

/// Exports all types in the [`TYPES`](static@crate::export::TYPES) map to the provided TypeScript file but allow you to provide a configuration for the exporter.
pub fn ts_with_cfg(path: &str, conf: &ExportConfig) -> Result<(), TsExportError> {
pub fn ts_with_cfg(path: &str, conf: &ExportConfig) -> Result<(), ExportError> {
let mut out = "// This file has been generated by Specta. DO NOT EDIT.\n\n".to_string();

let export_by_default = conf.export_by_default.unwrap_or(true);
Expand Down Expand Up @@ -39,7 +39,7 @@ pub fn ts_with_cfg(path: &str, conf: &ExportConfig) -> Result<(), TsExportError>
map.insert(dt.name.clone(), (sid, ext.impl_location))
{
if existing_sid != sid {
return Err(TsExportError::DuplicateTypeName(
return Err(ExportError::DuplicateTypeName(
dt.name.clone(),
ext.impl_location,
existing_impl_location,
Expand Down
2 changes: 2 additions & 0 deletions src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,6 @@ macro_rules! collect_functions {
}};
}

pub type CollectFunctionsResult = (Vec<FunctionDataType>, TypeMap);

pub use crate::collect_functions;
46 changes: 46 additions & 0 deletions src/lang/js_doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::borrow::Cow;

use crate::*;

pub use super::ts::*;

pub fn format_comment(cfg: &ExportConfig, typ: &NamedDataType, type_map: &TypeMap) -> Output {
format_comment_inner(&ExportContext { cfg, path: vec![] }, typ, type_map)
}

fn format_comment_inner(
ctx: &ExportContext,
typ @ NamedDataType {
name,
comments,
inner: item,
..
}: &NamedDataType,
type_map: &TypeMap,
) -> Output {
let ctx = ctx.with(PathItem::Type(name.clone()));

let name = sanitise_type_name(ctx.clone(), NamedLocation::Type, name)?;

let inline_ts = datatype_inner(ctx.clone(), &typ.inner, type_map)?;

// TODO: Export deprecated

Ok(comments::js_doc(
&comments
.iter()
.cloned()
.chain(
item.generics()
.map(|generics| {
generics
.iter()
.map(|generic| Cow::Owned(format!("@template {}", generic)))
.collect::<Vec<_>>() // TODO: We should be able to avoid this alloc with some work
})
.unwrap_or_default(),
)
.chain([format!(r#"@typedef {{ {inline_ts} }} {name}"#).into()])
.collect::<Vec<_>>(),
))
}
9 changes: 1 addition & 8 deletions src/lang/kotlin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use indoc::formatdoc;

use crate::*;

/// TODO
Expand Down Expand Up @@ -84,12 +82,7 @@ fn datatype(t: &DataTypeExt) -> Result<String, String> {
format!("data class {name}{generics} ({fields}{tag})")
}
};
formatdoc! {
r#"
@Serializable
{decl}\n
"#
}
format!("@Serializable\n{decl}\n")
}
DataType::Literal(_) => return Err("Kotlin does not support literal types!".to_owned()),
_ => todo!(),
Expand Down
10 changes: 10 additions & 0 deletions src/lang/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
#[cfg_attr(docsrs, doc(cfg(feature = "typescript")))]
pub mod ts;

#[cfg(all(feature = "js_doc", not(feature = "typescript")))]
compile_error!("`js_doc` feature requires `typescript` feature to be enabled");

/// [JSDoc](https://jsdoc.app) helpers.
///
/// Also requires `typescript` feature to be enabled.
#[cfg(all(feature = "js_doc", feature = "typescript"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "js_doc", feature = "typescript"))))]
pub mod js_doc;

// /// [Rust](https://www.rust-lang.org) language exporter.
// #[cfg(feature = "rust")]
// #[cfg_attr(docsrs, doc(cfg(feature = "rust")))]
Expand Down
1 change: 0 additions & 1 deletion src/lang/swift.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::*;
use indoc::*;

/// TODO
pub fn export<T: Type>() -> Result<String, String> {
Expand Down
30 changes: 2 additions & 28 deletions src/lang/ts/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,8 @@ use std::borrow::Cow;

use super::CommentFormatterFn;

/// 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),
}
// Assert that the function signature matches the expected type.
const _: CommentFormatterFn = js_doc;

/// Converts Typescript comments into JSDoc comments.
pub fn js_doc(comments: &[Cow<'static, str>]) -> String {
Expand All @@ -42,6 +19,3 @@ pub fn js_doc(comments: &[Cow<'static, str>]) -> String {
result.push_str(" */\n");
result
}

// Assert that the function signature matches the expected type.
const _: CommentFormatterFn = js_doc;
2 changes: 1 addition & 1 deletion src/lang/ts/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub(crate) enum PathItem {

#[derive(Clone)]
pub(crate) struct ExportContext<'a> {
pub(crate) conf: &'a ExportConfig,
pub(crate) cfg: &'a ExportConfig,
pub(crate) path: Vec<PathItem>,
}

Expand Down
4 changes: 2 additions & 2 deletions src/lang/ts/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl fmt::Display for NamedLocation {
/// The error type for the TypeScript exporter.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum TsExportError {
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}")]
Expand All @@ -48,7 +48,7 @@ pub enum TsExportError {
}

// TODO: This `impl` is cringe
impl PartialEq for TsExportError {
impl PartialEq for ExportError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::BigIntForbidden(l0), Self::BigIntForbidden(r0)) => l0 == r0,
Expand Down
28 changes: 27 additions & 1 deletion src/lang/ts/export_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{borrow::Cow, io, path::PathBuf};

use super::{comments, BigIntExportBehavior};
use super::comments;

/// The signature for a function responsible for exporting Typescript comments.
pub type CommentFormatterFn = fn(&[Cow<'static, str>]) -> String;
Expand Down Expand Up @@ -90,3 +90,29 @@ impl Default for ExportConfig {
}
}
}

/// 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),
}
Loading

0 comments on commit b474b8e

Please sign in to comment.