Finish implementation of typescript generator.

The implementation is still untested and will
have issues
This commit is contained in:
Fabian Stamm
2025-05-27 19:58:40 +02:00
parent 0e73f7b5b3
commit ffacba2e96
9 changed files with 381 additions and 279 deletions

View File

@ -137,9 +137,9 @@ impl FileGenerator {
self.a(6, content);
}
pub fn add_line(&mut self, line: &str) {
self.content.push(line.to_string());
}
// pub fn add_line(&mut self, line: &str) {
// self.content.push(line.to_string());
// }
pub fn get_content(&self) -> String {
self.content.join("\n")

View File

@ -1,7 +1,7 @@
use std::{
collections::{HashMap, HashSet},
error::Error,
fmt::Display,
fmt::{Debug, Display},
hash::{Hash, Hasher},
};
@ -13,8 +13,9 @@ use crate::parser::{
static BUILT_INS: [&str; 6] = ["int", "float", "string", "boolean", "bytes", "void"];
pub trait Definition {
pub trait Definition: Debug {
fn get_position(&self) -> ParserPosition;
fn get_name(&self) -> String;
}
#[derive(Debug, Clone)]
@ -186,6 +187,9 @@ impl Definition for TypeDefinition {
fn get_position(&self) -> ParserPosition {
self.position.clone()
}
fn get_name(&self) -> String {
self.name.clone()
}
}
#[derive(Debug, Clone)]
@ -205,6 +209,9 @@ impl Definition for EnumDefinition {
fn get_position(&self) -> ParserPosition {
self.position.clone()
}
fn get_name(&self) -> String {
self.name.clone()
}
}
#[derive(Debug, Clone)]
@ -225,6 +232,9 @@ impl Definition for ServiceDefinition {
fn get_position(&self) -> ParserPosition {
self.position.clone()
}
fn get_name(&self) -> String {
self.name.clone()
}
}
#[derive(Debug, Clone)]

View File

@ -10,31 +10,3 @@ pub use ir::IR;
pub use parser::{Parser, RootNode};
pub use process::FileProcessor;
pub use tokenizer::{tokenize, Token, TokenError, TokenPosition, TokenType};
#[cfg(test)]
mod test {
use crate::targets::{self, rust::RustCompiler};
#[cfg(test)]
#[ctor::ctor]
fn init() {
env_logger::init();
}
#[test]
pub fn parse_jrpc() {
let mut fp = crate::process::FileProcessor::new();
// let ir = fp.start_compile("./test.jrpc").unwrap();
let ir = fp.start_compile("http://127.0.0.1:7878/test.jrpc").unwrap();
println!("{:?}", ir);
}
#[test]
pub fn generate_rust() {
let mut fp = crate::process::FileProcessor::new();
let ir = fp.start_compile("./test.jrpc").unwrap();
targets::compile::<RustCompiler>(ir, "./output/rust").unwrap();
}
}

View File

@ -667,8 +667,8 @@ pub struct ParserError {
}
impl ParserError {
fn new(msg: &str, token: &Token) -> ParserError {
ParserError {
fn new(msg: &str, token: &Token) -> Self {
Self {
message: format!("{}: {}", msg, token.1),
token: token.clone(),
}

View File

@ -1,13 +1,47 @@
use std::{
error::Error,
fmt::{Debug, Display},
};
use anyhow::Result;
use crate::{
compile::{Compile, CompileContext},
ir::Definition,
IR,
};
pub mod rust;
pub mod typescript;
#[derive(Debug, Clone)]
pub struct CompilerError<D: Definition> {
pub message: String,
pub definition: D,
}
impl<D: Definition> CompilerError<D> {
fn new(msg: &str, definition: D) -> Self {
Self {
message: format!("{}: {}", msg, definition.get_name()),
definition,
}
}
}
impl<D: Definition> Error for CompilerError<D> {}
impl<D: Definition> Display for CompilerError<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CompilerError: {} at {:?}",
self.message,
self.definition.get_position()
)
}
}
pub fn compile<T: Compile>(ir: IR, output: &str) -> Result<()> {
let mut ctx = CompileContext::new(output);
let mut compiler = T::new(&ir.options)?;
@ -15,10 +49,29 @@ pub fn compile<T: Compile>(ir: IR, output: &str) -> Result<()> {
for step in ir.steps.iter() {
match step {
crate::ir::Step::Type(definition) => compiler.generate_type(&mut ctx, &definition)?,
crate::ir::Step::Enum(definition) => compiler.generate_enum(&mut ctx, &definition)?,
crate::ir::Step::Type(definition) => {
match compiler.generate_type(&mut ctx, &definition) {
Ok(_) => (),
Err(err) => {
return Err(CompilerError::new(&err.to_string(), definition.clone()).into())
}
}
}
crate::ir::Step::Enum(definition) => {
match compiler.generate_enum(&mut ctx, &definition) {
Ok(_) => (),
Err(err) => {
return Err(CompilerError::new(&err.to_string(), definition.clone()).into())
}
}
}
crate::ir::Step::Service(definition) => {
compiler.generate_service(&mut ctx, &definition)?
match compiler.generate_service(&mut ctx, &definition) {
Ok(_) => (),
Err(err) => {
return Err(CompilerError::new(&err.to_string(), definition.clone()).into())
}
}
}
}
}

View File

@ -3,9 +3,7 @@ use log::warn;
use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator};
use crate::ir::{
BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition, TypeModifier,
};
use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition};
use crate::shared::Keywords;
use crate::IR;

View File

@ -1,78 +1,105 @@
use anyhow::Result;
use log::{info, warn};
use anyhow::{anyhow, Result};
use log::info;
use std::collections::{HashMap, HashSet};
use crate::compile::{Compile, CompileContext, FileGenerator};
use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition};
use crate::shared::Keywords;
use crate::IR;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Flavour {
ESM,
Node,
// #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
// pub enum Flavour {
// ESM,
// Node,
// }
pub trait Flavour {
fn ext() -> &'static str;
fn name() -> &'static str;
}
pub struct TypeScriptCompiler {
flavour: Flavour,
pub struct Node;
impl Flavour for Node {
fn ext() -> &'static str {
""
}
fn name() -> &'static str {
"ts-node"
}
}
static TS_KEYWORDS: [&'static str; 52] = [
"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",
"static",
];
pub struct ESM;
impl Flavour for ESM {
fn ext() -> &'static str {
".js"
}
impl TypeScriptCompiler {
fn name() -> &'static str {
"ts-esm"
}
}
pub struct TypeScriptCompiler<F: Flavour> {
// flavour: Flavour,
flavour: std::marker::PhantomData<F>,
}
// static TS_KEYWORDS: [&'static str; 52] = [
// "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",
// "static",
// ];
impl<F: Flavour> TypeScriptCompiler<F> {
fn type_to_typescript(typ: &BaseType) -> String {
match typ {
BaseType::String => "string".to_string(),
@ -109,13 +136,9 @@ impl TypeScriptCompiler {
file: &mut FileGenerator,
depends: &HashSet<BaseType>,
) -> Result<()> {
let esm = if self.flavour == Flavour::ESM {
".js"
} else {
""
};
let esm = F::ext();
file.a0(format!(
"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, apply_array, apply_required, apply_optional, apply_map }} from \"./ts_base{esm}\""));
for dep in depends {
match dep {
BaseType::Custom(name) => {
@ -132,17 +155,76 @@ impl TypeScriptCompiler {
}
fn fix_keyword_name(name: &str) -> String {
if TS_KEYWORDS.contains(&name) {
format!("{}_", name)
} else {
name.to_string()
// if TS_KEYWORDS.contains(&name) {
// format!("{}_", name)
// } else {
// name.to_string()
// }
// TODO: Check if this is something that can be implemented. There is the issue of JSON generation...
name.to_string()
}
fn get_type_apply(typ: &Type, value: &str) -> String {
let apply_field_value = format!("apply_{}", typ.0.to_string());
match &typ.1 {
crate::ir::TypeModifier::None => format!("{}({})", apply_field_value, value),
crate::ir::TypeModifier::Optional => format!("apply_optional({}, {})", value, apply_field_value),
crate::ir::TypeModifier::Array => format!("apply_array({}, {})", value, apply_field_value),
crate::ir::TypeModifier::OptionalArray => format!("apply_optional({}, data => apply_array(data, {}))", value, apply_field_value),
crate::ir::TypeModifier::Map(map) => format!("apply_map({}, apply_{}, {})", value, map.to_string(), apply_field_value),
crate::ir::TypeModifier::OptionalMap(map) => format!("apply_optional({}, data => apply_map(data, apply_{}, {}))", value, map.to_string(), apply_field_value),
crate::ir::TypeModifier::MapArray(map) => format!("apply_map({}, apply_{}, data => apply_array(data, {}))", value, map.to_string(), apply_field_value),
crate::ir::TypeModifier::OptionalMapArray(map) => format!("apply_optional({}, data => apply_map(data, apply_{}, (data) => apply_array(data, {})))", value, map.to_string(), apply_field_value),
}
}
fn generate_service_lib(ctx: &CompileContext, ir: &IR) -> Result<()> {
fn generate_service_lib(&mut self, ctx: &CompileContext, ir: &IR) -> Result<()> {
let esm = F::ext();
let mut f = FileGenerator::new();
let mut fc = FileGenerator::new();
fc.a0(format!("export * from \"./ts_service_client{}\";", esm));
fc.a0("");
let mut fs = FileGenerator::new();
fs.a0(format!("export * from \"./ts_service_server{}\";", esm));
fs.a0("");
for step in &ir.steps {
match step {
Step::Enum(def) => {
f.a0(format!(
"import {}, {{ apply_{} }} from \"./{}{}\";",
def.name, def.name, def.name, esm
));
f.a0(format!("export {{ {}, apply_{} }};", def.name, def.name));
f.a0("");
}
Step::Type(def) => {
f.a0(format!(
"import {}, {{ apply_{} }} from \"./{}{}\";",
def.name, def.name, def.name, esm
));
f.a0(format!("export {{ {}, apply_{} }};", def.name, def.name));
f.a0("");
}
Step::Service(def) => {
fc.a0(format!(
"export {{ {} }} from \"./{}_client{}\"",
def.name, def.name, esm
));
fs.a0(format!(
"export {{ {} }} from \"./{}_server{}\"",
def.name, def.name, esm
));
}
};
}
ctx.write_file("index.ts", &f.into_content())?;
ctx.write_file("index_client.ts", &fc.into_content())?;
ctx.write_file("index_server.ts", &fs.into_content())?;
Ok(())
}
@ -152,11 +234,7 @@ impl TypeScriptCompiler {
ctx: &mut CompileContext,
definition: &ServiceDefinition,
) -> anyhow::Result<()> {
let esm = if self.flavour == Flavour::ESM {
".js"
} else {
""
};
let esm = F::ext();
ctx.write_file("ts_service_server.ts", &format!("
import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./ts_service_base{esm}\";
@ -190,7 +268,13 @@ import {{ VerificationError }} from \"./ts_base{esm}\";
let mut params = func
.inputs
.iter()
.map(|p| format!("{}: {}", p.name, Self::type_to_typescript_ext(&p.typ)))
.map(|p| {
format!(
"{}: {}",
Self::fix_keyword_name(&p.name),
Self::type_to_typescript_ext(&p.typ)
)
})
.collect::<Vec<String>>();
params.push("ctx: T".to_string());
let params = params.join(", ");
@ -207,61 +291,48 @@ import {{ VerificationError }} from \"./ts_base{esm}\";
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.a1(format!(
"_{}(params: any[] | any, ctx: T): void {{",
func.name,
));
}
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!(
"p[{}] = {};",
index,
Self::get_type_apply(&input.typ, &format!("p[{}]", index))
));
}
f.a2("p.push(ctx);");
let ret_value = if let Some(output) = &func.output {
let ret_apply = Self::get_type_apply(&output.typ, "res");
format!(".then(res => {})", ret_apply)
} else {
String::new()
};
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){};",
func.name, ret_value
));
f.a1("}");
f.a0("");
}
@ -279,11 +350,7 @@ import {{ VerificationError }} from \"./ts_base{esm}\";
ctx: &mut CompileContext,
definition: &ServiceDefinition,
) -> anyhow::Result<()> {
let esm = if self.flavour == Flavour::ESM {
".js"
} else {
""
};
let esm = F::ext();
ctx.write_file(
"ts_service_base.ts",
@ -364,7 +431,7 @@ import {{ VerificationError }} from \"./ts_base{esm}\";
}
}
impl Compile for TypeScriptCompiler {
impl<F: Flavour> Compile for TypeScriptCompiler<F> {
fn new(options: &HashMap<String, String>) -> Result<Self> {
let flavour = options
.get("flavour")
@ -372,15 +439,12 @@ impl Compile for TypeScriptCompiler {
.unwrap_or_else(|| "node".to_string());
info!("TypeScript target initialized with flavour: {}", flavour);
Ok(TypeScriptCompiler {
flavour: Flavour::ESM,
flavour: std::marker::PhantomData,
})
}
fn name(&self) -> String {
match self.flavour {
Flavour::ESM => "ts-esm".to_string(),
Flavour::Node => "ts-node".to_string(),
}
F::name().to_string()
}
fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> {
@ -418,8 +482,12 @@ impl Compile for TypeScriptCompiler {
));
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.a3(format!("if(init.{})", Self::fix_keyword_name(&field.name)));
f.a4(format!(
"this.{} = init[\"{}\"]",
Self::fix_keyword_name(&field.name),
Self::fix_keyword_name(&field.name)
));
}
f.a2("}");
f.a1("}");
@ -443,68 +511,12 @@ impl Compile for TypeScriptCompiler {
f.a1(format!("let res = new {}() as any;", definition.name));
for field in definition.fields.iter() {
if field.typ.is_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.typ.is_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.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.base),
field.name
));
}
f.a1("}");
let value_name = format!("data.{}", Self::fix_keyword_name(&field.name));
f.a1(format!(
"res.{} = {};",
Self::fix_keyword_name(&field.name),
Self::get_type_apply(&field.typ, &value_name)
));
}
f.a1("return res;");
@ -560,7 +572,7 @@ impl Compile for TypeScriptCompiler {
}
fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> anyhow::Result<()> {
Self::generate_service_lib(ctx, ir)?;
self.generate_service_lib(ctx, ir)?;
Ok(())
}
}