From 6bceec493997ea2e3777288d8dfdd9cdd9db386d Mon Sep 17 00:00:00 2001 From: HoLLy Date: Wed, 9 Aug 2023 21:40:31 +0200 Subject: [PATCH 1/3] Pretty-print output typescript definitions --- src/emit.rs | 96 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/src/emit.rs b/src/emit.rs index a66f6c4..0bfde4e 100644 --- a/src/emit.rs +++ b/src/emit.rs @@ -3,7 +3,7 @@ use crate::type_expr::{ TypeArray, TypeDefinition, TypeExpr, TypeInfo, TypeIntersection, TypeName, TypeObject, TypeString, TypeTuple, TypeUnion, }; -use std::io; +use std::{borrow::Cow, io}; /// A Rust type that has a corresponding TypeScript type definition. /// @@ -146,9 +146,35 @@ pub trait TypeDef: 'static { pub(crate) struct EmitCtx<'ctx> { w: &'ctx mut dyn io::Write, root_namespace: Option<&'ctx str>, + indent: usize, stats: Stats, } +impl EmitCtx<'_> { + fn indent(&mut self) { + self.indent += 1; + } + + fn deindent(&mut self) { + debug_assert!( + self.indent > 0, + "indentation must be > 0 when deindenting" + ); + self.indent -= 1; + } + + fn current_indentation(&self) -> Cow<'static, str> { + // hard-code common values to avoid frequent string construction + match self.indent { + 0 => "".into(), + 1 => " ".into(), + 2 => " ".into(), + 3 => " ".into(), + n => " ".repeat(n).into(), + } + } +} + pub(crate) trait Emit { fn emit(&self, ctx: &mut EmitCtx<'_>) -> io::Result<()>; } @@ -208,6 +234,7 @@ impl<'ctx> EmitCtx<'ctx> { Self { w, root_namespace, + indent: 0, stats, } } @@ -243,7 +270,7 @@ where let Self(args) = self; if !args.is_empty() { write!(ctx.w, "<")?; - SepList(args, ",").emit(ctx)?; + SepList(args, ", ").emit(ctx)?; write!(ctx.w, ">")?; } Ok(()) @@ -298,7 +325,7 @@ impl Emit for TypeTuple { let Self { docs, elements } = self; docs.emit(ctx)?; write!(ctx.w, "[")?; - SepList(elements, ",").emit(ctx)?; + SepList(elements, ", ").emit(ctx)?; write!(ctx.w, "]")?; Ok(()) } @@ -311,15 +338,20 @@ impl Emit for TypeObject { index_signature, fields, } = self; - docs.emit(ctx)?; - write!(ctx.w, "{{")?; + if let Some(docs) = docs { + docs.emit(ctx)?; + write!(ctx.w, "{}", ctx.current_indentation())?; + } + writeln!(ctx.w, "{{")?; + ctx.indent(); if let Some(IndexSignature { docs, name, value }) = index_signature { docs.emit(ctx)?; - write!(ctx.w, "[")?; + write!(ctx.w, "{}[", ctx.current_indentation())?; name.emit(ctx)?; write!(ctx.w, ":string]:")?; value.emit(ctx)?; write!(ctx.w, ";")?; + writeln!(ctx.w)?; } for ObjectField { docs, @@ -329,15 +361,17 @@ impl Emit for TypeObject { } in *fields { docs.emit(ctx)?; + write!(ctx.w, "{}", ctx.current_indentation())?; name.emit(ctx)?; if *optional { write!(ctx.w, "?")?; } - write!(ctx.w, ":")?; + write!(ctx.w, ": ")?; r#type.emit(ctx)?; - write!(ctx.w, ";")?; + writeln!(ctx.w, ";")?; } - write!(ctx.w, "}}")?; + ctx.deindent(); + write!(ctx.w, "{}}}", ctx.current_indentation())?; Ok(()) } } @@ -361,7 +395,7 @@ impl Emit for TypeUnion { write!(ctx.w, "never")?; } else { write!(ctx.w, "(")?; - SepList(members, "|").emit(ctx)?; + SepList(members, " | ").emit(ctx)?; write!(ctx.w, ")")?; } Ok(()) @@ -376,7 +410,7 @@ impl Emit for TypeIntersection { write!(ctx.w, "unknown")?; } else { write!(ctx.w, "(")?; - SepList(members, "&").emit(ctx)?; + SepList(members, " & ").emit(ctx)?; write!(ctx.w, ")")?; } Ok(()) @@ -395,11 +429,11 @@ impl Emit for Docs { fn emit(&self, ctx: &mut EmitCtx<'_>) -> io::Result<()> { let Self(docs) = self; writeln!(ctx.w)?; - writeln!(ctx.w, "/**")?; + writeln!(ctx.w, "{}/**", ctx.current_indentation())?; for line in docs.lines() { - writeln!(ctx.w, " * {}", line)?; + writeln!(ctx.w, "{} * {}", ctx.current_indentation(), line)?; } - writeln!(ctx.w, " */")?; + writeln!(ctx.w, "{} */", ctx.current_indentation())?; Ok(()) } } @@ -438,19 +472,26 @@ impl EmitCtx<'_> { { self.stats.type_definitions += 1; if !path.is_empty() { - write!(self.w, "export namespace ")?; + write!( + self.w, + "{}export namespace ", + self.current_indentation() + )?; SepList(path, ".").emit(self)?; - writeln!(self.w, "{{")?; + writeln!(self.w, " {{")?; + self.indent(); } docs.emit(self)?; - write!(self.w, "export type ")?; + write!(self.w, "{}export type ", self.current_indentation())?; name.emit(self)?; Generics(generic_vars).emit(self)?; - write!(self.w, "=")?; + write!(self.w, " = ")?; def.emit(self)?; write!(self.w, ";")?; if !path.is_empty() { - write!(self.w, "}}")?; + writeln!(self.w)?; + self.deindent(); + write!(self.w, "{}}}", self.current_indentation())?; } writeln!(self.w)?; } @@ -560,20 +601,22 @@ pub fn write_definition_file_from_type_infos( where W: io::Write, { + let mut ctx = EmitCtx::new(&mut writer, options.root_namespace); if let Some(header) = options.header { - writeln!(&mut writer, "{}", header)?; + writeln!(&mut ctx.w, "{}", header)?; } if let Some(root_namespace) = options.root_namespace { - writeln!(&mut writer, "export default {};", root_namespace)?; - writeln!(&mut writer, "export namespace {}{{", root_namespace)?; + writeln!(&mut ctx.w, "export default {};", root_namespace)?; + writeln!(&mut ctx.w, "export namespace {} {{", root_namespace)?; + ctx.indent(); } - let mut ctx = EmitCtx::new(&mut writer, options.root_namespace); ctx.emit_type_def(type_infos)?; - let stats = ctx.stats; if options.root_namespace.is_some() { - writeln!(&mut writer, "}}")?; + ctx.deindent(); + writeln!(&mut ctx.w, "}}")?; } - Ok(stats) + debug_assert_eq!(ctx.indent, 0, "indentation must be 0 after printing"); + Ok(ctx.stats) } impl TypeInfo { @@ -625,6 +668,7 @@ impl TypeInfo { { let mut ctx = EmitCtx::new(&mut writer, root_namespace); ctx.emit_type_ref(self)?; + debug_assert_eq!(ctx.indent, 0, "indentation must be 0 after printing"); Ok(()) } } From bd51840a30d1cbd764fa32e6db296b67f2df5a7f Mon Sep 17 00:00:00 2001 From: HoLLy Date: Wed, 9 Aug 2023 21:49:58 +0200 Subject: [PATCH 2/3] Update test cases to account for new formatting --- src/emit.rs | 16 ++- src/lib.rs | 45 +++++-- tests/test.rs | 326 ++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 276 insertions(+), 111 deletions(-) diff --git a/src/emit.rs b/src/emit.rs index 0bfde4e..416cc6b 100644 --- a/src/emit.rs +++ b/src/emit.rs @@ -55,9 +55,12 @@ use std::{borrow::Cow, io}; /// r#"// AUTO-GENERATED by typescript-type-def /// /// export default types; -/// export namespace types{ -/// export type Uuid=string; -/// export type User={"id":types.Uuid;"name":string;}; +/// export namespace types { +/// export type Uuid = string; +/// export type User = { +/// "id": types.Uuid; +/// "name": string; +/// }; /// } /// "# /// ); @@ -87,8 +90,11 @@ use std::{borrow::Cow, io}; /// r#"// AUTO-GENERATED by typescript-type-def /// /// export default types; -/// export namespace types{ -/// export type User={"id":string;"name":string;}; +/// export namespace types { +/// export type User = { +/// "id": string; +/// "name": string; +/// }; /// } /// "# /// ); diff --git a/src/lib.rs b/src/lib.rs index f017e24..cc9a50d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,9 +57,12 @@ //! r#"// AUTO-GENERATED by typescript-type-def //! //! export default types; -//! export namespace types{ -//! export type Usize=number; -//! export type Foo={"a":types.Usize;"b":string;}; +//! export namespace types { +//! export type Usize = number; +//! export type Foo = { +//! "a": types.Usize; +//! "b": string; +//! }; //! } //! "# //! ); @@ -116,11 +119,19 @@ //! r#"// AUTO-GENERATED by typescript-type-def //! //! export default types; -//! export namespace types{ -//! export type Foo={"a":string;}; -//! export type Bar={"a":string;}; -//! export type Qux={"a":string;}; -//! export type Baz={"a":types.Qux;}; +//! export namespace types { +//! export type Foo = { +//! "a": string; +//! }; +//! export type Bar = { +//! "a": string; +//! }; +//! export type Qux = { +//! "a": string; +//! }; +//! export type Baz = { +//! "a": types.Qux; +//! }; //! } //! "# //! ); @@ -179,11 +190,19 @@ //! r#"// AUTO-GENERATED by typescript-type-def //! //! export default types; -//! export namespace types{ -//! export type Foo={"a":string;}; -//! export type Bar={"a":string;}; -//! export type Qux={"a":string;}; -//! export type Baz={"a":types.Qux;}; +//! export namespace types { +//! export type Foo = { +//! "a": string; +//! }; +//! export type Bar = { +//! "a": string; +//! }; +//! export type Qux = { +//! "a": string; +//! }; +//! export type Baz = { +//! "a": types.Qux; +//! }; //! } //! "# //! ); diff --git a/tests/test.rs b/tests/test.rs index d959660..1164b33 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -58,9 +58,9 @@ fn emit() { assert_eq_str!( emitted, r#"export default types; -export namespace types{ -export type Usize=number; -export type Test=(Record<(types.Usize|null),(string)[]>)[]; +export namespace types { + export type Usize = number; + export type Test = (Record<(types.Usize | null), (string)[]>)[]; } "# ); @@ -176,20 +176,80 @@ mod derive { assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Usize=number; -export type Parent={"FOO_BAR":types.Usize;}; -export type U8=number; -export type Test=(types.Parent&{"a":string;"b":(types.Usize|null);"c"?:(boolean)[];"d"?:types.U8;"FFF":string;"g":({"Ok":string;}|{"Err":types.Usize;});"h":({"Ok":null;}|{"Err":types.Usize;});"i":(string)[];}); -export type Test2=[types.Test,types.Usize,string]; -export type Test3=types.Test2; -export type Test4=(types.Test3|[string,types.Usize]|{"a":string;"b":types.Usize;}); -export type Test5=("a"|"b"|"cool-beans"|"DDD"); -export type Test6=({"A":{"a":types.Usize;};}|{"B":[types.Usize,string];}|{"C":string;}|"D"); -export type Test7=null; -export type TEST_8={}; -export type Test9=never; -export type Test10=({"type":"A";"value":(types.Parent&{"a":string;"b":types.Usize;});}|{"type":"B";"value":{"A":types.Test4;"B":types.Test5;"C":types.Test6;"D":types.Test7;"E":types.TEST_8;"F":(types.Test9|null);"G":null;};}|{"type":"C";"value":types.Parent;}|{"type":"D";}|{"type":"E";"value":{};}|{"type":"F";"value":[];}); +export namespace types { + export type Usize = number; + export type Parent = { + "FOO_BAR": types.Usize; + }; + export type U8 = number; + export type Test = (types.Parent & { + "a": string; + "b": (types.Usize | null); + "c"?: (boolean)[]; + "d"?: types.U8; + "FFF": string; + "g": ({ + "Ok": string; + } | { + "Err": types.Usize; + }); + "h": ({ + "Ok": null; + } | { + "Err": types.Usize; + }); + "i": (string)[]; + }); + export type Test2 = [types.Test, types.Usize, string]; + export type Test3 = types.Test2; + export type Test4 = (types.Test3 | [string, types.Usize] | { + "a": string; + "b": types.Usize; + }); + export type Test5 = ("a" | "b" | "cool-beans" | "DDD"); + export type Test6 = ({ + "A": { + "a": types.Usize; + }; + } | { + "B": [types.Usize, string]; + } | { + "C": string; + } | "D"); + export type Test7 = null; + export type TEST_8 = { + }; + export type Test9 = never; + export type Test10 = ({ + "type": "A"; + "value": (types.Parent & { + "a": string; + "b": types.Usize; + }); + } | { + "type": "B"; + "value": { + "A": types.Test4; + "B": types.Test5; + "C": types.Test6; + "D": types.Test7; + "E": types.TEST_8; + "F": (types.Test9 | null); + "G": null; + }; + } | { + "type": "C"; + "value": types.Parent; + } | { + "type": "D"; + } | { + "type": "E"; + "value": { + }; + } | { + "type": "F"; + "value": []; + }); } "# ); @@ -263,9 +323,19 @@ export type Test10=({"type":"A";"value":(types.Parent&{"a":string;"b":types.Usiz assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Inner={"x":boolean;}; -export type Test=(({"type":"A";}&{"a":types.Inner;})|({"type":"B";}&types.Inner)|{"type":"D";}); +export namespace types { + export type Inner = { + "x": boolean; + }; + export type Test = (({ + "type": "A"; + } & { + "a": types.Inner; + }) | ({ + "type": "B"; + } & types.Inner) | { + "type": "D"; + }); } "# ); @@ -301,9 +371,24 @@ export type Test=(({"type":"A";}&{"a":types.Inner;})|({"type":"B";}&types.Inner) assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Inner={"x":boolean;}; -export type Test=({"type":"A";"value":{"a":types.Inner;};}|{"type":"B";"value":types.Inner;}|{"type":"C";"value":[types.Inner,types.Inner];}|{"type":"D";}); +export namespace types { + export type Inner = { + "x": boolean; + }; + export type Test = ({ + "type": "A"; + "value": { + "a": types.Inner; + }; + } | { + "type": "B"; + "value": types.Inner; + } | { + "type": "C"; + "value": [types.Inner, types.Inner]; + } | { + "type": "D"; + }); } "# ); @@ -336,9 +421,13 @@ export type Test=({"type":"A";"value":{"a":types.Inner;};}|{"type":"B";"value":t assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Inner={"x":boolean;}; -export type Test=({"a":types.Inner;}|types.Inner|[types.Inner,types.Inner]|null); +export namespace types { + export type Inner = { + "x": boolean; + }; + export type Test = ({ + "a": types.Inner; + } | types.Inner | [types.Inner, types.Inner] | null); } "# ); @@ -370,9 +459,19 @@ export type Test=({"a":types.Inner;}|types.Inner|[types.Inner,types.Inner]|null) assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Inner={"x":boolean;}; -export type Test=({"A":{"a":types.Inner;};}|{"B":types.Inner;}|{"C":[types.Inner,types.Inner];}|"D"); +export namespace types { + export type Inner = { + "x": boolean; + }; + export type Test = ({ + "A": { + "a": types.Inner; + }; + } | { + "B": types.Inner; + } | { + "C": [types.Inner, types.Inner]; + } | "D"); } "# ); @@ -423,50 +522,63 @@ export type Test=({"A":{"a":types.Inner;};}|{"B":types.Inner;}|{"C":[types.Inner assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export namespace test{ - -/** - * struct `Test3` - */ -export type Test3={ -/** - * field `a` of `Test3` - */ -"a":string;};} - -/** - * struct `Test2` - */ -export type Test2={ -/** - * field `a` of `Test2` - */ -"a":types.test.Test3;}; - -/** - * enum `Test` - */ -export type Test=({ -/** - * struct variant `Test::A` - */ -"A":{ -/** - * struct variant field `Test::A.a` - */ -"a":types.Test2;};}|{ -/** - * newtype variant `Test::B` - */ -"B":string;}|{ -/** - * tuple variant `Test::C` - */ -"C":[string,string];}| -/** - * unit variant `Test::D` - */ +export namespace types { + export namespace test { + + /** + * struct `Test3` + */ + export type Test3 = { + + /** + * field `a` of `Test3` + */ + "a": string; + }; + } + + /** + * struct `Test2` + */ + export type Test2 = { + + /** + * field `a` of `Test2` + */ + "a": types.test.Test3; + }; + + /** + * enum `Test` + */ + export type Test = ({ + + /** + * struct variant `Test::A` + */ + "A": { + + /** + * struct variant field `Test::A.a` + */ + "a": types.Test2; + }; + } | { + + /** + * newtype variant `Test::B` + */ + "B": string; + } | { + + /** + * tuple variant `Test::C` + */ + "C": [string, string]; + } | + /** + * unit variant `Test::D` + */ "D"); } "# @@ -504,9 +616,15 @@ export type Test=({ assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type struct={"field":string;}; -export type enum=("variant1"|{"variant2":{"field":types.struct;};}); +export namespace types { + export type struct = { + "field": string; + }; + export type enum = ("variant1" | { + "variant2": { + "field": types.struct; + }; + }); } "# ); @@ -523,9 +641,12 @@ export type enum=("variant1"|{"variant2":{"field":types.struct;};}); assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export namespace x.y.z{ -export type Test={"a":string;};} +export namespace types { + export namespace x.y.z { + export type Test = { + "a": string; + }; + } } "# ); @@ -563,10 +684,17 @@ export type Test={"a":string;};} assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Test={"a":Record;}; -export type Usize=number; -export type Test2={"a":types.Test;"b":(types.Test|null);"c":Record>;"d":types.Test>;}; +export namespace types { + export type Test = { + "a": Record; + }; + export type Usize = number; + export type Test2 = { + "a": types.Test; + "b": (types.Test | null); + "c": Record>; + "d": types.Test>; + }; } "# ); @@ -589,8 +717,11 @@ export type Test2={"a":types.Test;"b":(types.Test|null);"c" assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Test={"a"?:string;"b"?:string;}; +export namespace types { + export type Test = { + "a"?: string; + "b"?: string; + }; } "# ); @@ -633,11 +764,18 @@ export type Test={"a"?:string;"b"?:string;}; assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type Test3={"bar":string;}; -export type ExternalStringWrapper=string; -export type I16=number; -export type Test=(types.Test3&{"a":string;"b":types.ExternalStringWrapper;"c":string;"d":types.I16;}); +export namespace types { + export type Test3 = { + "bar": string; + }; + export type ExternalStringWrapper = string; + export type I16 = number; + export type Test = (types.Test3 & { + "a": string; + "b": types.ExternalStringWrapper; + "c": string; + "d": types.I16; + }); } "# ); @@ -660,8 +798,10 @@ export type Test=(types.Test3&{"a":string;"b":types.ExternalStringWrapper;"c":st assert_eq_str!( result, - r#"export type Usize=number; -export type Test={"a":Usize;}; + r#"export type Usize = number; +export type Test = { + "a": Usize; +}; "# ); } @@ -685,7 +825,7 @@ mod json_value { test_emit::(), r#"export default types; export namespace types{ -export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;}); +export type JSONValue=(null|boolean|number|string|(JSONValue)[] | {[key:string]:JSONValue;}); export type Test={"a":string;"b":types.JSONValue;"c":number;}; } "# From 76fad74c2a2f09374a3b14f5e5b74817c169b7ff Mon Sep 17 00:00:00 2001 From: HoLLy Date: Fri, 11 Aug 2023 23:39:05 +0200 Subject: [PATCH 3/3] Fix failing unit test --- tests/test.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index 1164b33..748f934 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -824,9 +824,15 @@ mod json_value { assert_eq_str!( test_emit::(), r#"export default types; -export namespace types{ -export type JSONValue=(null|boolean|number|string|(JSONValue)[] | {[key:string]:JSONValue;}); -export type Test={"a":string;"b":types.JSONValue;"c":number;}; +export namespace types { + export type JSONValue = (null | boolean | number | string | (JSONValue)[] | { + [key:string]:JSONValue; + }); + export type Test = { + "a": string; + "b": types.JSONValue; + "c": number; + }; } "# );