diff --git a/libjrpc/src/compile.rs b/libjrpc/src/compile.rs index 30d59b0..e6b2967 100644 --- a/libjrpc/src/compile.rs +++ b/libjrpc/src/compile.rs @@ -67,6 +67,24 @@ impl CompileContext { result } + pub fn strip_template_ignores(content: &str) -> String { + let mut result = String::new(); + let mut ignore = false; + for line in content.lines() { + if ignore { + ignore = false; + continue; + } + if line.trim().contains("@template-ignore") { + ignore = true; + continue; + } + result.push_str(line); + result.push_str("\n"); + } + 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!")?; diff --git a/libjrpc/src/ir.rs b/libjrpc/src/ir.rs index 9680541..fb5bc28 100644 --- a/libjrpc/src/ir.rs +++ b/libjrpc/src/ir.rs @@ -31,7 +31,7 @@ pub enum Step { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Type { +pub enum BaseType { Int, Float, String, @@ -41,57 +41,143 @@ pub enum Type { Custom(String), } -impl ToString for Type { +impl ToString for BaseType { fn to_string(&self) -> String { match self { - Type::Int => "int".to_string(), - Type::Float => "float".to_string(), - Type::String => "string".to_string(), - Type::Bool => "bool".to_string(), - Type::Bytes => "bytes".to_string(), - Type::Void => "void".to_string(), - Type::Custom(name) => name.clone(), + BaseType::Int => "int".to_string(), + BaseType::Float => "float".to_string(), + BaseType::String => "string".to_string(), + BaseType::Bool => "bool".to_string(), + BaseType::Bytes => "bytes".to_string(), + BaseType::Void => "void".to_string(), + BaseType::Custom(name) => name.clone(), } } } -impl Hash for Type { +impl Hash for BaseType { fn hash(&self, state: &mut H) { self.to_string().hash(state); } } -impl From<&String> for Type { +impl From<&String> for BaseType { fn from(value: &String) -> Self { Self::from(value.as_str()) } } -impl From for Type { +impl From for BaseType { fn from(value: String) -> Self { Self::from(value.as_str()) } } -impl From<&str> for Type { +impl From<&str> for BaseType { fn from(s: &str) -> Self { match s { - "int" => Type::Int, - "float" => Type::Float, - "string" => Type::String, - "bool" => Type::Bool, - "boolean" => Type::Bool, - "bytes" => Type::Bytes, - "void" => Type::Void, - _ => Type::Custom(s.to_string()), + "int" => BaseType::Int, + "float" => BaseType::Float, + "string" => BaseType::String, + "bool" => BaseType::Bool, + "boolean" => BaseType::Bool, + "bytes" => BaseType::Bytes, + "void" => BaseType::Void, + _ => BaseType::Custom(s.to_string()), } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TypeModifier { + None, + Array, + Optional, + Map(BaseType), + OptionalArray, + OptionalMap(BaseType), + OptionalMapArray(BaseType), + MapArray(BaseType), +} + +impl TypeModifier { + pub fn from_flags(optional: bool, array: bool, map: Option) -> Self { + match (optional, array, map) { + (false, false, None) => Self::None, + (false, true, None) => Self::Array, + (true, false, None) => Self::Optional, + (true, true, None) => Self::OptionalArray, + (false, false, Some(map)) => Self::Map(map), + (false, true, Some(map)) => Self::MapArray(map), + (true, false, Some(map)) => Self::OptionalMap(map), + (true, true, Some(map)) => Self::OptionalMapArray(map), + } + } + + pub fn get_flags(&self) -> (bool, bool, Option) { + match self.clone() { + Self::None => (false, false, None), + Self::Array => (false, true, None), + Self::Optional => (true, false, None), + Self::OptionalArray => (true, true, None), + Self::Map(map) => (false, false, Some(map)), + Self::MapArray(map) => (false, true, Some(map)), + Self::OptionalMap(map) => (true, false, Some(map)), + Self::OptionalMapArray(map) => (true, true, Some(map)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Type(pub BaseType, pub TypeModifier); + +impl Type { + pub fn from_flags(base: BaseType, optional: bool, array: bool, map: Option) -> Self { + Self(base, TypeModifier::from_flags(optional, array, map)) + } + + /// Returns flags in the order of: (OPTIONAL, ARRAY, MAP) + pub fn into_flags(self) -> (BaseType, bool, bool, Option) { + let b = self.0.clone(); + match self.1 { + TypeModifier::None => (b, false, false, None), + TypeModifier::Array => (b, false, true, None), + TypeModifier::Optional => (b, true, false, None), + TypeModifier::OptionalArray => (b, true, true, None), + TypeModifier::Map(map) => (b, false, false, Some(map)), + TypeModifier::MapArray(map) => (b, false, true, Some(map)), + TypeModifier::OptionalMap(map) => (b, true, false, Some(map)), + TypeModifier::OptionalMapArray(map) => (b, true, true, Some(map)), + } + } + + pub fn is_optional(&self) -> bool { + self.1.get_flags().0 + } + + pub fn is_array(&self) -> bool { + self.1.get_flags().1 + } + + pub fn is_map(&self) -> bool { + self.1.get_flags().2.is_some() + } + + pub fn get_map(&self) -> Option { + self.1.get_flags().2.clone() + } +} +// { +// pub base: BaseType, +// pub array: bool, +// pub optional: bool, +// pub map: Option, +// } + #[derive(Debug, Clone)] pub struct TypeDefinition { pub name: String, - pub depends: HashSet, + pub depends: HashSet, pub fields: Vec, pub position: ParserPosition, } @@ -106,9 +192,6 @@ impl Definition for TypeDefinition { pub struct Field { pub name: String, pub typ: Type, - pub array: bool, - pub optional: bool, - pub map: Option, } #[derive(Debug, Clone)] @@ -133,7 +216,7 @@ pub struct EnumField { #[derive(Debug, Clone)] pub struct ServiceDefinition { pub name: String, - pub depends: HashSet, + pub depends: HashSet, pub methods: Vec, pub position: ParserPosition, } @@ -156,14 +239,11 @@ pub struct Method { pub struct MethodInput { pub name: String, pub typ: Type, - pub array: bool, - pub optional: bool, } #[derive(Debug, Clone)] pub struct MethodOutput { pub typ: Type, - pub array: bool, } #[derive(Debug, Clone)] @@ -182,7 +262,7 @@ fn build_type(stmt: &TypeStatement) -> Result { }; for field in &stmt.fields { - let typ = Type::from(&field.fieldtype); + let typ = BaseType::from(&field.fieldtype); typedef.depends.insert(typ.clone()); if let Some(maptype) = &field.map { @@ -193,10 +273,12 @@ fn build_type(stmt: &TypeStatement) -> Result { typedef.fields.push(Field { name: field.name.clone(), - typ: typ.clone(), - array: field.array, - optional: field.optional, - map: field.map.as_ref().map(|s| Type::from(s)), + typ: Type::from_flags( + typ.clone(), + field.optional, + field.array, + field.map.as_ref().map(|s| BaseType::from(s)), + ), }); } @@ -247,14 +329,11 @@ fn build_service(stmt: &ServiceStatement) -> Result { name: method.name.clone(), inputs: Vec::new(), output: method.return_type.as_ref().map(|rt| { - let typ = Type::from(&rt.fieldtype); - if typ != Type::Void { - servdef.depends.insert(typ.clone()); - } - MethodOutput { - typ, - array: rt.array, + let typ = Type::from_flags(BaseType::from(&rt.fieldtype), false, rt.array, None); + if typ.0 != BaseType::Void { + servdef.depends.insert(typ.0.clone()); } + MethodOutput { typ } }), decorators: MethodDecorators { description: None, @@ -265,7 +344,7 @@ fn build_service(stmt: &ServiceStatement) -> Result { let mut optional_starts = false; for inp in &method.inputs { - let typ = Type::from(&inp.fieldtype); + let typ = BaseType::from(&inp.fieldtype); servdef.depends.insert(typ.clone()); if optional_starts && !inp.optional { @@ -280,9 +359,7 @@ fn build_service(stmt: &ServiceStatement) -> Result { methoddef.inputs.push(MethodInput { name: inp.name.clone(), - typ, - array: inp.array, - optional: inp.optional, + typ: Type::from_flags(typ.clone(), inp.optional, inp.array, None), }); } @@ -426,7 +503,7 @@ pub fn build_ir(root: &Vec) -> Result { match step { Step::Type(typedef) => { for dep in &typedef.depends { - if let Type::Custom(dep) = dep { + if let BaseType::Custom(dep) = dep { if !all_types.contains(dep) { return Err(IRError::new_from_def( &format!("Type {} depends on unknown type {}", typedef.name, dep), @@ -439,7 +516,7 @@ pub fn build_ir(root: &Vec) -> Result { } Step::Service(servdef) => { for dep in &servdef.depends { - if let Type::Custom(dep) = dep { + if let BaseType::Custom(dep) = dep { if !all_types.contains(dep) { return Err(IRError::new_from_def( &format!( diff --git a/libjrpc/src/targets/rust.rs b/libjrpc/src/targets/rust.rs index bf76efd..240be8e 100644 --- a/libjrpc/src/targets/rust.rs +++ b/libjrpc/src/targets/rust.rs @@ -3,7 +3,9 @@ use log::warn; use std::collections::{HashMap, HashSet}; use crate::compile::{Compile, CompileContext, FileGenerator}; -use crate::ir::{EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; +use crate::ir::{ + BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition, TypeModifier, +}; use crate::shared::Keywords; use crate::IR; @@ -14,37 +16,43 @@ pub struct RustCompiler { static RUST_KEYWORDS: [&'static str; 6] = ["type", "return", "static", "pub", "enum", "self"]; impl RustCompiler { - fn type_to_rust(typ: &Type) -> String { + fn type_to_rust(typ: &BaseType) -> String { match typ { - Type::String => "String".to_string(), - Type::Int => "i64".to_string(), - Type::Float => "f64".to_string(), - Type::Bool => "bool".to_string(), - Type::Bytes => "Vec".to_string(), - Type::Void => "()".to_string(), - Type::Custom(name) => name.clone(), + BaseType::String => "String".to_string(), + BaseType::Int => "i64".to_string(), + BaseType::Float => "f64".to_string(), + BaseType::Bool => "bool".to_string(), + BaseType::Bytes => "Vec".to_string(), + BaseType::Void => "()".to_string(), + BaseType::Custom(name) => name.clone(), } } - fn type_to_rust_ext(typ: &Type, optional: bool, array: bool) -> String { - let mut result = Self::type_to_rust(typ); - if optional { - result = format!("Option<{}>", result); - } + fn type_to_rust_ext(typ: &Type) -> String { + let mut result = Self::type_to_rust(&typ.0); + + let (optional, array, map) = typ.1.get_flags(); + if array { result = format!("Vec<{}>", result); } + if let Some(map) = &map { + result = format!("HashMap<{}, {}>", Self::type_to_rust(&map), result); + } + if optional { + result = format!("Option<{}>", result); + } result } fn add_dependencies( &mut self, file: &mut FileGenerator, - depends: &HashSet, + depends: &HashSet, ) -> Result<()> { for dep in depends { match dep { - Type::Custom(name) => { + BaseType::Custom(name) => { file.a0(&format!("use crate::{};", name)); } _ => {} @@ -148,16 +156,16 @@ impl RustCompiler { format!( "{}: {}", Self::fix_keyword_name(&arg.name), - Self::type_to_rust_ext(&arg.typ, arg.optional, arg.array) + Self::type_to_rust_ext(&arg.typ) ) }) .collect::>() .join(", "); - let ret = method.output.as_ref().map_or_else( - || "()".to_owned(), - |r| Self::type_to_rust_ext(&r.typ, false, r.array), - ); + let ret = method + .output + .as_ref() + .map_or_else(|| "()".to_owned(), |r| Self::type_to_rust_ext(&r.typ)); f.a1("#[allow(non_snake_case)]"); f.a( @@ -238,7 +246,7 @@ impl RustCompiler { format!( ".map_err(|_| \"Parameter for field '{}' should be of type '{}'!\")?{}", arg.name, - arg.typ.to_string(), + arg.typ.0.to_string(), if i < method.inputs.len() - 1 { "," } else { "" } ), ); @@ -266,7 +274,7 @@ impl RustCompiler { format!( ".map_err(|_| \"Parameter for field {} should be of type '{}'!\")?{}", arg.name, - arg.typ.to_string(), + arg.typ.0.to_string(), if i < method.inputs.len() - 1 { "," } else { "" } ), ); @@ -332,13 +340,13 @@ impl RustCompiler { format!( "{}: {}", Self::fix_keyword_name(&arg.name), - Self::type_to_rust_ext(&arg.typ, arg.optional, arg.array) + Self::type_to_rust_ext(&arg.typ) ) }) .collect::>() .join(", "); let ret = method.output.as_ref().map_or("()".to_string(), |output| { - Self::type_to_rust_ext(&output.typ, false, output.array) + Self::type_to_rust_ext(&output.typ) }); f.a1("#[allow(non_snake_case)]"); @@ -369,7 +377,7 @@ impl RustCompiler { f.a2("let l_res = self.client.send_request(l_req).await;"); f.a2("match l_res {"); f.a3("Err(e) => Err(e),"); - if output.typ == Type::Void { + if output.typ.0 == BaseType::Void { f.a3("Ok(_) => Ok(())"); } else { f.a3("Ok(o) => serde_json::from_value(o).map_err(|e| Box::from(e))"); @@ -440,7 +448,11 @@ impl Compile for RustCompiler { ) -> anyhow::Result<()> { let mut f = FileGenerator::new(); - if definition.fields.iter().any(|e| e.map.is_some()) { + if definition + .fields + .iter() + .any(|e| e.typ.1.get_flags().2.is_some()) + { f.a0("use std::collections::hash_map::HashMap;") } f.a0("use serde::{Deserialize, Serialize};"); @@ -451,7 +463,6 @@ impl Compile for RustCompiler { f.a0(format!("pub struct {} {{", definition.name)); for field in definition.fields.iter() { f.a(1, "#[allow(non_snake_case)]"); - let func = format!("pub {}:", Self::fix_keyword_name(&field.name)); if Keywords::is_keyword(&field.name) { warn!( @@ -462,49 +473,11 @@ impl Compile for RustCompiler { f.a(1, format!("#[serde(rename = \"{}\")]", field.name)); } - let mut opts = String::new(); - let mut opte = String::new(); - - if field.optional { - opts = "Option<".to_string(); - opte = ">".to_string(); - } - - if field.array { - f.a( - 1, - format!( - "{} {}Vec<{}>{},", - func, - opts, - Self::type_to_rust(&field.typ), - opte - ), - ); - } else if let Some(map) = &field.map { - f.a( - 1, - format!( - "{} {}HashMap<{}, {}>{},", - func, - opts, - Self::type_to_rust(map), - Self::type_to_rust(&field.typ), - opte - ), - ); - } else { - f.a( - 1, - format!( - "{} {}{}{},", - func, - opts, - Self::type_to_rust(&field.typ), - opte - ), - ); - } + f.a1(format!( + "pub {}: {}", + Self::fix_keyword_name(&field.name), + Self::type_to_rust_ext(&field.typ) + )); } f.a0("}"); diff --git a/libjrpc/src/targets/typescript.rs b/libjrpc/src/targets/typescript.rs index 58e19ae..c04dd52 100644 --- a/libjrpc/src/targets/typescript.rs +++ b/libjrpc/src/targets/typescript.rs @@ -3,7 +3,7 @@ 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::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; use crate::shared::Keywords; use crate::IR; @@ -17,7 +17,7 @@ pub struct TypeScriptCompiler { flavour: Flavour, } -static TS_KEYWORDS: [&'static str; 51] = [ +static TS_KEYWORDS: [&'static str; 52] = [ "abstract", "arguments", "await", @@ -69,28 +69,25 @@ static TS_KEYWORDS: [&'static str; 51] = [ "while", "with", "yield", + "static", ]; impl TypeScriptCompiler { - fn type_to_typescript(typ: &Type) -> String { + fn type_to_typescript(typ: &BaseType) -> 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(), + BaseType::String => "string".to_string(), + BaseType::Int => "number".to_string(), + BaseType::Float => "number".to_string(), + BaseType::Bool => "boolean".to_string(), + BaseType::Bytes => "Uint8Array".to_string(), + BaseType::Void => "void".to_string(), + BaseType::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); + fn type_to_typescript_ext(typ: &Type) -> String { + let mut result = Self::type_to_typescript(&typ.0); + let (optional, array, map) = typ.1.get_flags(); if optional { result = format!("({} | undefined)", result); } @@ -100,7 +97,7 @@ impl TypeScriptCompiler { if let Some(map) = map { result = format!( "{{ [key: {} ]: {} }}", - Self::type_to_typescript(map), + Self::type_to_typescript(&map), result ); } @@ -110,7 +107,7 @@ impl TypeScriptCompiler { fn add_dependencies( &mut self, file: &mut FileGenerator, - depends: &HashSet, + depends: &HashSet, ) -> Result<()> { let esm = if self.flavour == Flavour::ESM { ".js" @@ -121,7 +118,7 @@ impl TypeScriptCompiler { "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) => { + BaseType::Custom(name) => { file.a0(&format!( "import {name}, {{ apply_{name} }} from \"./{name}{esm}\";" )); @@ -155,8 +152,125 @@ impl TypeScriptCompiler { ctx: &mut CompileContext, definition: &ServiceDefinition, ) -> anyhow::Result<()> { + let esm = if self.flavour == Flavour::ESM { + ".js" + } else { + "" + }; + + ctx.write_file("ts_service_server.ts", &format!(" +import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./ts_service_base{esm}\"; +import {{ VerificationError }} from \"./ts_base{esm}\"; + +{} + ", + CompileContext::strip_template_ignores(include_str!("../../templates/TypeScript/ts_service_server.ts"))))?; + let mut f = FileGenerator::new(); + f.a0(format!( + "import {{ Service }} from \"./ts_service_server{esm}\";" + )); + self.add_dependencies(&mut f, &definition.depends)?; + + f.a0(format!( + "export abstract class {} extends Service {{", + definition.name + )); + f.a1(format!("public name = \"{}\"", definition.name)); + f.a1(format!("constructor() {{")); + f.a2(format!("super();")); + for func in &definition.methods { + f.a2(format!("this.functions.add(\"{}\");", func.name)); + } + f.a1("}"); + f.a0(""); + + for func in &definition.methods { + let mut params = func + .inputs + .iter() + .map(|p| format!("{}: {}", p.name, Self::type_to_typescript_ext(&p.typ))) + .collect::>(); + params.push("ctx: T".to_string()); + let params = params.join(", "); + + if let Some(output) = &func.output { + f.a1(format!( + "public abstract {}({}): Promise<{}>;", + func.name, + params, + Self::type_to_typescript_ext(&output.typ) + )); + f.a1(format!( + "_{}(params: any[] | any, ctx: T): Promise<{}> {{", + func.name, + Self::type_to_typescript_ext(&output.typ) + )); + let params_str_arr = func + .inputs + .iter() + .map(|e| format!("\"{}\"", e.name)) + .collect::>() + .join(", "); + f.a2(format!( + "let p = this._into_parameters([{}], params)", + params_str_arr + )); + + for (index, input) in func.inputs.iter().enumerate() { + f.a2(format!( + "if(p[{}] !== null && p[{}] !== undefined) {{", + index, index + )); + if input.typ.array { + f.a2(format!("for (const elm of p[{}]) {{", index)); + f.a3(format!("apply_{}(elm);", input.name)); + f.a2(format!("}}")); + } else if let Some(_map) = &input.typ.map { + // TODO: Implement map type handling + panic!("Map in arguments is not allowed!"); + } else { + f.a3(format!( + "apply_{}(p[{}]);", + input.typ.base.to_string(), + index + )); + } + f.a2("}"); + if !input.typ.optional { + f.a2(format!( + "else throw new Error(`Missing required parameter ${}`);", + input.name + )); + } + f.a0(""); + } + + f.a2("p.push(ctx);"); + + let ret_apply = String::new(); + if output.typ. + + f.a2("//@ts-ignore This will cause a typescript error when strict checking, since p is not a tuple"); + f.a2(format!( + "return this.{}.call(this, ...p).then(res=>{});", + func.name, ret_apply + )); + + f.a1("}"); + } else { + f.a1(format!("public abstract {}({}): void;", func.name, params)); + } + + f.a0(""); + } + // f.a2(format!("}}")); + // f.a0(format!("")); + f.a0("}"); + + ctx.write_file(&format!("{}_server.ts", definition.name), &f.into_content())?; + Ok(()) } @@ -171,25 +285,31 @@ impl TypeScriptCompiler { "" }; - 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}\"; + ctx.write_file( + "ts_service_base.ts", + include_str!("../../templates/TypeScript/ts_service_base.ts"), + )?; - {} - ", include_str!("../../templates/TypeScript/ts_service_client.ts")))?; + ctx.write_file("ts_service_client.ts", &format!(" +import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./ts_service_base{esm}\"; +import {{ VerificationError }} from \"./ts_base{esm}\"; + +{} + ", + CompileContext::strip_template_ignores(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}\"" + "import {{ Service, ServiceProvider, getRandomID }} from \"./ts_service_client{esm}\"" )); - f.a0("export type {"); - for dep in &definition.depends { - f.a1(format!("{},", Self::type_to_typescript(&dep))); - } - f.a0("}"); + // 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 {{", @@ -199,7 +319,46 @@ impl TypeScriptCompiler { 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! + for fnc in &definition.methods { + let params = fnc + .inputs + .iter() + .map(|p| { + format!( + "{}: {}", + Self::fix_keyword_name(&p.name), + Self::type_to_typescript_ext(&p.typ) + ) + }) + .collect::>() + .join(", "); + + if let Some(output) = &fnc.output { + f.a1(format!( + "async {}({}): Promise<{}> {{", + fnc.name, + params, + Self::type_to_typescript_ext(&output.typ) + )); + f.a2("return new Promise((ok, err) => {"); + f.a3(format!( + "return this._provider.sendRequest(\"{}.{}\", [...arguments], {{ok, err}});", + definition.name, fnc.name + )); + f.a2("});"); + f.a1("}"); + } else { + f.a1(format!("{}({}): void {{", fnc.name, params,)); + f.a2(format!( + "this._provider.sendNotification(\"{}.{}\", [...arguments]);", + definition.name, fnc.name + )); + f.a1("}"); + } + } + f.a0("}"); + + ctx.write_file(&format!("{}_client.ts", definition.name), &f.into_content())?; Ok(()) } @@ -244,8 +403,7 @@ impl Compile for TypeScriptCompiler { 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); + let typ = Self::type_to_typescript_ext(&field.typ); f.a1(format!( "public {}: {};", @@ -285,7 +443,7 @@ impl Compile for TypeScriptCompiler { f.a1(format!("let res = new {}() as any;", definition.name)); for field in definition.fields.iter() { - if field.optional { + if field.typ.is_optional() { f.a1(format!( "if (data.{} !== null && data.{} !== undefined ) {{", field.name, field.name @@ -301,20 +459,7 @@ impl Compile for TypeScriptCompiler { 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 { + if field.typ.is_map() { f.a2(format!( "if (typeof data.{} != \"object\") throw new VerificationError(\"map\", \"{}\", data.{}); ", field.name, @@ -326,13 +471,36 @@ impl Compile for TypeScriptCompiler { "Object.entries(data.{}).forEach(([key, val]) => res.{}[key] = apply_{}(val));", field.name, field.name, - Self::type_to_typescript(&field.typ), + Self::type_to_typescript(&field.typ.base), )); + } + + + if field.typ.is_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.base), + )); + } + + if field.typ.array { + + } else if let Some(_map) = &field.typ.map { + + } else { f.a2(format!( "res.{} = apply_{}(data.{})", field.name, - Self::type_to_typescript(&field.typ), + Self::type_to_typescript(&field.typ.base), field.name )); } diff --git a/libjrpc/templates/TypeScript/ts_service_client.ts b/libjrpc/templates/TypeScript/ts_service_client.ts index 0048b83..90f49af 100644 --- a/libjrpc/templates/TypeScript/ts_service_client.ts +++ b/libjrpc/templates/TypeScript/ts_service_client.ts @@ -1,105 +1,128 @@ //@template-ignore import { VerificationError } from "./ts_base"; //@template-ignore -import { type RequestObject, type ResponseObject, ErrorCodes, Logging } from "./ts_service_base"; - +import { + //@template-ignore + type RequestObject, + //@template-ignore + type ResponseObject, + //@template-ignore + Logging, + //@template-ignore +} from "./ts_service_base"; export type IMessageCallback = (data: any) => void; export type ResponseListener = { - ok: (response:any)=>void; - err: (error: Error)=>void; -} + ok: (response: any) => void; + err: (error: Error) => void; +}; export class Service { - public _name: string = null as any; + public _name: string = null as any; - constructor(protected _provider: ServiceProvider, name: string) { - this._name = name; - this._provider.services.set(name, this); - } + constructor( + protected _provider: ServiceProvider, + name: string, + ) { + this._name = name; + this._provider.services.set(name, this); + } } export class ServiceProvider { - services = new Map(); - requests = new Map(); + services = new Map(); + requests = new Map(); - constructor(private sendPacket: IMessageCallback) {} + constructor(private sendPacket: IMessageCallback) {} - onPacket(msg: RequestObject | ResponseObject) { - Logging.log("CLIENT: Received message:", msg); - if("method" in msg) { - if(msg.id){ - Logging.log("CLIENT: Determined type is Request"); - // Request, which are not supported by client, so ignore - return; - } else { - Logging.log("CLIENT: Determined type is Notification"); - //Notification. Send to Notification handler - const [srvName, fncName] = msg.method.split("."); - let service = this.services.get(srvName) - if(!service) { - Logging.log("CLIENT: Did not find Service wanted by Notification!", srvName); - } else { - //TODO: Implement Event thingy (or so :)) - } - } + onPacket(msg: RequestObject | ResponseObject) { + Logging.log("CLIENT: Received message:", msg); + if ("method" in msg) { + if (msg.id) { + Logging.log("CLIENT: Determined type is Request"); + // Request, which are not supported by client, so ignore + return; } else { - Logging.log("CLIENT: Determined type is Response"); - // Response - let resListener = this.requests.get(msg.id); - if(!resListener) return; // Ignore wrong responses - if(msg.error) { - if(msg.error.data && msg.error.data.$ == "verification_error") { - resListener.err(new VerificationError(msg.error.data.type, msg.error.data.field, msg.error.data.value)) - } else { - resListener.err(new Error(msg.error.message)); - } - } else { - resListener.ok(msg.result); - } + Logging.log("CLIENT: Determined type is Notification"); + //Notification. Send to Notification handler + const [srvName, fncName] = msg.method.split("."); + let service = this.services.get(srvName); + if (!service) { + Logging.log( + "CLIENT: Did not find Service wanted by Notification!", + srvName, + ); + } else { + //TODO: Implement Event thingy (or so :)) + } } - - } + } else { + Logging.log("CLIENT: Determined type is Response"); + // Response + let resListener = this.requests.get(msg.id); + if (!resListener) return; // Ignore wrong responses + if (msg.error) { + if (msg.error.data && msg.error.data.$ == "verification_error") { + resListener.err( + new VerificationError( + msg.error.data.type, + msg.error.data.field, + msg.error.data.value, + ), + ); + } else { + resListener.err(new Error(msg.error.message)); + } + } else { + resListener.ok(msg.result); + } + } + } - sendMessage(msg: RequestObject, res?: ResponseListener) { - Logging.log("CLIENT: Sending Messgage", msg); - if(msg.id) { - this.requests.set(msg.id, res); - } - this.sendPacket(msg) - } + sendNotification(method: string, params: any[]) { + Logging.log("CLIENT: Sending Notification", method, params); + this.sendPacket({ + jsonrpc: "2.0", + method, + params, + }); + } + + sendRequest(method: string, params: any[], res?: ResponseListener) { + Logging.log("CLIENT: Sending Request", method, params); + const id = getRandomID(16); + this.requests.set(id, res); + this.sendPacket({ + jsonrpc: "2.0", + method, + params, + id, + }); + } } - - - - - - declare var require: any; export const getRandomBytes = ( - typeof self !== "undefined" && (self.crypto || (self as any).msCrypto) - ? function () { - // Browsers - var crypto = self.crypto || (self as any).msCrypto; - var QUOTA = 65536; - return function (n: number) { - var a = new Uint8Array(n); - for (var i = 0; i < n; i += QUOTA) { - crypto.getRandomValues( - a.subarray(i, i + Math.min(n - i, QUOTA)) - ); - } - return a; - }; - } - : function () { - // Node - return require("crypto").randomBytes; - } + typeof self !== "undefined" && (self.crypto || (self as any).msCrypto) + ? function () { + // Browsers + var crypto = self.crypto || (self as any).msCrypto; + var QUOTA = 65536; + return function (n: number) { + var a = new Uint8Array(n); + for (var i = 0; i < n; i += QUOTA) { + crypto.getRandomValues(a.subarray(i, i + Math.min(n - i, QUOTA))); + } + return a; + }; + } + : function () { + // Node + return require("crypto").randomBytes; + } )() as (cnt: number) => Uint8Array; export const getRandomID = (length: number) => { - return btoa(String.fromCharCode.apply(null, getRandomBytes(length) as any)); + return btoa(String.fromCharCode.apply(null, getRandomBytes(length) as any)); }; diff --git a/libjrpc/templates/TypeScript/ts_service_server.ts b/libjrpc/templates/TypeScript/ts_service_server.ts index 0a4e011..b6b283d 100644 --- a/libjrpc/templates/TypeScript/ts_service_server.ts +++ b/libjrpc/templates/TypeScript/ts_service_server.ts @@ -1,130 +1,162 @@ //@template-ignore import { VerificationError } from "./ts_base"; //@template-ignore -import { type RequestObject, type ResponseObject, ErrorCodes, Logging } from "./ts_service_base"; - +import { + //@template-ignore + type RequestObject, + //@template-ignore + type ResponseObject, + //@template-ignore + ErrorCodes, + //@template-ignore + Logging, + //@template-ignore +} from "./ts_service_base"; export class Service { - public name: string = null as any; - public functions = new Set(); + public name: string = null as any; + public functions = new Set(); - constructor() { } + _into_parameters(params: string[], value: any[] | any): any[] { + let p: any[] = []; + if (Array.isArray(value)) { + p = params; + if (p.length > params.length) { + throw new VerificationError(`Too many parameters provided`); + } + + while (p.length < params.length) { + p.push(undefined); + } + return value; + } else { + p = params.map((p) => value[p]); + } + + return p; + } + + constructor() {} } type ISendMessageCB = (data: any, catchedErr?: Error) => void; export class ServiceProvider { - services = new Map>(); - addService(service: Service) { - this.services.set(service.name, service); - Logging.log("SERVER: Adding Service to provider:", service.name); - Logging.log("SERVER: Service provides:", [...service.functions.keys()]) - } + services = new Map>(); + addService(service: Service) { + this.services.set(service.name, service); + Logging.log("SERVER: Adding Service to provider:", service.name); + Logging.log("SERVER: Service provides:", [...service.functions.keys()]); + } - getSession(send: ISendMessageCB, ctx?: Partial): Session { - return new Session(this, send, ctx); - } + getSession(send: ISendMessageCB, ctx?: Partial): Session { + return new Session(this, send, ctx); + } } class Session { - ctx: Partial; + ctx: Partial; - constructor( - private provider: ServiceProvider, - private _send: ISendMessageCB, - ctx?: Partial - ) { - this.ctx = ctx || {}; - } + constructor( + private provider: ServiceProvider, + private _send: ISendMessageCB, + ctx?: Partial, + ) { + this.ctx = ctx || {}; + } - send(data: any, catchedErr?: Error) { - Logging.log("SERVER: Sending Message", data) - this._send(data, catchedErr); - } + send(data: any, catchedErr?: Error) { + Logging.log("SERVER: Sending Message", data); + this._send(data, catchedErr); + } - async onMessage(data: RequestObject) { - Logging.log("SERVER: Received Message", data); - try { - if (!data.method) { - if (data.id) { - this.send({ - jsonrpc: "2.0", - id: data.id, - error: { - code: ErrorCodes.InvalidRequest, - message: "No method defined!", - }, - } as ResponseObject); - } - return; - } - - const [srvName, fncName] = data.method.split("."); - Logging.log("SERVER: Message for", srvName, fncName); - - const service = this.provider.services.get(srvName); - if (!service) { - Logging.log("SERVER: Did not find Service"); - if (data.id) { - this.send({ - jsonrpc: "2.0", - id: data.id, - error: { - code: ErrorCodes.MethodNotFound, - message: "Service not found!", - }, - } as ResponseObject); - } - return; - } - - const fnc = service.functions.has(fncName); - if (!fnc) { - Logging.log("SERVER: Did not find Function"); - if (data.id) { - this.send({ - jsonrpc: "2.0", - id: data.id, - error: { - code: ErrorCodes.MethodNotFound, - message: "Function not found!", - }, - } as ResponseObject); - } - return; - } - - let result = await (service as any)["_" + fncName](data.params, this.ctx); - if (data.id) { //Request - this.send({ - jsonrpc: "2.0", - id: data.id, - result: result, - } as ResponseObject); - } //else Notification and response is ignored - } catch (err) { - if (data.id) { - this.send( - { - jsonrpc: "2.0", - id: data.id, - error: { - code: ErrorCodes.InternalError, - message: err.message, - data: err instanceof VerificationError ? { - $: "verification_error", - type: err.type, - field: err.field, - value: err.value - } : { - $: "unknown_error" - }, - }, - } as ResponseObject, - err - ); - } - //TODO: Think about else case + async onMessage(data: RequestObject) { + Logging.log("SERVER: Received Message", data); + try { + if (!data.method) { + if (data.id) { + this.send({ + jsonrpc: "2.0", + id: data.id, + error: { + code: ErrorCodes.InvalidRequest, + message: "No method defined!", + }, + } as ResponseObject); + } + return; } - } + + const [srvName, fncName] = data.method.split("."); + Logging.log("SERVER: Message for", srvName, fncName); + + const service = this.provider.services.get(srvName); + if (!service) { + Logging.log("SERVER: Did not find Service"); + if (data.id) { + this.send({ + jsonrpc: "2.0", + id: data.id, + error: { + code: ErrorCodes.MethodNotFound, + message: "Service not found!", + }, + } as ResponseObject); + } + return; + } + + const fnc = service.functions.has(fncName); + if (!fnc) { + Logging.log("SERVER: Did not find Function"); + if (data.id) { + this.send({ + jsonrpc: "2.0", + id: data.id, + error: { + code: ErrorCodes.MethodNotFound, + message: "Function not found!", + }, + } as ResponseObject); + } + return; + } + + let result = await (service as any)["_" + fncName](data.params, this.ctx); + if (data.id) { + //Request + this.send({ + jsonrpc: "2.0", + id: data.id, + result: result, + } as ResponseObject); + } //else Notification and response is ignored + } catch (err) { + if (data.id) { + this.send( + { + jsonrpc: "2.0", + id: data.id, + error: { + code: ErrorCodes.InternalError, + message: err.message, + data: + err instanceof VerificationError + ? { + $: "verification_error", + type: err.type, + field: err.field, + value: err.value, + } + : { + $: "unknown_error", + }, + }, + } as ResponseObject, + err, + ); + } + //TODO: Think about else case + } + } }