From 45ebb2c0d7de4b2bf78d788c52069d3a29880006 Mon Sep 17 00:00:00 2001 From: Fabian Stamm Date: Mon, 26 May 2025 23:54:41 +0200 Subject: [PATCH] Working on implementing the typescript target --- libjrpc/src/compile.rs | 24 +- libjrpc/src/targets/mod.rs | 1 + libjrpc/src/targets/rust.rs | 78 +++--- libjrpc/src/targets/typescript.rs | 398 ++++++++++++++++++++++++++++++ src/main.rs | 24 +- 5 files changed, 472 insertions(+), 53 deletions(-) create mode 100644 libjrpc/src/targets/typescript.rs diff --git a/libjrpc/src/compile.rs b/libjrpc/src/compile.rs index 2f29104..30d59b0 100644 --- a/libjrpc/src/compile.rs +++ b/libjrpc/src/compile.rs @@ -45,7 +45,29 @@ impl CompileContext { } } - pub fn write_file(&self, filename: &str, content: String) -> Result<()> { + pub fn to_snake(name: &str) -> String { + let mut result = String::new(); + let mut last_upper = false; + for c in name.chars() { + if c.is_uppercase() { + if last_upper { + result.push(c.to_ascii_lowercase()); + } else { + if !result.is_empty() { + result.push('_'); + } + result.push(c.to_ascii_lowercase()); + } + last_upper = true; + } else { + result.push(c); + last_upper = false; + } + } + result + } + + pub fn write_file(&self, filename: &str, content: &str) -> Result<()> { let res_path = self.output_folder.clone().join(filename); let res_dir = res_path.parent().context("Path has no parent!")?; std::fs::create_dir_all(res_dir)?; diff --git a/libjrpc/src/targets/mod.rs b/libjrpc/src/targets/mod.rs index b76f306..b3d114a 100644 --- a/libjrpc/src/targets/mod.rs +++ b/libjrpc/src/targets/mod.rs @@ -6,6 +6,7 @@ use crate::{ }; pub mod rust; +pub mod typescript; pub fn compile(ir: IR, output: &str) -> Result<()> { let mut ctx = CompileContext::new(output); diff --git a/libjrpc/src/targets/rust.rs b/libjrpc/src/targets/rust.rs index 7549109..bf76efd 100644 --- a/libjrpc/src/targets/rust.rs +++ b/libjrpc/src/targets/rust.rs @@ -63,28 +63,6 @@ impl RustCompiler { } } - fn to_snake(name: &str) -> String { - let mut result = String::new(); - let mut last_upper = false; - for c in name.chars() { - if c.is_uppercase() { - if last_upper { - result.push(c.to_ascii_lowercase()); - } else { - if !result.is_empty() { - result.push('_'); - } - result.push(c.to_ascii_lowercase()); - } - last_upper = true; - } else { - result.push(c); - last_upper = false; - } - } - result - } - fn generate_service_lib(ctx: &CompileContext, ir: &IR) -> Result<()> { let mut f = FileGenerator::new(); let mut fc = FileGenerator::new(); @@ -96,32 +74,40 @@ impl RustCompiler { for step in ir.steps.iter() { match step { Step::Type(def) => { - f.a0(format!("mod {};", Self::to_snake(&def.name))); + f.a0(format!("mod {};", CompileContext::to_snake(&def.name))); f.a( 0, - format!("pub use {}::{};", Self::to_snake(&def.name), def.name), + format!( + "pub use {}::{};", + CompileContext::to_snake(&def.name), + def.name + ), ); } Step::Enum(def) => { - f.a0(format!("mod {};", Self::to_snake(&def.name))); + f.a0(format!("mod {};", CompileContext::to_snake(&def.name))); f.a( 0, - format!("pub use {}::{};", Self::to_snake(&def.name), def.name), + format!( + "pub use {}::{};", + CompileContext::to_snake(&def.name), + def.name + ), ); } Step::Service(def) => { - fs.a0(format!("mod {};", Self::to_snake(&def.name))); + fs.a0(format!("mod {};", CompileContext::to_snake(&def.name))); fs.a0(format!( "pub use {}::{{ {}, {}Handler }};", - Self::to_snake(&def.name), + CompileContext::to_snake(&def.name), def.name, def.name )); - fc.a0(format!("mod {};", Self::to_snake(&def.name))); + fc.a0(format!("mod {};", CompileContext::to_snake(&def.name))); fc.a0(format!( "pub use {}::{};", - Self::to_snake(&def.name), + CompileContext::to_snake(&def.name), def.name, )); } @@ -131,9 +117,9 @@ impl RustCompiler { f.a0("pub mod server;"); f.a0("pub mod client;"); - ctx.write_file("src/lib.rs", f.get_content())?; - ctx.write_file("src/server/mod.rs", fs.get_content())?; - ctx.write_file("src/client/mod.rs", fc.get_content())?; + ctx.write_file("src/lib.rs", &f.get_content())?; + ctx.write_file("src/server/mod.rs", &fs.get_content())?; + ctx.write_file("src/client/mod.rs", &fc.get_content())?; Ok(()) } @@ -304,8 +290,11 @@ impl RustCompiler { f.a0("}"); ctx.write_file( - &format!("src/server/{}.rs", Self::to_snake(&definition.name)), - f.into_content(), + &format!( + "src/server/{}.rs", + CompileContext::to_snake(&definition.name) + ), + &f.into_content(), )?; Ok(()) @@ -396,8 +385,11 @@ impl RustCompiler { f.a0("}"); ctx.write_file( - &format!("src/client/{}.rs", Self::to_snake(&definition.name)), - f.into_content(), + &format!( + "src/client/{}.rs", + CompileContext::to_snake(&definition.name) + ), + &f.into_content(), )?; Ok(()) @@ -428,14 +420,14 @@ impl Compile for RustCompiler { fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { ctx.write_file( "Cargo.toml", - include_str!("../../templates/Rust/Cargo.toml") + &include_str!("../../templates/Rust/Cargo.toml") .to_owned() .replace("__name__", &self.crate_name), )?; ctx.write_file( "src/base_lib.rs", - include_str!("../../templates/Rust/src/lib.rs").to_owned(), + &include_str!("../../templates/Rust/src/lib.rs").to_owned(), )?; Ok(()) @@ -518,8 +510,8 @@ impl Compile for RustCompiler { f.a0("}"); ctx.write_file( - &format!("src/{}.rs", Self::to_snake(&definition.name)), - f.into_content(), + &format!("src/{}.rs", CompileContext::to_snake(&definition.name)), + &f.into_content(), )?; Ok(()) @@ -544,8 +536,8 @@ impl Compile for RustCompiler { f.a0("}"); ctx.write_file( - &format!("src/{}.rs", Self::to_snake(&definition.name)), - f.into_content(), + &format!("src/{}.rs", CompileContext::to_snake(&definition.name)), + &f.into_content(), )?; Ok(()) diff --git a/libjrpc/src/targets/typescript.rs b/libjrpc/src/targets/typescript.rs new file mode 100644 index 0000000..58e19ae --- /dev/null +++ b/libjrpc/src/targets/typescript.rs @@ -0,0 +1,398 @@ +use anyhow::Result; +use log::{info, warn}; +use std::collections::{HashMap, HashSet}; + +use crate::compile::{Compile, CompileContext, FileGenerator}; +use crate::ir::{EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; +use crate::shared::Keywords; +use crate::IR; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Flavour { + ESM, + Node, +} + +pub struct TypeScriptCompiler { + flavour: Flavour, +} + +static TS_KEYWORDS: [&'static str; 51] = [ + "abstract", + "arguments", + "await", + "boolean", + "break", + "byte", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "final", + "finally", + "for", + "function", + "goto", + "if", + "implements", + "import", + "in", + "instanceof", + "interface", + "let", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", +]; + +impl TypeScriptCompiler { + fn type_to_typescript(typ: &Type) -> String { + match typ { + Type::String => "string".to_string(), + Type::Int => "number".to_string(), + Type::Float => "number".to_string(), + Type::Bool => "boolean".to_string(), + Type::Bytes => "Uint8Array".to_string(), + Type::Void => "void".to_string(), + Type::Custom(name) => name.clone(), + } + } + + fn type_to_typescript_ext( + typ: &Type, + optional: bool, + array: bool, + map: &Option, + ) -> String { + let mut result = Self::type_to_typescript(typ); + if optional { + result = format!("({} | undefined)", result); + } + if array { + result = format!("({})[]", result); + } + if let Some(map) = map { + result = format!( + "{{ [key: {} ]: {} }}", + Self::type_to_typescript(map), + result + ); + } + result + } + + fn add_dependencies( + &mut self, + file: &mut FileGenerator, + depends: &HashSet, + ) -> Result<()> { + let esm = if self.flavour == Flavour::ESM { + ".js" + } else { + "" + }; + file.a0(format!( + "import {{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }} from \"./ts_base{esm}\"")); + for dep in depends { + match dep { + Type::Custom(name) => { + file.a0(&format!( + "import {name}, {{ apply_{name} }} from \"./{name}{esm}\";" + )); + } + _ => {} + } + } + file.a0(""); + file.a0(""); + Ok(()) + } + + fn fix_keyword_name(name: &str) -> String { + if TS_KEYWORDS.contains(&name) { + format!("{}_", name) + } else { + name.to_string() + } + } + + fn generate_service_lib(ctx: &CompileContext, ir: &IR) -> Result<()> { + let mut f = FileGenerator::new(); + let mut fc = FileGenerator::new(); + let mut fs = FileGenerator::new(); + + Ok(()) + } + + fn generate_service_server( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + Ok(()) + } + + fn generate_service_client( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + let esm = if self.flavour == Flavour::ESM { + ".js" + } else { + "" + }; + + ctx.write_file("ts_service_client.ts", &format!(" + import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./service_base{esm}\"; + import {{ VerificationError }} from \"./ts_base{esm}\"; + + {} + ", include_str!("../../templates/TypeScript/ts_service_client.ts")))?; + + let mut f = FileGenerator::new(); + self.add_dependencies(&mut f, &definition.depends)?; + + f.a0(format!( + "import {{ Service, ServiceProvider, getRandomID }} from \"./service_client{esm}\"" + )); + + f.a0("export type {"); + for dep in &definition.depends { + f.a1(format!("{},", Self::type_to_typescript(&dep))); + } + f.a0("}"); + + f.a0(format!( + "export class {} extends Service {{", + definition.name + )); + f.a1("constructor(provider: ServiceProvider) {"); + f.a2(format!("super(provider, \"{}\");", definition.name)); + f.a1("}"); + + //TODO: Change the way methods are implemented in a way, that the jsonrpc, etc. fields are exportedf into the ServiceProvider class. This should make the actual function body a lot easier! + + Ok(()) + } +} + +impl Compile for TypeScriptCompiler { + fn new(options: &HashMap) -> Result { + let flavour = options + .get("flavour") + .cloned() + .unwrap_or_else(|| "node".to_string()); + info!("TypeScript target initialized with flavour: {}", flavour); + Ok(TypeScriptCompiler { + flavour: Flavour::ESM, + }) + } + + fn name(&self) -> String { + match self.flavour { + Flavour::ESM => "ts-esm".to_string(), + Flavour::Node => "ts-node".to_string(), + } + } + + fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { + ctx.write_file( + "ts_base.ts", + include_str!("../../templates/TypeScript/ts_base.ts"), + )?; + + Ok(()) + } + + fn generate_type( + &mut self, + ctx: &mut CompileContext, + definition: &TypeDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + self.add_dependencies(&mut f, &definition.depends)?; + + f.a0(format!("export default class {} {{", definition.name)); + for field in definition.fields.iter() { + let typ = + Self::type_to_typescript_ext(&field.typ, field.optional, field.array, &field.map); + + f.a1(format!( + "public {}: {};", + Self::fix_keyword_name(&field.name), + typ + )); + } + f.a0(""); + f.a1(format!( + "constructor(init?: Partial<{}>) {{", + definition.name + )); + f.a2("if(init) {"); + for field in definition.fields.iter() { + f.a3(format!("if(init.{})", field.name)); + f.a4(format!("this.{} = init[\"{}\"]", field.name, field.name)); + } + f.a2("}"); + f.a1("}"); + + f.a0(""); + f.a1(format!("static apply(data: {}) {{", definition.name)); + f.a2(format!("apply_{}(data);", definition.name)); + f.a1("}"); + + f.a0("}"); + + f.a0(format!( + "export function apply_{}(data: {}) {{", + definition.name, definition.name + )); + + f.a1(format!( + "if(typeof data !== \"object\") throw new VerificationError(\"{}\", undefined, data);", + definition.name + )); + f.a1(format!("let res = new {}() as any;", definition.name)); + + for field in definition.fields.iter() { + if field.optional { + f.a1(format!( + "if (data.{} !== null && data.{} !== undefined ) {{", + field.name, field.name + )); + } else { + f.a1(format!( + "if (data.{} === null || data.{} === undefined ) throw new VerificationError(\"{}\", \"{}\", data.{});", + field.name, field.name, + definition.name, + field.name, + field.name + )); + f.a1("else {"); + } + + if field.array { + f.a2(format!( + "if (!Array.isArray(data.{})) throw new VerificationError(\"array\", \"{}\", data.{});", + field.name, + definition.name, + field.name + )); + f.a2(format!( + "res.{} = data.{}.map(elm => apply_{}(elm));", + field.name, + field.name, + Self::type_to_typescript(&field.typ), + )); + } else if let Some(map) = &field.map { + f.a2(format!( + "if (typeof data.{} != \"object\") throw new VerificationError(\"map\", \"{}\", data.{}); ", + field.name, + definition.name, + field.name + )); + f.a2(format!("res.{} = {{}}", field.name)); + f.a2(format!( + "Object.entries(data.{}).forEach(([key, val]) => res.{}[key] = apply_{}(val));", + field.name, + field.name, + Self::type_to_typescript(&field.typ), + )); + } else { + f.a2(format!( + "res.{} = apply_{}(data.{})", + field.name, + Self::type_to_typescript(&field.typ), + field.name + )); + } + f.a1("}"); + } + f.a1("return res;"); + + f.a0("}"); + + ctx.write_file(&format!("{}.ts", definition.name), &f.into_content())?; + + Ok(()) + } + + fn generate_enum( + &mut self, + ctx: &mut CompileContext, + definition: &EnumDefinition, + ) -> anyhow::Result<()> { + let mut f = FileGenerator::new(); + + self.add_dependencies(&mut f, &HashSet::new())?; + + f.a0(format!("enum {} {{", definition.name)); + for value in &definition.values { + f.a1(format!("{}={},", value.name, value.value)); + } + f.a0("}"); + f.a0(""); + f.a0(format!("export default {};", definition.name)); + f.a0(""); + f.a0(format!( + "export function apply_{}(data: {}): {} {{", + definition.name, definition.name, definition.name + )); + f.a1("data = Number(data);"); + f.a1(format!( + "if ({}[data] == undefined) throw new VerificationError(\"{}\", undefined, data);", + definition.name, definition.name + )); + f.a1("return data;"); + f.a0("}"); + + ctx.write_file(&format!("{}.ts", definition.name), &f.into_content())?; + + Ok(()) + } + + fn generate_service( + &mut self, + ctx: &mut CompileContext, + definition: &ServiceDefinition, + ) -> anyhow::Result<()> { + self.generate_service_client(ctx, definition)?; + self.generate_service_server(ctx, definition)?; + Ok(()) + } + + fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> anyhow::Result<()> { + Self::generate_service_lib(ctx, ir)?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 14b0348..2144cf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ use anyhow::Result; use clap::{Parser, Subcommand}; -use libjrpc::{targets::rust::RustCompiler, FileProcessor}; +use libjrpc::{ + targets::{rust::RustCompiler, typescript::TypeScriptCompiler}, + FileProcessor, +}; #[cfg(test)] mod test; @@ -44,15 +47,18 @@ pub fn main() -> Result<()> { let ir = fp.start_compile(&input)?; let output_split = output.split(':').collect::>(); - let output_target = output_split[0]; - let output_dir = output_split[1]; + if output_split.len() != 2 { + println!("The output must follow the structure: "); + } else { + let output_target = output_split[0]; + let output_dir = output_split[1]; - match output_target { - "rust" => { - libjrpc::targets::compile::(ir, output_dir)?; - } - _ => { - println!("Unsupported target: {}", output_target); + match output_target { + "rust" => libjrpc::targets::compile::(ir, output_dir)?, + "ts-node" => libjrpc::targets::compile::(ir, output_dir)?, + _ => { + println!("Unsupported target: {}", output_target); + } } }