Working on implementing the typescript target

This commit is contained in:
Fabian Stamm
2025-05-26 23:54:41 +02:00
parent c8e72dbba8
commit 45ebb2c0d7
5 changed files with 472 additions and 53 deletions

View File

@ -6,6 +6,7 @@ use crate::{
};
pub mod rust;
pub mod typescript;
pub fn compile<T: Compile>(ir: IR, output: &str) -> Result<()> {
let mut ctx = CompileContext::new(output);

View File

@ -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(())

View File

@ -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<Type>,
) -> 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<Type>,
) -> 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<String, String>) -> Result<Self> {
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(())
}
}