Anki/rslib/proto/ts.rs
Damien Elmes f3b6deefe9 Combine all backend methods into a single js/d.ts file, like in Python
Easier to import from, and allows us to declare the output of the build
action without having to iterate over all the proto filenames. Have
confirmed it doesn't break esbuild's tree shaking.
2023-07-03 13:46:38 +10:00

161 lines
4.3 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashSet;
use std::fmt::Write as WriteFmt;
use std::path::Path;
use anki_io::create_dir_all;
use anki_io::write_file_if_changed;
use anki_proto_gen::BackendService;
use anki_proto_gen::Method;
use anyhow::Result;
use inflections::Inflect;
pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> {
let root = Path::new("../../out/ts/lib");
create_dir_all(root)?;
let mut dts_out = String::new();
let mut js_out = String::new();
let mut referenced_packages = HashSet::new();
for service in services {
if service.name == "BackendAnkidroidService" {
continue;
}
for method in service.all_methods() {
let method = MethodDetails::from_method(method);
record_referenced_type(&mut referenced_packages, &method.input_type);
record_referenced_type(&mut referenced_packages, &method.output_type);
write_dts_method(&method, &mut dts_out);
write_js_method(&method, &mut js_out);
}
}
let imports = imports(referenced_packages);
write_file_if_changed(
root.join("backend.d.ts"),
format!("{}{}{}", dts_header(), imports, dts_out),
)?;
write_file_if_changed(
root.join("backend.js"),
format!("{}{}{}", js_header(), imports, js_out),
)?;
Ok(())
}
fn dts_header() -> String {
r#"// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; https://www.gnu.org/licenses/agpl.html
import type { PlainMessage } from "@bufbuild/protobuf";
import type { PostProtoOptions } from "./post";
"#
.into()
}
fn imports(referenced_packages: HashSet<String>) -> String {
let mut out = String::new();
for package in referenced_packages {
writeln!(
&mut out,
"import * as {} from \"./anki/{}_pb\";",
package,
package.to_snake_case()
)
.unwrap();
}
out
}
fn write_dts_method(
MethodDetails {
method_name,
input_type,
output_type,
comments,
}: &MethodDetails,
out: &mut String,
) {
let comments = format_comments(comments);
writeln!(
out,
r#"{comments}export declare function {method_name}(input: PlainMessage<{input_type}>, options?: PostProtoOptions): Promise<{output_type}>;"#
).unwrap()
}
fn js_header() -> String {
r#"// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; https://www.gnu.org/licenses/agpl.html
import { postProto } from "./post";
"#
.into()
}
fn write_js_method(
MethodDetails {
method_name,
input_type,
output_type,
..
}: &MethodDetails,
out: &mut String,
) {
write!(
out,
r#"export async function {method_name}(input, options = {{}}) {{
return await postProto("{method_name}", new {input_type}(input), {output_type}, options);
}}
"#
)
.unwrap();
}
fn format_comments(comments: &Option<String>) -> String {
comments
.as_ref()
.map(|s| format!("/** {s} */\n"))
.unwrap_or_default()
}
struct MethodDetails {
method_name: String,
input_type: String,
output_type: String,
comments: Option<String>,
}
impl MethodDetails {
fn from_method(method: &Method) -> MethodDetails {
let name = method.name.to_camel_case();
let input_type = full_name_to_imported_reference(method.proto.input().full_name());
let output_type = full_name_to_imported_reference(method.proto.output().full_name());
let comments = method.comments.clone();
Self {
method_name: name,
input_type,
output_type,
comments,
}
}
}
fn record_referenced_type(referenced_packages: &mut HashSet<String>, type_name: &str) {
referenced_packages.insert(type_name.split('.').next().unwrap().to_string());
}
// e.g. anki.import_export.ImportResponse ->
// importExport.ImportResponse
fn full_name_to_imported_reference(name: &str) -> String {
let mut name = name.splitn(3, '.');
name.next().unwrap();
format!(
"{}.{}",
name.next().unwrap().to_camel_case(),
name.next().unwrap()
)
}