Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add functionality to export doc strings on types #187

Merged
merged 14 commits into from
Feb 9, 2024
Merged
5 changes: 5 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod types;

struct DerivedTS {
name: String,
docs: Vec<String>,
inline: TokenStream,
decl: TokenStream,
inline_flattened: Option<TokenStream>,
Expand Down Expand Up @@ -64,6 +65,7 @@ impl DerivedTS {

let DerivedTS {
name,
docs,
inline,
decl,
inline_flattened,
Expand Down Expand Up @@ -91,6 +93,9 @@ impl DerivedTS {
fn name() -> String {
#name.to_owned()
}
fn docs() -> Vec<String> {
vec![#( #docs.to_string(), )*]
}
fn inline() -> String {
#inline
}
Expand Down
13 changes: 10 additions & 3 deletions macros/src/types/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
deps::Dependencies,
types,
types::generics::{format_generics, format_type},
DerivedTS,
utils, DerivedTS,
};

pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
Expand All @@ -18,13 +18,16 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
None => s.ident.to_string(),
};

let docs = utils::get_docs_from_attributes(&s.attrs);

if s.variants.is_empty() {
return Ok(empty_enum(name, enum_attr));
return Ok(empty_enum(name, docs, enum_attr));
}

if s.variants.is_empty() {
return Ok(DerivedTS {
name,
docs,
inline: quote!("never".to_owned()),
decl: quote!("type {} = never;"),
inline_flattened: None,
Expand Down Expand Up @@ -53,6 +56,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
inline_flattened: None,
dependencies,
name,
docs,
export: enum_attr.export,
export_to: enum_attr.export_to,
})
Expand All @@ -79,6 +83,8 @@ fn format_variant(

let variant_type = types::type_def(
&StructAttr::from(variant_attr),
// since this is the type of a field, we do not need to pass attributes to calculate docs
&vec![],
// since we are generating the variant as a struct, it doesn't have a name
&format_ident!("_"),
&variant.fields,
Expand Down Expand Up @@ -165,12 +171,13 @@ fn format_variant(
}

// bindings for an empty enum (`never` in TS)
fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
fn empty_enum(name: impl Into<String>, docs: Vec<String>, enum_attr: EnumAttr) -> DerivedTS {
let name = name.into();
DerivedTS {
inline: quote!("never".to_owned()),
decl: quote!(format!("type {} = never;", #name)),
name,
docs,
inline_flattened: None,
dependencies: Dependencies::default(),
export: enum_attr.export,
Expand Down
3 changes: 2 additions & 1 deletion macros/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generi
Type::Tuple(tuple) => {
if tuple.elems.is_empty() {
// empty tuples `()` should be treated as `null`
return super::unit::null(&StructAttr::default(), "")
return super::unit::null(&StructAttr::default(), "", &vec![])
.unwrap()
.inline;
}

// we convert the tuple field to a struct: `(A, B, C)` => `struct A(A, B, C)`
let tuple_struct = super::type_def(
&StructAttr::default(),
&vec![],
&format_ident!("_"),
&tuple_type_to_tuple_struct(tuple).fields,
generics,
Expand Down
24 changes: 15 additions & 9 deletions macros/src/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use syn::{Fields, Generics, Ident, ItemStruct, Result};
use syn::{Attribute, Fields, Generics, Ident, ItemStruct, Result};

use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS};
use crate::{
attr::StructAttr,
utils::{self, to_ts_ident},
DerivedTS,
};

mod r#enum;
mod generics;
Expand All @@ -14,26 +18,28 @@ pub(crate) use r#enum::r#enum_def;
pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
let attr = StructAttr::from_attrs(&s.attrs)?;

type_def(&attr, &s.ident, &s.fields, &s.generics)
type_def(&attr, &s.attrs, &s.ident, &s.fields, &s.generics)
}

fn type_def(
attr: &StructAttr,
attrs: &Vec<Attribute>,
ident: &Ident,
fields: &Fields,
generics: &Generics,
) -> Result<DerivedTS> {
let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident));
let docs = utils::get_docs_from_attributes(attrs);
match fields {
escritorio-gustavo marked this conversation as resolved.
Show resolved Hide resolved
Fields::Named(named) => match named.named.len() {
0 => unit::empty_object(attr, &name),
_ => named::named(attr, &name, named, generics),
0 => unit::empty_object(attr, &name, &docs),
_ => named::named(attr, &name, &docs, named, generics),
},
Fields::Unnamed(unnamed) => match unnamed.unnamed.len() {
0 => unit::empty_array(attr, &name),
1 => newtype::newtype(attr, &name, unnamed, generics),
_ => tuple::tuple(attr, &name, unnamed, generics),
0 => unit::empty_array(attr, &name, &docs),
1 => newtype::newtype(attr, &name, &docs, unnamed, generics),
_ => tuple::tuple(attr, &name, &docs, unnamed, generics),
},
Fields::Unit => unit::null(attr, &name),
Fields::Unit => unit::null(attr, &name, &docs),
}
}
2 changes: 2 additions & 0 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
pub(crate) fn named(
attr: &StructAttr,
name: &str,
docs: &Vec<String>,
fields: &FieldsNamed,
generics: &Generics,
) -> Result<DerivedTS> {
Expand Down Expand Up @@ -48,6 +49,7 @@ pub(crate) fn named(
decl: quote!(format!("interface {}{} {}", #name, #generic_args, Self::inline())),
inline_flattened: Some(fields),
name: name.to_owned(),
docs: docs.to_owned(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
Expand Down
4 changes: 3 additions & 1 deletion macros/src/types/newtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
pub(crate) fn newtype(
attr: &StructAttr,
name: &str,
docs: &Vec<String>,
fields: &FieldsUnnamed,
generics: &Generics,
) -> Result<DerivedTS> {
Expand All @@ -32,7 +33,7 @@ pub(crate) fn newtype(

match (&rename_inner, skip, optional, flatten) {
(Some(_), ..) => syn_err!("`rename` is not applicable to newtype fields"),
(_, true, ..) => return super::unit::null(attr, name),
(_, true, ..) => return super::unit::null(attr, name, docs),
(_, _, true, ..) => syn_err!("`optional` is not applicable to newtype fields"),
(_, _, _, true) => syn_err!("`flatten` is not applicable to newtype fields"),
_ => {}
Expand All @@ -57,6 +58,7 @@ pub(crate) fn newtype(
inline: inline_def,
inline_flattened: None,
name: name.to_owned(),
docs: docs.to_owned(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
Expand Down
2 changes: 2 additions & 0 deletions macros/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
pub(crate) fn tuple(
attr: &StructAttr,
name: &str,
docs: &Vec<String>,
fields: &FieldsUnnamed,
generics: &Generics,
) -> Result<DerivedTS> {
Expand Down Expand Up @@ -46,6 +47,7 @@ pub(crate) fn tuple(
},
inline_flattened: None,
name: name.to_owned(),
docs: docs.to_owned(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
Expand Down
9 changes: 6 additions & 3 deletions macros/src/types/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,45 @@ use syn::Result;

use crate::{attr::StructAttr, deps::Dependencies, DerivedTS};

pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
pub(crate) fn empty_object(attr: &StructAttr, name: &str, docs: &Vec<String>) -> Result<DerivedTS> {
check_attributes(attr)?;

Ok(DerivedTS {
inline: quote!("Record<string, never>".to_owned()),
decl: quote!(format!("type {} = Record<string, never>;", #name)),
inline_flattened: None,
name: name.to_owned(),
docs: docs.to_owned(),
dependencies: Dependencies::default(),
export: attr.export,
export_to: attr.export_to.clone(),
})
}

pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
pub(crate) fn empty_array(attr: &StructAttr, name: &str, docs: &Vec<String>) -> Result<DerivedTS> {
check_attributes(attr)?;

Ok(DerivedTS {
inline: quote!("never[]".to_owned()),
decl: quote!(format!("type {} = never[];", #name)),
inline_flattened: None,
name: name.to_owned(),
docs: docs.to_owned(),
dependencies: Dependencies::default(),
export: attr.export,
export_to: attr.export_to.clone(),
})
}

pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
pub(crate) fn null(attr: &StructAttr, name: &str, docs: &Vec<String>) -> Result<DerivedTS> {
check_attributes(attr)?;

Ok(DerivedTS {
inline: quote!("null".to_owned()),
decl: quote!(format!("type {} = null;", #name)),
inline_flattened: None,
name: name.to_owned(),
docs: docs.to_owned(),
dependencies: Dependencies::default(),
export: attr.export,
export_to: attr.export_to.clone(),
Expand Down
27 changes: 26 additions & 1 deletion macros/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::convert::TryFrom;

use proc_macro2::Ident;
use syn::{Attribute, Error, Result};
use syn::{Attribute, Error, Expr, Lit, Meta, Result};

macro_rules! syn_err {
($l:literal $(, $a:expr)*) => {
Expand Down Expand Up @@ -113,6 +113,31 @@ pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>(
.into_iter()
}

/// Return a vector of all lines of doc comments in the given vector of attributes.
pub fn get_docs_from_attributes(attrs: &Vec<Attribute>) -> Vec<String> {
attrs
.into_iter()
.map(|attr| match &attr.meta {
Meta::Path(_) => None,
Meta::List(_) => None,
Meta::NameValue(name_value) => {
if name_value.path.is_ident("doc") {
match &name_value.value {
Expr::Lit(lit) => match &lit.lit {
Lit::Str(str) => Some(str.value()),
_ => panic!("doc attribute with non string literal found"),
},
_ => panic!("doc attribute with non literal expression found"),
}
} else {
None
}
}
})
.flatten()
.collect()
}

#[cfg(feature = "serde-compat")]
mod warning {
use std::{fmt::Display, io::Write};
Expand Down
19 changes: 19 additions & 0 deletions ts-rs/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {

/// Push the declaration of `T`
fn generate_decl<T: TS + ?Sized>(out: &mut String) {
// Type Docs
let docs = &T::docs();
out.push_str(&format_docs(&docs));

// Type Definition
out.push_str("export ");
out.push_str(&T::decl());
}
Expand Down Expand Up @@ -174,3 +179,17 @@ where
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}

/// Returns an unindented docstring that has a newline at the end if it has content.
pub fn format_docs(docs: &Vec<String>) -> String {
escritorio-gustavo marked this conversation as resolved.
Show resolved Hide resolved
match docs.is_empty() {
true => "".to_string(),
false => {
let lines = docs
.into_iter()
.map(|doc| format!(" *{doc}"))
.collect::<Vec<_>>();
format!("/**\n{}\n */\n", lines.join("\n"))
}
}
}
5 changes: 5 additions & 0 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ pub trait TS {
format!("{}<{}>", Self::name(), args.join(", "))
}

/// All lines of documentation comments for this type.
fn docs() -> Vec<String> {
escritorio-gustavo marked this conversation as resolved.
Show resolved Hide resolved
vec![]
}

/// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
/// This function will panic if the type cannot be inlined.
fn inline() -> String {
Expand Down
Loading
Loading