Some improvements to the type system

This commit is contained in:
Fabian Stamm
2025-05-27 17:30:13 +02:00
parent 45ebb2c0d7
commit 0e73f7b5b3
6 changed files with 649 additions and 358 deletions

View File

@ -67,6 +67,24 @@ impl CompileContext {
result 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<()> { pub fn write_file(&self, filename: &str, content: &str) -> Result<()> {
let res_path = self.output_folder.clone().join(filename); let res_path = self.output_folder.clone().join(filename);
let res_dir = res_path.parent().context("Path has no parent!")?; let res_dir = res_path.parent().context("Path has no parent!")?;

View File

@ -31,7 +31,7 @@ pub enum Step {
} }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Type { pub enum BaseType {
Int, Int,
Float, Float,
String, String,
@ -41,57 +41,143 @@ pub enum Type {
Custom(String), Custom(String),
} }
impl ToString for Type { impl ToString for BaseType {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Type::Int => "int".to_string(), BaseType::Int => "int".to_string(),
Type::Float => "float".to_string(), BaseType::Float => "float".to_string(),
Type::String => "string".to_string(), BaseType::String => "string".to_string(),
Type::Bool => "bool".to_string(), BaseType::Bool => "bool".to_string(),
Type::Bytes => "bytes".to_string(), BaseType::Bytes => "bytes".to_string(),
Type::Void => "void".to_string(), BaseType::Void => "void".to_string(),
Type::Custom(name) => name.clone(), BaseType::Custom(name) => name.clone(),
} }
} }
} }
impl Hash for Type { impl Hash for BaseType {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.to_string().hash(state); self.to_string().hash(state);
} }
} }
impl From<&String> for Type { impl From<&String> for BaseType {
fn from(value: &String) -> Self { fn from(value: &String) -> Self {
Self::from(value.as_str()) Self::from(value.as_str())
} }
} }
impl From<String> for Type { impl From<String> for BaseType {
fn from(value: String) -> Self { fn from(value: String) -> Self {
Self::from(value.as_str()) Self::from(value.as_str())
} }
} }
impl From<&str> for Type { impl From<&str> for BaseType {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
match s { match s {
"int" => Type::Int, "int" => BaseType::Int,
"float" => Type::Float, "float" => BaseType::Float,
"string" => Type::String, "string" => BaseType::String,
"bool" => Type::Bool, "bool" => BaseType::Bool,
"boolean" => Type::Bool, "boolean" => BaseType::Bool,
"bytes" => Type::Bytes, "bytes" => BaseType::Bytes,
"void" => Type::Void, "void" => BaseType::Void,
_ => Type::Custom(s.to_string()), _ => 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<BaseType>) -> 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<BaseType>) {
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<BaseType>) -> 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<BaseType>) {
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<BaseType> {
self.1.get_flags().2.clone()
}
}
// {
// pub base: BaseType,
// pub array: bool,
// pub optional: bool,
// pub map: Option<BaseType>,
// }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TypeDefinition { pub struct TypeDefinition {
pub name: String, pub name: String,
pub depends: HashSet<Type>, pub depends: HashSet<BaseType>,
pub fields: Vec<Field>, pub fields: Vec<Field>,
pub position: ParserPosition, pub position: ParserPosition,
} }
@ -106,9 +192,6 @@ impl Definition for TypeDefinition {
pub struct Field { pub struct Field {
pub name: String, pub name: String,
pub typ: Type, pub typ: Type,
pub array: bool,
pub optional: bool,
pub map: Option<Type>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -133,7 +216,7 @@ pub struct EnumField {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServiceDefinition { pub struct ServiceDefinition {
pub name: String, pub name: String,
pub depends: HashSet<Type>, pub depends: HashSet<BaseType>,
pub methods: Vec<Method>, pub methods: Vec<Method>,
pub position: ParserPosition, pub position: ParserPosition,
} }
@ -156,14 +239,11 @@ pub struct Method {
pub struct MethodInput { pub struct MethodInput {
pub name: String, pub name: String,
pub typ: Type, pub typ: Type,
pub array: bool,
pub optional: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MethodOutput { pub struct MethodOutput {
pub typ: Type, pub typ: Type,
pub array: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -182,7 +262,7 @@ fn build_type(stmt: &TypeStatement) -> Result<TypeDefinition> {
}; };
for field in &stmt.fields { for field in &stmt.fields {
let typ = Type::from(&field.fieldtype); let typ = BaseType::from(&field.fieldtype);
typedef.depends.insert(typ.clone()); typedef.depends.insert(typ.clone());
if let Some(maptype) = &field.map { if let Some(maptype) = &field.map {
@ -193,10 +273,12 @@ fn build_type(stmt: &TypeStatement) -> Result<TypeDefinition> {
typedef.fields.push(Field { typedef.fields.push(Field {
name: field.name.clone(), name: field.name.clone(),
typ: typ.clone(), typ: Type::from_flags(
array: field.array, typ.clone(),
optional: field.optional, field.optional,
map: field.map.as_ref().map(|s| Type::from(s)), field.array,
field.map.as_ref().map(|s| BaseType::from(s)),
),
}); });
} }
@ -247,14 +329,11 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
name: method.name.clone(), name: method.name.clone(),
inputs: Vec::new(), inputs: Vec::new(),
output: method.return_type.as_ref().map(|rt| { output: method.return_type.as_ref().map(|rt| {
let typ = Type::from(&rt.fieldtype); let typ = Type::from_flags(BaseType::from(&rt.fieldtype), false, rt.array, None);
if typ != Type::Void { if typ.0 != BaseType::Void {
servdef.depends.insert(typ.clone()); servdef.depends.insert(typ.0.clone());
}
MethodOutput {
typ,
array: rt.array,
} }
MethodOutput { typ }
}), }),
decorators: MethodDecorators { decorators: MethodDecorators {
description: None, description: None,
@ -265,7 +344,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
let mut optional_starts = false; let mut optional_starts = false;
for inp in &method.inputs { for inp in &method.inputs {
let typ = Type::from(&inp.fieldtype); let typ = BaseType::from(&inp.fieldtype);
servdef.depends.insert(typ.clone()); servdef.depends.insert(typ.clone());
if optional_starts && !inp.optional { if optional_starts && !inp.optional {
@ -280,9 +359,7 @@ fn build_service(stmt: &ServiceStatement) -> Result<ServiceDefinition> {
methoddef.inputs.push(MethodInput { methoddef.inputs.push(MethodInput {
name: inp.name.clone(), name: inp.name.clone(),
typ, typ: Type::from_flags(typ.clone(), inp.optional, inp.array, None),
array: inp.array,
optional: inp.optional,
}); });
} }
@ -426,7 +503,7 @@ pub fn build_ir(root: &Vec<RootNode>) -> Result<IR> {
match step { match step {
Step::Type(typedef) => { Step::Type(typedef) => {
for dep in &typedef.depends { for dep in &typedef.depends {
if let Type::Custom(dep) = dep { if let BaseType::Custom(dep) = dep {
if !all_types.contains(dep) { if !all_types.contains(dep) {
return Err(IRError::new_from_def( return Err(IRError::new_from_def(
&format!("Type {} depends on unknown type {}", typedef.name, dep), &format!("Type {} depends on unknown type {}", typedef.name, dep),
@ -439,7 +516,7 @@ pub fn build_ir(root: &Vec<RootNode>) -> Result<IR> {
} }
Step::Service(servdef) => { Step::Service(servdef) => {
for dep in &servdef.depends { for dep in &servdef.depends {
if let Type::Custom(dep) = dep { if let BaseType::Custom(dep) = dep {
if !all_types.contains(dep) { if !all_types.contains(dep) {
return Err(IRError::new_from_def( return Err(IRError::new_from_def(
&format!( &format!(

View File

@ -3,7 +3,9 @@ use log::warn;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator}; 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::shared::Keywords;
use crate::IR; use crate::IR;
@ -14,37 +16,43 @@ pub struct RustCompiler {
static RUST_KEYWORDS: [&'static str; 6] = ["type", "return", "static", "pub", "enum", "self"]; static RUST_KEYWORDS: [&'static str; 6] = ["type", "return", "static", "pub", "enum", "self"];
impl RustCompiler { impl RustCompiler {
fn type_to_rust(typ: &Type) -> String { fn type_to_rust(typ: &BaseType) -> String {
match typ { match typ {
Type::String => "String".to_string(), BaseType::String => "String".to_string(),
Type::Int => "i64".to_string(), BaseType::Int => "i64".to_string(),
Type::Float => "f64".to_string(), BaseType::Float => "f64".to_string(),
Type::Bool => "bool".to_string(), BaseType::Bool => "bool".to_string(),
Type::Bytes => "Vec<u8>".to_string(), BaseType::Bytes => "Vec<u8>".to_string(),
Type::Void => "()".to_string(), BaseType::Void => "()".to_string(),
Type::Custom(name) => name.clone(), BaseType::Custom(name) => name.clone(),
} }
} }
fn type_to_rust_ext(typ: &Type, optional: bool, array: bool) -> String { fn type_to_rust_ext(typ: &Type) -> String {
let mut result = Self::type_to_rust(typ); let mut result = Self::type_to_rust(&typ.0);
if optional {
result = format!("Option<{}>", result); let (optional, array, map) = typ.1.get_flags();
}
if array { if array {
result = format!("Vec<{}>", result); 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 result
} }
fn add_dependencies( fn add_dependencies(
&mut self, &mut self,
file: &mut FileGenerator, file: &mut FileGenerator,
depends: &HashSet<Type>, depends: &HashSet<BaseType>,
) -> Result<()> { ) -> Result<()> {
for dep in depends { for dep in depends {
match dep { match dep {
Type::Custom(name) => { BaseType::Custom(name) => {
file.a0(&format!("use crate::{};", name)); file.a0(&format!("use crate::{};", name));
} }
_ => {} _ => {}
@ -148,16 +156,16 @@ impl RustCompiler {
format!( format!(
"{}: {}", "{}: {}",
Self::fix_keyword_name(&arg.name), 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::<Vec<String>>() .collect::<Vec<String>>()
.join(", "); .join(", ");
let ret = method.output.as_ref().map_or_else( let ret = method
|| "()".to_owned(), .output
|r| Self::type_to_rust_ext(&r.typ, false, r.array), .as_ref()
); .map_or_else(|| "()".to_owned(), |r| Self::type_to_rust_ext(&r.typ));
f.a1("#[allow(non_snake_case)]"); f.a1("#[allow(non_snake_case)]");
f.a( f.a(
@ -238,7 +246,7 @@ impl RustCompiler {
format!( format!(
".map_err(|_| \"Parameter for field '{}' should be of type '{}'!\")?{}", ".map_err(|_| \"Parameter for field '{}' should be of type '{}'!\")?{}",
arg.name, arg.name,
arg.typ.to_string(), arg.typ.0.to_string(),
if i < method.inputs.len() - 1 { "," } else { "" } if i < method.inputs.len() - 1 { "," } else { "" }
), ),
); );
@ -266,7 +274,7 @@ impl RustCompiler {
format!( format!(
".map_err(|_| \"Parameter for field {} should be of type '{}'!\")?{}", ".map_err(|_| \"Parameter for field {} should be of type '{}'!\")?{}",
arg.name, arg.name,
arg.typ.to_string(), arg.typ.0.to_string(),
if i < method.inputs.len() - 1 { "," } else { "" } if i < method.inputs.len() - 1 { "," } else { "" }
), ),
); );
@ -332,13 +340,13 @@ impl RustCompiler {
format!( format!(
"{}: {}", "{}: {}",
Self::fix_keyword_name(&arg.name), 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::<Vec<String>>() .collect::<Vec<String>>()
.join(", "); .join(", ");
let ret = method.output.as_ref().map_or("()".to_string(), |output| { 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)]"); 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("let l_res = self.client.send_request(l_req).await;");
f.a2("match l_res {"); f.a2("match l_res {");
f.a3("Err(e) => Err(e),"); f.a3("Err(e) => Err(e),");
if output.typ == Type::Void { if output.typ.0 == BaseType::Void {
f.a3("Ok(_) => Ok(())"); f.a3("Ok(_) => Ok(())");
} else { } else {
f.a3("Ok(o) => serde_json::from_value(o).map_err(|e| Box::from(e))"); 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<()> { ) -> anyhow::Result<()> {
let mut f = FileGenerator::new(); 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 std::collections::hash_map::HashMap;")
} }
f.a0("use serde::{Deserialize, Serialize};"); f.a0("use serde::{Deserialize, Serialize};");
@ -451,7 +463,6 @@ impl Compile for RustCompiler {
f.a0(format!("pub struct {} {{", definition.name)); f.a0(format!("pub struct {} {{", definition.name));
for field in definition.fields.iter() { for field in definition.fields.iter() {
f.a(1, "#[allow(non_snake_case)]"); f.a(1, "#[allow(non_snake_case)]");
let func = format!("pub {}:", Self::fix_keyword_name(&field.name));
if Keywords::is_keyword(&field.name) { if Keywords::is_keyword(&field.name) {
warn!( warn!(
@ -462,49 +473,11 @@ impl Compile for RustCompiler {
f.a(1, format!("#[serde(rename = \"{}\")]", field.name)); f.a(1, format!("#[serde(rename = \"{}\")]", field.name));
} }
let mut opts = String::new(); f.a1(format!(
let mut opte = String::new(); "pub {}: {}",
Self::fix_keyword_name(&field.name),
if field.optional { Self::type_to_rust_ext(&field.typ)
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.a0("}"); f.a0("}");

View File

@ -3,7 +3,7 @@ use log::{info, warn};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator}; 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::shared::Keywords;
use crate::IR; use crate::IR;
@ -17,7 +17,7 @@ pub struct TypeScriptCompiler {
flavour: Flavour, flavour: Flavour,
} }
static TS_KEYWORDS: [&'static str; 51] = [ static TS_KEYWORDS: [&'static str; 52] = [
"abstract", "abstract",
"arguments", "arguments",
"await", "await",
@ -69,28 +69,25 @@ static TS_KEYWORDS: [&'static str; 51] = [
"while", "while",
"with", "with",
"yield", "yield",
"static",
]; ];
impl TypeScriptCompiler { impl TypeScriptCompiler {
fn type_to_typescript(typ: &Type) -> String { fn type_to_typescript(typ: &BaseType) -> String {
match typ { match typ {
Type::String => "string".to_string(), BaseType::String => "string".to_string(),
Type::Int => "number".to_string(), BaseType::Int => "number".to_string(),
Type::Float => "number".to_string(), BaseType::Float => "number".to_string(),
Type::Bool => "boolean".to_string(), BaseType::Bool => "boolean".to_string(),
Type::Bytes => "Uint8Array".to_string(), BaseType::Bytes => "Uint8Array".to_string(),
Type::Void => "void".to_string(), BaseType::Void => "void".to_string(),
Type::Custom(name) => name.clone(), BaseType::Custom(name) => name.clone(),
} }
} }
fn type_to_typescript_ext( fn type_to_typescript_ext(typ: &Type) -> String {
typ: &Type, let mut result = Self::type_to_typescript(&typ.0);
optional: bool, let (optional, array, map) = typ.1.get_flags();
array: bool,
map: &Option<Type>,
) -> String {
let mut result = Self::type_to_typescript(typ);
if optional { if optional {
result = format!("({} | undefined)", result); result = format!("({} | undefined)", result);
} }
@ -100,7 +97,7 @@ impl TypeScriptCompiler {
if let Some(map) = map { if let Some(map) = map {
result = format!( result = format!(
"{{ [key: {} ]: {} }}", "{{ [key: {} ]: {} }}",
Self::type_to_typescript(map), Self::type_to_typescript(&map),
result result
); );
} }
@ -110,7 +107,7 @@ impl TypeScriptCompiler {
fn add_dependencies( fn add_dependencies(
&mut self, &mut self,
file: &mut FileGenerator, file: &mut FileGenerator,
depends: &HashSet<Type>, depends: &HashSet<BaseType>,
) -> Result<()> { ) -> Result<()> {
let esm = if self.flavour == Flavour::ESM { let esm = if self.flavour == Flavour::ESM {
".js" ".js"
@ -121,7 +118,7 @@ impl TypeScriptCompiler {
"import {{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }} from \"./ts_base{esm}\"")); "import {{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }} from \"./ts_base{esm}\""));
for dep in depends { for dep in depends {
match dep { match dep {
Type::Custom(name) => { BaseType::Custom(name) => {
file.a0(&format!( file.a0(&format!(
"import {name}, {{ apply_{name} }} from \"./{name}{esm}\";" "import {name}, {{ apply_{name} }} from \"./{name}{esm}\";"
)); ));
@ -155,8 +152,125 @@ impl TypeScriptCompiler {
ctx: &mut CompileContext, ctx: &mut CompileContext,
definition: &ServiceDefinition, definition: &ServiceDefinition,
) -> anyhow::Result<()> { ) -> 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(); 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 {}<T> extends Service<T> {{",
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::<Vec<String>>();
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::<Vec<String>>()
.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(()) Ok(())
} }
@ -171,25 +285,31 @@ impl TypeScriptCompiler {
"" ""
}; };
ctx.write_file(
"ts_service_base.ts",
include_str!("../../templates/TypeScript/ts_service_base.ts"),
)?;
ctx.write_file("ts_service_client.ts", &format!(" ctx.write_file("ts_service_client.ts", &format!("
import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./service_base{esm}\"; import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./ts_service_base{esm}\";
import {{ VerificationError }} from \"./ts_base{esm}\"; import {{ VerificationError }} from \"./ts_base{esm}\";
{} {}
", include_str!("../../templates/TypeScript/ts_service_client.ts")))?; ",
CompileContext::strip_template_ignores(include_str!("../../templates/TypeScript/ts_service_client.ts"))))?;
let mut f = FileGenerator::new(); let mut f = FileGenerator::new();
self.add_dependencies(&mut f, &definition.depends)?; self.add_dependencies(&mut f, &definition.depends)?;
f.a0(format!( f.a0(format!(
"import {{ Service, ServiceProvider, getRandomID }} from \"./service_client{esm}\"" "import {{ Service, ServiceProvider, getRandomID }} from \"./ts_service_client{esm}\""
)); ));
f.a0("export type {"); // f.a0("export type {");
for dep in &definition.depends { // for dep in &definition.depends {
f.a1(format!("{},", Self::type_to_typescript(&dep))); // f.a1(format!("{},", Self::type_to_typescript(&dep)));
} // }
f.a0("}"); // f.a0("}");
f.a0(format!( f.a0(format!(
"export class {} extends Service {{", "export class {} extends Service {{",
@ -199,7 +319,46 @@ impl TypeScriptCompiler {
f.a2(format!("super(provider, \"{}\");", definition.name)); f.a2(format!("super(provider, \"{}\");", definition.name));
f.a1("}"); 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::<Vec<String>>()
.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<any>((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(()) Ok(())
} }
@ -244,8 +403,7 @@ impl Compile for TypeScriptCompiler {
f.a0(format!("export default class {} {{", definition.name)); f.a0(format!("export default class {} {{", definition.name));
for field in definition.fields.iter() { for field in definition.fields.iter() {
let typ = let typ = Self::type_to_typescript_ext(&field.typ);
Self::type_to_typescript_ext(&field.typ, field.optional, field.array, &field.map);
f.a1(format!( f.a1(format!(
"public {}: {};", "public {}: {};",
@ -285,7 +443,7 @@ impl Compile for TypeScriptCompiler {
f.a1(format!("let res = new {}() as any;", definition.name)); f.a1(format!("let res = new {}() as any;", definition.name));
for field in definition.fields.iter() { for field in definition.fields.iter() {
if field.optional { if field.typ.is_optional() {
f.a1(format!( f.a1(format!(
"if (data.{} !== null && data.{} !== undefined ) {{", "if (data.{} !== null && data.{} !== undefined ) {{",
field.name, field.name field.name, field.name
@ -301,20 +459,7 @@ impl Compile for TypeScriptCompiler {
f.a1("else {"); f.a1("else {");
} }
if field.array { if field.typ.is_map() {
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!( f.a2(format!(
"if (typeof data.{} != \"object\") throw new VerificationError(\"map\", \"{}\", data.{}); ", "if (typeof data.{} != \"object\") throw new VerificationError(\"map\", \"{}\", data.{}); ",
field.name, field.name,
@ -326,13 +471,36 @@ impl Compile for TypeScriptCompiler {
"Object.entries(data.{}).forEach(([key, val]) => res.{}[key] = apply_{}(val));", "Object.entries(data.{}).forEach(([key, val]) => res.{}[key] = apply_{}(val));",
field.name, field.name,
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 { } else {
f.a2(format!( f.a2(format!(
"res.{} = apply_{}(data.{})", "res.{} = apply_{}(data.{})",
field.name, field.name,
Self::type_to_typescript(&field.typ), Self::type_to_typescript(&field.typ.base),
field.name field.name
)); ));
} }

View File

@ -1,20 +1,30 @@
//@template-ignore //@template-ignore
import { VerificationError } from "./ts_base"; import { VerificationError } from "./ts_base";
//@template-ignore //@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 IMessageCallback = (data: any) => void;
export type ResponseListener = { export type ResponseListener = {
ok: (response: any) => void; ok: (response: any) => void;
err: (error: Error) => void; err: (error: Error) => void;
} };
export class Service { export class Service {
public _name: string = null as any; public _name: string = null as any;
constructor(protected _provider: ServiceProvider, name: string) { constructor(
protected _provider: ServiceProvider,
name: string,
) {
this._name = name; this._name = name;
this._provider.services.set(name, this); this._provider.services.set(name, this);
} }
@ -37,9 +47,12 @@ export class ServiceProvider {
Logging.log("CLIENT: Determined type is Notification"); Logging.log("CLIENT: Determined type is Notification");
//Notification. Send to Notification handler //Notification. Send to Notification handler
const [srvName, fncName] = msg.method.split("."); const [srvName, fncName] = msg.method.split(".");
let service = this.services.get(srvName) let service = this.services.get(srvName);
if (!service) { if (!service) {
Logging.log("CLIENT: Did not find Service wanted by Notification!", srvName); Logging.log(
"CLIENT: Did not find Service wanted by Notification!",
srvName,
);
} else { } else {
//TODO: Implement Event thingy (or so :)) //TODO: Implement Event thingy (or so :))
} }
@ -51,7 +64,13 @@ export class ServiceProvider {
if (!resListener) return; // Ignore wrong responses if (!resListener) return; // Ignore wrong responses
if (msg.error) { if (msg.error) {
if (msg.error.data && msg.error.data.$ == "verification_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)) resListener.err(
new VerificationError(
msg.error.data.type,
msg.error.data.field,
msg.error.data.value,
),
);
} else { } else {
resListener.err(new Error(msg.error.message)); resListener.err(new Error(msg.error.message));
} }
@ -59,23 +78,29 @@ export class ServiceProvider {
resListener.ok(msg.result); resListener.ok(msg.result);
} }
} }
} }
sendMessage(msg: RequestObject, res?: ResponseListener) { sendNotification(method: string, params: any[]) {
Logging.log("CLIENT: Sending Messgage", msg); Logging.log("CLIENT: Sending Notification", method, params);
if(msg.id) { this.sendPacket({
this.requests.set(msg.id, res); jsonrpc: "2.0",
} method,
this.sendPacket(msg) 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; declare var require: any;
export const getRandomBytes = ( export const getRandomBytes = (
@ -87,9 +112,7 @@ export const getRandomBytes = (
return function (n: number) { return function (n: number) {
var a = new Uint8Array(n); var a = new Uint8Array(n);
for (var i = 0; i < n; i += QUOTA) { for (var i = 0; i < n; i += QUOTA) {
crypto.getRandomValues( crypto.getRandomValues(a.subarray(i, i + Math.min(n - i, QUOTA)));
a.subarray(i, i + Math.min(n - i, QUOTA))
);
} }
return a; return a;
}; };

View File

@ -1,13 +1,41 @@
//@template-ignore //@template-ignore
import { VerificationError } from "./ts_base"; import { VerificationError } from "./ts_base";
//@template-ignore //@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<T> { export class Service<T> {
public name: string = null as any; public name: string = null as any;
public functions = new Set<string>(); public functions = new Set<string>();
_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() {} constructor() {}
} }
@ -18,7 +46,7 @@ export class ServiceProvider<T = any> {
addService(service: Service<T>) { addService(service: Service<T>) {
this.services.set(service.name, service); this.services.set(service.name, service);
Logging.log("SERVER: Adding Service to provider:", service.name); Logging.log("SERVER: Adding Service to provider:", service.name);
Logging.log("SERVER: Service provides:", [...service.functions.keys()]) Logging.log("SERVER: Service provides:", [...service.functions.keys()]);
} }
getSession(send: ISendMessageCB, ctx?: Partial<T>): Session<T> { getSession(send: ISendMessageCB, ctx?: Partial<T>): Session<T> {
@ -32,13 +60,13 @@ class Session<T> {
constructor( constructor(
private provider: ServiceProvider, private provider: ServiceProvider,
private _send: ISendMessageCB, private _send: ISendMessageCB,
ctx?: Partial<T> ctx?: Partial<T>,
) { ) {
this.ctx = ctx || {}; this.ctx = ctx || {};
} }
send(data: any, catchedErr?: Error) { send(data: any, catchedErr?: Error) {
Logging.log("SERVER: Sending Message", data) Logging.log("SERVER: Sending Message", data);
this._send(data, catchedErr); this._send(data, catchedErr);
} }
@ -95,7 +123,8 @@ class Session<T> {
} }
let result = await (service as any)["_" + fncName](data.params, this.ctx); let result = await (service as any)["_" + fncName](data.params, this.ctx);
if (data.id) { //Request if (data.id) {
//Request
this.send({ this.send({
jsonrpc: "2.0", jsonrpc: "2.0",
id: data.id, id: data.id,
@ -111,17 +140,20 @@ class Session<T> {
error: { error: {
code: ErrorCodes.InternalError, code: ErrorCodes.InternalError,
message: err.message, message: err.message,
data: err instanceof VerificationError ? { data:
err instanceof VerificationError
? {
$: "verification_error", $: "verification_error",
type: err.type, type: err.type,
field: err.field, field: err.field,
value: err.value value: err.value,
} : { }
$: "unknown_error" : {
$: "unknown_error",
}, },
}, },
} as ResponseObject, } as ResponseObject,
err err,
); );
} }
//TODO: Think about else case //TODO: Think about else case