Repository: Aleph-Alpha/ts-rs
Pull request #269 (2024-03-14 00:57:51)

Add '#[ts(bound)]' attribute


Patch:
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index 9db5895..232d61f 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -1,12 +1,14 @@
 use std::collections::HashMap;
 
-use syn::{Attribute, Ident, Result, Type};
+use syn::{Attribute, Ident, Result, Type, WherePredicate};
 
 use crate::{
     attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection},
     utils::{parse_attrs, parse_docs},
 };
 
+use super::parse_bound;
+
 #[derive(Default)]
 pub struct EnumAttr {
     pub rename_all: Option<Inflection>,
@@ -16,6 +18,7 @@ pub struct EnumAttr {
     pub export: bool,
     pub docs: String,
     pub concrete: HashMap<Ident, Type>,
+    pub bound: Option<Vec<WherePredicate>>,
     tag: Option<String>,
     untagged: bool,
     content: Option<String>,
@@ -71,6 +74,7 @@ impl EnumAttr {
             export,
             docs,
             concrete,
+            bound,
         }: EnumAttr,
     ) {
         self.rename = self.rename.take().or(rename);
@@ -83,6 +87,10 @@ impl EnumAttr {
         self.export_to = self.export_to.take().or(export_to);
         self.docs = docs;
         self.concrete.extend(concrete);
+        self.bound = self.bound
+            .take()
+            .map(|b| b.into_iter().chain(bound.clone().unwrap_or_default()).collect())
+            .or(bound);
     }
 }
 
@@ -97,6 +105,7 @@ impl_parse! {
         "content" => out.content = Some(parse_assign_str(input)?),
         "untagged" => out.untagged = true,
         "concrete" => out.concrete = parse_concrete(input)?,
+        "bound" => out.bound = Some(parse_bound(input)?),
     }
 }
 
@@ -108,6 +117,7 @@ impl_parse! {
         "rename_all_fields" => out.0.rename_all_fields = Some(parse_assign_inflection(input)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
         "content" => out.0.content = Some(parse_assign_str(input)?),
-        "untagged" => out.0.untagged = true
+        "untagged" => out.0.untagged = true,
+        "bound" => out.0.bound = Some(parse_bound(input)?),
     }
 }
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index e5b4c49..fcf9fbd 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -5,7 +5,7 @@ pub use r#enum::*;
 pub use r#struct::*;
 use syn::{
     parse::{Parse, ParseStream},
-    Error, Lit, Result, Token,
+    Error, Lit, Result, Token, WherePredicate, punctuated::Punctuated,
 };
 pub use variant::*;
 
@@ -107,7 +107,7 @@ fn parse_concrete(input: ParseStream) -> Result<HashMap<syn::Ident, syn::Type>>
     syn::parenthesized!(content in input);
 
     Ok(
-        syn::punctuated::Punctuated::<Concrete, Token![,]>::parse_terminated(&content)?
+        Punctuated::<Concrete, Token![,]>::parse_terminated(&content)?
             .into_iter()
             .map(|concrete| (concrete.ident, concrete.ty))
             .collect(),
@@ -128,3 +128,15 @@ where
         other => Err(Error::new(other.span(), "expected string")),
     }
 }
+
+fn parse_bound(input: ParseStream) -> Result<Vec<WherePredicate>> {
+    input.parse::<Token![=]>()?;
+    match Lit::parse(input)? {
+        Lit::Str(string) => {
+            let parser = Punctuated::<WherePredicate, Token![,]>::parse_terminated;
+
+            Ok(string.parse_with(parser)?.into_iter().collect())
+        },
+        other => Err(Error::new(other.span(), "expected string")),
+    }
+}
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 49cc4b2..6ed3bf9 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,12 +1,14 @@
 use std::{collections::HashMap, convert::TryFrom};
 
-use syn::{Attribute, Ident, Result, Type};
+use syn::{Attribute, Ident, Result, Type, WherePredicate};
 
 use crate::{
     attr::{parse_assign_str, parse_concrete, Inflection, VariantAttr},
     utils::{parse_attrs, parse_docs},
 };
 
+use super::parse_bound;
+
 #[derive(Default, Clone)]
 pub struct StructAttr {
     pub rename_all: Option<Inflection>,
@@ -16,6 +18,7 @@ pub struct StructAttr {
     pub tag: Option<String>,
     pub docs: String,
     pub concrete: HashMap<Ident, Type>,
+    pub bound: Option<Vec<WherePredicate>>,
 }
 
 #[cfg(feature = "serde-compat")]
@@ -45,6 +48,7 @@ impl StructAttr {
             tag,
             docs,
             concrete,
+            bound,
         }: StructAttr,
     ) {
         self.rename = self.rename.take().or(rename);
@@ -54,6 +58,10 @@ impl StructAttr {
         self.tag = self.tag.take().or(tag);
         self.docs = docs;
         self.concrete.extend(concrete);
+        self.bound = self.bound
+            .take()
+            .map(|b| b.into_iter().chain(bound.clone().unwrap_or_default()).collect())
+            .or(bound);
     }
 }
 
@@ -80,6 +88,7 @@ impl_parse! {
         "export" => out.export = true,
         "export_to" => out.export_to = Some(parse_assign_str(input)?),
         "concrete" => out.concrete = parse_concrete(input)?,
+        "bound" => out.bound = Some(parse_bound(input)?),
     }
 }
 
@@ -89,6 +98,7 @@ impl_parse! {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
+        "bound" => out.0.bound = Some(parse_bound(input)?),
         // parse #[serde(default)] to not emit a warning
         "deny_unknown_fields" | "default" => {
             use syn::Token;
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 0ddb662..c61e8b0 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -8,7 +8,7 @@ use quote::{format_ident, quote};
 use syn::{
     parse_quote, spanned::Spanned, ConstParam, GenericParam, Generics, Item, LifetimeParam, Result,
     Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
-    WhereClause,
+    WhereClause, WherePredicate,
 };
 
 use crate::{deps::Dependencies, utils::format_generics};
@@ -26,6 +26,7 @@ struct DerivedTS {
     inline_flattened: Option<TokenStream>,
     dependencies: Dependencies,
     concrete: HashMap<Ident, Type>,
+    bound: Option<Vec<WherePredicate>>,
 
     export: bool,
     export_to: Option<String>,
@@ -59,7 +60,12 @@ impl DerivedTS {
         };
 
         let ident = self.ts_name.clone();
-        let impl_start = generate_impl_block_header(&rust_ty, &generics, &self.dependencies);
+        let impl_start = generate_impl_block_header(
+            &rust_ty,
+            &generics,
+            self.bound.as_deref(),
+            &self.dependencies,
+        );
         let assoc_type = generate_assoc_type(&rust_ty, &generics, &self.concrete);
         let name = self.generate_name_fn(&generics);
         let inline = self.generate_inline_fn();
@@ -295,11 +301,12 @@ fn generate_assoc_type(
 fn generate_impl_block_header(
     ty: &Ident,
     generics: &Generics,
+    bounds: Option<&[WherePredicate]>,
     dependencies: &Dependencies,
 ) -> TokenStream {
     use GenericParam as G;
 
-    let bounds = generics.params.iter().map(|param| match param {
+    let params = generics.params.iter().map(|param| match param {
         G::Type(TypeParam {
             ident,
             colon_token,
@@ -325,8 +332,15 @@ fn generate_impl_block_header(
         G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
     });
 
-    let where_bound = generate_where_clause(generics, dependencies);
-    quote!(impl <#(#bounds),*> ::ts_rs::TS for #ty <#(#type_args),*> #where_bound)
+    let where_bound = match bounds {
+        Some(bounds) => quote! { where #(#bounds),* },
+        None => {
+            let bounds = generate_where_clause(generics, dependencies);
+            quote! { #bounds }
+        }
+    };
+
+    quote!(impl <#(#params),*> ::ts_rs::TS for #ty <#(#type_args),*> #where_bound)
 }
 
 fn generate_where_clause(generics: &Generics, dependencies: &Dependencies) -> WhereClause {
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index f94a6a7..f35cb98 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -30,6 +30,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
             export: enum_attr.export,
             export_to: enum_attr.export_to,
             concrete: enum_attr.concrete,
+            bound: enum_attr.bound,
         });
     }
 
@@ -55,6 +56,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
         export_to: enum_attr.export_to,
         ts_name: name,
         concrete: enum_attr.concrete,
+        bound: enum_attr.bound,
     })
 }
 
@@ -205,5 +207,6 @@ fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
         export_to: enum_attr.export_to,
         ts_name: name,
         concrete: enum_attr.concrete,
+        bound: enum_attr.bound,
     }
 }
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index c21c679..d076d54 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -51,6 +51,7 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
 
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index 4d29fbd..182811d 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -71,5 +71,6 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index b815b92..40d9f66 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -36,6 +36,7 @@ pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Re
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
 
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 40bacb7..88cad88 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -15,6 +15,7 @@ pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
 
@@ -30,6 +31,7 @@ pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
 
@@ -45,6 +47,7 @@ pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
         concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
     })
 }
 
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index d21e251..c389b97 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -223,6 +223,44 @@ pub mod typelist;
 ///   ```
 ///   <br/><br/>
 ///
+/// - **`#[ts(bound)]`**
+///   Override the bounds generated on the `TS` implementation for this type. This is useful in
+///   combination with `#[ts(concrete)]`, when the type's generic parameters aren't directly used
+///   in a field or variant.
+///
+///   Example:
+///   ```
+///   # use ts_rs::TS;
+///
+///   trait Container {
+///       type Value: TS;
+///   }
+///
+///   struct MyContainer;
+///
+///   ##[derive(TS)]
+///   struct MyValue;
+///
+///   impl Container for MyContainer {
+///       type Value = MyValue;
+///   }
+///
+///   ##[derive(TS)]
+///   ##[ts(export, concrete(C = MyContainer))]
+///   struct Inner<C: Container> {
+///       value: C::Value,
+///   }
+///
+///   ##[derive(TS)]
+///   // Without `#[ts(bound)]`, `#[derive(TS)]` would generate an unnecessary
+///   // `C: TS` bound
+///   ##[ts(export, concrete(C = MyContainer), bound = "C::Value: TS")]
+///   struct Outer<C: Container> {
+///       inner: Inner<C>,
+///   }
+///   ```
+///   <br/><br/>
+///
 /// ### struct attributes
 /// - **`#[ts(tag = "..")]`**  
 ///   Include the structs name (or value of `#[ts(rename = "..")]`) as a field with the given key.


Test patch:
diff --git a/ts-rs/tests/bound.rs b/ts-rs/tests/bound.rs
new file mode 100644
index 0000000..485d2de
--- /dev/null
+++ b/ts-rs/tests/bound.rs
@@ -0,0 +1,36 @@
+#![allow(dead_code)]
+
+use ts_rs::TS;
+
+trait Driver {
+    type Info;
+}
+
+struct TsDriver;
+
+#[derive(TS)]
+struct TsInfo;
+
+impl Driver for TsDriver {
+    type Info = TsInfo;
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "bound/")]
+#[ts(concrete(D = TsDriver))]
+struct Inner<D: Driver> {
+    info: D::Info,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "bound/")]
+#[ts(concrete(D = TsDriver), bound = "D::Info: TS")]
+struct Outer<D: Driver> {
+    inner: Inner<D>,
+}
+
+#[test]
+fn test_bound() {
+    assert_eq!(Outer::<TsDriver>::decl(), "type Outer = { inner: Inner, };");
+    assert_eq!(Inner::<TsDriver>::decl(), "type Inner = { info: TsInfo, };");
+}