From 2323480aaf3dace303094f09b46b3b06621dabb5 Mon Sep 17 00:00:00 2001 From: Haris <4259838+Wulf@users.noreply.github.com> Date: Wed, 9 Aug 2023 23:33:12 -0400 Subject: [PATCH] 2.0.0: Update deps, refactoring, add doc comments test --- Cargo.lock | 40 +++++++---- Cargo.toml | 12 ++-- src/lib.rs | 2 - src/to_typescript/enums.rs | 67 ++++++++---------- src/utils.rs | 110 ++++++++++++++++++------------ test/doc_comments/rust.rs | 36 ++++++++++ test/doc_comments/tsync.sh | 8 +++ test/doc_comments/typescript.d.ts | 26 +++++++ test/doc_comments/typescript.ts | 29 ++++++++ test/enum/rust.rs | 1 + test/enum/typescript.d.ts | 11 ++- test/enum/typescript.ts | 11 ++- test/test_all.sh | 4 +- 13 files changed, 238 insertions(+), 119 deletions(-) create mode 100644 test/doc_comments/rust.rs create mode 100755 test/doc_comments/tsync.sh create mode 100644 test/doc_comments/typescript.d.ts create mode 100644 test/doc_comments/typescript.ts diff --git a/Cargo.lock b/Cargo.lock index 7eeee63..8e74384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.75", "version_check", ] @@ -108,18 +108,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -141,9 +141,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.23" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ "clap", "lazy_static", @@ -152,15 +152,15 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.75", ] [[package]] @@ -174,6 +174,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -185,13 +196,13 @@ dependencies = [ [[package]] name = "tsync" -version = "1.7.0" +version = "2.0.0" dependencies = [ "convert_case", "proc-macro2", "quote", "structopt", - "syn", + "syn 2.0.28", "tsync-macro", "walkdir", ] @@ -240,12 +251,11 @@ checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] diff --git a/Cargo.toml b/Cargo.toml index b4186b0..1620861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tsync" description = "Generate typescript types from rust code." -version = "1.7.0" +version = "2.0.0" readme = "README.md" repository = "https://github.com/Wulf/tsync" license = "MIT OR Apache-2.0" @@ -11,11 +11,11 @@ authors = ["Haris <4259838+Wulf@users.noreply.github.com>"] edition = "2018" [dependencies] -structopt = "0.3.22" -syn = { version = "1.0.75", features = ["full", "extra-traits"] } -proc-macro2 = "1.0.52" -quote = "1.0.23" -walkdir = "2.3.2" +structopt = "0.3.26" +syn = { version = "2.0.28", features = ["full", "extra-traits"] } +proc-macro2 = "1.0.66" +quote = "1.0.32" +walkdir = "2.3.3" tsync-macro = "0.1.0" convert_case = "0.6.0" diff --git a/src/lib.rs b/src/lib.rs index 6f8367b..a0e2556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,8 +75,6 @@ fn process_rust_file( uses_typeinterface: bool, ) { if debug { - dbg!(uses_typeinterface); - println!( "processing rust file: {:?}", input_path.clone().into_os_string().into_string().unwrap() diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index d4765b4..a8f9576 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -1,7 +1,7 @@ +use crate::typescript::convert_type; use crate::{utils, BuildState}; use convert_case::{Case, Casing}; use syn::__private::ToTokens; -use crate::typescript::convert_type; static RENAME_RULES: &[(&str, convert_case::Case)] = &[ ("lowercase", Case::Lower), @@ -17,7 +17,7 @@ static RENAME_RULES: &[(&str, convert_case::Case)] = &[ /// Conversion of Rust Enum to Typescript using external tagging as per https://serde.rs/enum-representations.html /// however conversion will adhere to the `serde` `tag` such that enums are intenrally tagged /// (while the other forms such as adjacent tagging aren't supported). -/// `renameAll` attributes for the name of the tag will also be adhered to. +/// `rename_all` attributes for the name of the tag will also be adhered to. impl super::ToTypescript for syn::ItemEnum { fn convert_to_ts(self, state: &mut BuildState, debug: bool, uses_typeinterface: bool) { // check we don't have any tuple structs that could mess things up. @@ -33,8 +33,7 @@ impl super::ToTypescript for syn::ItemEnum { println!("#[tsync] failed for enum {}", self.ident); } return; - } - else { + } else { is_newtype = true; } } @@ -52,21 +51,21 @@ impl super::ToTypescript for syn::ItemEnum { if is_single { if utils::has_attribute_arg("derive", "Serialize_repr", &self.attrs) { - make_numeric_enum(self, state, casing, uses_typeinterface) + add_numeric_enum(self, state, casing, uses_typeinterface) } else { - make_enum(self, state, casing, uses_typeinterface) + add_enum(self, state, casing, uses_typeinterface) } } else if let Some(tag_name) = utils::get_attribute_arg("serde", "tag", &self.attrs) { - make_variant(tag_name, self, state, casing, uses_typeinterface) + add_internally_tagged_enum(tag_name, self, state, casing, uses_typeinterface) } else { - make_externally_tagged_variant(self, state, casing, uses_typeinterface) + add_externally_tagged_enum(self, state, casing, uses_typeinterface) } } } /// This convert an all unit enums to a union of const strings in Typescript. /// It will ignore any discriminants. -fn make_enum( +fn add_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, @@ -115,7 +114,7 @@ fn make_enum( /// } /// ``` /// -fn make_numeric_enum( +fn add_numeric_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, @@ -188,7 +187,7 @@ fn make_numeric_enum( /// Cat = 1, /// } /// ``` -fn make_variant( +fn add_internally_tagged_enum( tag_name: String, exported_struct: syn::ItemEnum, state: &mut BuildState, @@ -203,18 +202,17 @@ fn make_variant( )); for variant in exported_struct.variants.iter() { - // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant.fields.iter().fold(false, |state, v| { - state || v.ident.is_none() - }); + let is_newtype = variant + .fields + .iter() + .fold(false, |state, v| state || v.ident.is_none()); if is_newtype { // TODO: Generate newtype structure // This should contain the discriminant plus all fields of the inner structure as a flat structure // TODO: Check for case where discriminant name matches an inner structure field name // We should reject clashes - } - else { + } else { state.types.push('\n'); state.types.push_str(&format!( " | {interface_name}__{variant_name}", @@ -228,9 +226,10 @@ fn make_variant( for variant in exported_struct.variants { // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant.fields.iter().fold(false, |state, v| { - state || v.ident.is_none() - }); + let is_newtype = variant + .fields + .iter() + .fold(false, |state, v| state || v.ident.is_none()); if !is_newtype { state.types.push('\n'); let comments = utils::get_comments(variant.attrs); @@ -261,7 +260,7 @@ fn make_variant( } /// This follows serde's default approach of external tagging -fn make_externally_tagged_variant( +fn add_externally_tagged_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, @@ -284,27 +283,21 @@ fn make_externally_tagged_variant( variant.ident.to_string() }; // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant.fields.iter().fold(false, |state, v| { - state || v.ident.is_none() - }); + let is_newtype = variant + .fields + .iter() + .fold(false, |state, v| state || v.ident.is_none()); if is_newtype { // add discriminant - state.types.push_str(&format!( - " | {{ \"{}\":", - field_name - )); + state.types.push_str(&format!(" | {{ \"{}\":", field_name)); for field in variant.fields { - state.types.push_str(&format!( - " {}", - convert_type(&field.ty).ts_type, - )); + state + .types + .push_str(&format!(" {}", convert_type(&field.ty).ts_type,)); } - state - .types - .push_str(&format!(" }}")); - } - else { + state.types.push_str(&format!(" }}")); + } else { // add discriminant state.types.push_str(&format!( " | {{\n{}\"{}\": {{", diff --git a/src/utils.rs b/src/utils.rs index 07fc1c1..e048d1f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,9 @@ -use syn::{Attribute, NestedMeta, __private::ToTokens}; +use quote::ToTokens; +use syn::{punctuated::Punctuated, Attribute, MetaNameValue, Token}; pub fn has_attribute(needle: &str, attributes: &[syn::Attribute]) -> bool { attributes.iter().any(|attr| { - attr.path + attr.path() .segments .iter() .any(|segment| segment.ident == needle) @@ -10,61 +11,76 @@ pub fn has_attribute(needle: &str, attributes: &[syn::Attribute]) -> bool { } /// Get the value matching an attribute and argument combination -pub fn get_attribute_arg( - needle: &str, - arg: &str, - attributes: &[syn::Attribute], -) -> Option { +pub fn get_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> Option { if let Some(attr) = get_attribute(needle, attributes) { // check if attribute list contains the argument we are interested in - if let Ok(syn::Meta::List(args)) = attr.parse_meta() { - // accept the literal following the argument we want - for subs in args.nested { - if let NestedMeta::Meta(syn::Meta::NameValue(meta)) = subs { - // check if the meta refers to the argument we want - if meta - .path - .get_ident() - .filter(|x| *x == arg) - .is_some() - { - if let syn::Lit::Str(out) = meta.lit { - return Some(out.value()); - } + let mut found = false; + let mut value = String::new(); + + // TODO: don't use a for loop here or iterator here + let tokens = attr.meta.to_token_stream().into_iter(); + for token in tokens { + if let proc_macro2::TokenTree::Ident(ident) = token { + // this detects the 'serde' part in #[serde(rename_all = "UPPERCASE")] + // we use get_attribute to make sure we've gotten the right attribute, + // hence, we'll ignore it here + } else if let proc_macro2::TokenTree::Group(group) = token { + // this detects the '(...)' part in #[serde(rename_all = "UPPERCASE", tag = "type")] + // we can use this to get the value of a particular argument + // or to see if it exists at all + + // make sure the delimiter is what we're expecting + if group.delimiter() != proc_macro2::Delimiter::Parenthesis { + continue; + } + + let name_value_pairs = ::syn::parse::Parser::parse2( + Punctuated::::parse_terminated, + group.stream(), + ); + + if name_value_pairs.is_err() { + continue; + } + + let name_value_pairs = name_value_pairs.unwrap(); + + for name_value_pair in name_value_pairs { + if name_value_pair.path.is_ident(arg) { + found = true; + value = name_value_pair.value.to_token_stream().to_string(); + // removes quotes around the value + value = value[1..value.len() - 1].to_string(); + + break; } } } } + + if found { + return Some(value); + } else { + return None; + } } None } /// Check has an attribute arg. pub fn has_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> bool { - if let Some(attr) = get_attribute(needle, attributes) { - // check if attribute list contains the argument we are interested in - if let Ok(syn::Meta::List(args)) = attr.parse_meta() { - // accept the literal following the argument we want - for subs in args.nested { - if let NestedMeta::Meta(meta) = subs { - // check if the meta refers to the argument we want - if meta.to_token_stream().to_string() == arg { - return true; - } - } - } - } - } - false + get_attribute_arg(needle, arg, attributes).is_some() } /// Get the doc string comments from the syn::attributes +/// note: the compiler transforms doc comments into attributes +/// see: https://docs.rs/syn/2.0.28/syn/struct.Attribute.html#doc-comments pub fn get_comments(attributes: Vec) -> Vec { let mut comments: Vec = vec![]; for attribute in attributes { let mut is_doc = false; - for segment in attribute.path.segments { + for segment in attribute.path().segments.clone() { if segment.ident == "doc" { is_doc = true; break; @@ -72,12 +88,19 @@ pub fn get_comments(attributes: Vec) -> Vec { } if is_doc { - for token in attribute.tokens { - if let proc_macro2::TokenTree::Literal(comment) = token { - let comment = comment.to_string(); - let comment = comment[1..comment.len() - 1].trim(); - comments.push(comment.to_string()); + match attribute.meta { + syn::Meta::NameValue(name_value) => { + let comment = name_value.value.to_token_stream(); + + for token in comment.into_iter() { + if let proc_macro2::TokenTree::Literal(comment) = token { + let comment = comment.to_string(); + let comment = comment[1..comment.len() - 1].trim(); + comments.push(comment.to_string()); + } + } } + _ => continue, } } } @@ -116,7 +139,8 @@ pub fn get_attribute(needle: &str, attributes: &[syn::Attribute]) -> Option /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run -- -i rust.rs -o typescript.d.ts +cargo run -- -i rust.rs -o typescript.ts \ No newline at end of file diff --git a/test/doc_comments/typescript.d.ts b/test/doc_comments/typescript.d.ts new file mode 100644 index 0000000..b732e36 --- /dev/null +++ b/test/doc_comments/typescript.d.ts @@ -0,0 +1,26 @@ +/* This file is generated and managed by tsync */ + +/** enum comment */ +type EnumTest = + | EnumTest__One + | EnumTest__Three; + +/** enum property comment */ +type EnumTest__One = { + type: "ONE"; +}; +/** enum struct comment */ +type EnumTest__Three = { + type: "THREE"; + /** enum struct property comment */ + id: string; +}; + +/** struct comment */ +interface StructTest { + /** struct field comment */ + name: string; +} + +/** type comment */ +type TypeTest = string diff --git a/test/doc_comments/typescript.ts b/test/doc_comments/typescript.ts new file mode 100644 index 0000000..4eaf9d9 --- /dev/null +++ b/test/doc_comments/typescript.ts @@ -0,0 +1,29 @@ +/* This file is generated and managed by tsync */ + +/** enum comment */ +export type EnumTest = + | EnumTest__One + | EnumTest__Three; + +/** enum property comment */ +type EnumTest__One = { + type: "ONE"; +}; +/** enum struct comment */ +type EnumTest__Three = { + type: "THREE"; + /** enum struct property comment */ + id: string; +}; + +/** struct comment */ +export interface StructTest { + /** struct field comment */ + name: string; +} + +/** type comment */ +export type TypeTest = string + +/** const comment */ +export const CONST_TEST = "test"; diff --git a/test/enum/rust.rs b/test/enum/rust.rs index b75ba04..e957ab9 100644 --- a/test/enum/rust.rs +++ b/test/enum/rust.rs @@ -7,6 +7,7 @@ use tsync::tsync; #[serde(tag = "typetypetype")] #[serde(renameAll = "kebab-case")] #[serde(tag = "type")] +#[serde(rename_all = "UPPERCASE", tag = "type")] #[tsync] enum InternalTopping { /// Tasty! diff --git a/test/enum/typescript.d.ts b/test/enum/typescript.d.ts index f887910..a4bafad 100644 --- a/test/enum/typescript.d.ts +++ b/test/enum/typescript.d.ts @@ -13,11 +13,11 @@ type InternalTopping = * Not vegetarian */ type InternalTopping__Pepperoni = { - type: "Pepperoni"; + type: "PEPPERONI"; }; /** For cheese lovers */ type InternalTopping__ExtraCheese = { - type: "ExtraCheese"; + type: "EXTRA CHEESE"; kind: string; }; @@ -63,8 +63,5 @@ type AnimalTwo = | "dog_long_extra" | "cat"; /** Integer enums should follow rust discrimination if literals (doesn't evaluate expression) */ -declare enum Foo { - Bar = 0, - Baz = 123, - Quux = 124, -} +type Foo = + | "Bar" | "Baz" | "Quux"; diff --git a/test/enum/typescript.ts b/test/enum/typescript.ts index ae7468c..eacf84c 100644 --- a/test/enum/typescript.ts +++ b/test/enum/typescript.ts @@ -13,11 +13,11 @@ export type InternalTopping = * Not vegetarian */ type InternalTopping__Pepperoni = { - type: "Pepperoni"; + type: "PEPPERONI"; }; /** For cheese lovers */ type InternalTopping__ExtraCheese = { - type: "ExtraCheese"; + type: "EXTRA CHEESE"; kind: string; }; @@ -63,8 +63,5 @@ export type AnimalTwo = | "dog_long_extra" | "cat"; /** Integer enums should follow rust discrimination if literals (doesn't evaluate expression) */ -export enum Foo { - Bar = 0, - Baz = 123, - Quux = 124, -} +export type Foo = + | "Bar" | "Baz" | "Quux"; diff --git a/test/test_all.sh b/test/test_all.sh index bfb1c5f..ca3f5bd 100755 --- a/test/test_all.sh +++ b/test/test_all.sh @@ -5,8 +5,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd $SCRIPT_DIR ./directory_input/tsync.sh - ./struct/tsync.sh ./type/tsync.sh ./const/tsync.sh -./enum/tsync.sh \ No newline at end of file +./enum/tsync.sh +./doc_comments/tsync.sh