mod client;
mod models;
pub(crate) mod naming;
mod package;
use std::fmt::Write;
use soothfast_spec::dialect::Info;
use crate::model::{Sdk, Ty};
use crate::{SdkFileSet, SdkOptions};
pub(crate) const HEADER: &str = "// Code generated by `cargo soothfast sdk gen`. DO NOT EDIT.";
const RUNTIME: &str = include_str!("../../assets/typescript/runtime.ts");
const SERVER: &str = include_str!("../../assets/typescript/server.ts");
pub(crate) fn emit(info: &Info, sdk: &Sdk, opts: &SdkOptions) -> Result<SdkFileSet, String> {
let base_url = crate::base_url_for(info, opts)?;
let mut out = SdkFileSet {
notes: sdk.notes.clone(),
..SdkFileSet::default()
};
let files = &mut out.files;
files.insert(
"README.md".into(),
package::readme(info, sdk, opts, base_url.as_deref()),
);
files.insert("package.json".into(), package::package_json(info, opts));
files.insert("tsconfig.json".into(), package::tsconfig(opts));
files.insert(
"src/client.ts".into(),
client::render(sdk, opts, base_url.as_deref()),
);
files.insert("src/index.ts".into(), package::index(info, sdk, opts));
files.insert("src/models.ts".into(), models::render(sdk, opts));
files.insert("src/runtime.ts".into(), RUNTIME.to_string());
if let Some(binary) = &opts.embed {
files.insert("src/server.ts".into(), SERVER.to_string());
for target in crate::target::Target::matrix(&opts.targets)? {
let dir = package::platform_package(opts, target);
files.insert(
format!("platforms/{dir}/package.json"),
package::platform_package_json(opts, target, binary),
);
}
}
Ok(out)
}
pub(crate) fn annotation(ty: &Ty, prefix: &str) -> String {
match ty {
Ty::Str => "string".into(),
Ty::Int | Ty::Float => "number".into(),
Ty::Bool => "boolean".into(),
Ty::Any => "unknown".into(),
Ty::Unit => "void".into(),
Ty::List(t) => format!("{}[]", atom(t, prefix)),
Ty::Map(t) => format!("Record<string, {}>", annotation(t, prefix)),
Ty::Optional(t) => format!("{} | null", annotation(t, prefix)),
Ty::Named(n) => format!("{prefix}{}", naming::type_name(n)),
Ty::Union(arms) => arms
.iter()
.map(|a| annotation(a, prefix))
.collect::<Vec<_>>()
.join(" | "),
Ty::Literal(values) => values
.iter()
.map(|v| naming::string_lit(v))
.collect::<Vec<_>>()
.join(" | "),
}
}
fn atom(ty: &Ty, prefix: &str) -> String {
let rendered = annotation(ty, prefix);
let needs_parens = match ty {
Ty::Optional(_) => true,
Ty::Union(arms) => arms.len() > 1,
Ty::Literal(values) => values.len() > 1,
_ => false,
};
if needs_parens {
format!("({rendered})")
} else {
rendered
}
}
pub(crate) fn member(key: &str, ty: &Ty, required: bool, prefix: &str) -> String {
let optional = if required { "" } else { "?" };
format!(
"{}{optional}: {};",
naming::prop_key(key),
annotation(ty, prefix)
)
}
pub(crate) fn doc(out: &mut String, indent: &str, lines: &[&str]) {
let lines: Vec<String> = lines.iter().map(|l| escape(l)).collect();
match lines.len() {
0 => {}
1 => {
let _ = writeln!(out, "{indent}/** {} */", lines[0]);
}
_ => {
let _ = writeln!(out, "{indent}/**");
for (i, line) in lines.iter().enumerate() {
if i > 0 {
let _ = writeln!(out, "{indent} *");
}
let _ = writeln!(out, "{indent} * {line}");
}
let _ = writeln!(out, "{indent} */");
}
}
}
fn escape(text: &str) -> String {
text.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.replace("*/", "*\\/")
}