Some improvements to the type system
This commit is contained in:
@ -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<u8>".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<u8>".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<Type>,
|
||||
depends: &HashSet<BaseType>,
|
||||
) -> 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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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("}");
|
||||
|
@ -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<Type>,
|
||||
) -> 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<Type>,
|
||||
depends: &HashSet<BaseType>,
|
||||
) -> 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 {}<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(())
|
||||
}
|
||||
|
||||
@ -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::<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(())
|
||||
}
|
||||
@ -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
|
||||
));
|
||||
}
|
||||
|
Reference in New Issue
Block a user