Working on implementing the typescript target
This commit is contained in:
@ -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);
|
||||
|
@ -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(())
|
||||
|
398
libjrpc/src/targets/typescript.rs
Normal file
398
libjrpc/src/targets/typescript.rs
Normal 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(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user