use std::fmt::Write as _;
use sofie_model::App;
use super::{
Answer, Api, Endpoint, Property, Scalar, Schema, Shape, VALUE_SHAPES, value_properties,
};
pub(crate) const TYPESCRIPT_HEADER: &str = "// @generated by `sofie typescript`; do not edit.\n";
#[must_use]
pub fn typescript(app: &App) -> String {
let api = Api::of(app);
let mut text = format!(
"{TYPESCRIPT_HEADER}// The JSON endpoints of {}.\n\n/** What a committed record answers. */\nexport interface Committed {{\n \
id: string;\n version: number;\n}}\n",
api.title
);
for (name, shape) in VALUE_SHAPES {
if api.mentions_value(name) {
text.push_str(&interface(name, &value_properties(shape)));
}
}
for (name, schema) in &api.schemas {
match schema {
Schema::Enum(names) => {
let quoted: Vec<String> = names.iter().map(|name| format!("\"{name}\"")).collect();
let _ = write!(text, "\nexport type {name} = {};\n", quoted.join(" | "));
}
Schema::Object(properties) => text.push_str(&interface(name, properties)),
}
}
text.push_str("\nexport interface Endpoints {\n");
for endpoint in &api.endpoints {
text.push_str(&endpoint_entry(endpoint));
}
text.push_str("}\n");
text
}
fn interface(name: &str, properties: &[Property]) -> String {
let mut text = format!("\nexport interface {name} {{\n");
for property in properties {
let optional = if property.required { "" } else { "?" };
let _ = writeln!(
text,
" {}{optional}: {};",
property.name,
type_of(&property.shape)
);
}
text.push_str("}\n");
text
}
fn endpoint_entry(endpoint: &Endpoint) -> String {
let named: Vec<String> = endpoint
.parameters
.iter()
.map(|name| format!("{name}: string"))
.collect();
let parameters = if named.is_empty() {
"{}".to_owned()
} else {
format!("{{ {} }}", named.join("; "))
};
let output = match &endpoint.output {
Answer::Empty => "void".to_owned(),
Answer::Data(shape) => type_of(shape),
};
let query = endpoint
.query
.as_ref()
.map(|query| format!("\n query: {};", query.name))
.unwrap_or_default();
let mut text = String::new();
if let Some(summary) = &endpoint.summary {
let _ = writeln!(text, " /** {} */", summary.replace("*/", "* /"));
}
let _ = writeln!(
text,
" \"{}\": {{\n area: \"{}\";\n method: \"{}\";\n path: \"{}\";\n parameters: {parameters};{query}\n \
input: {};\n output: {output};\n }};",
endpoint.name,
endpoint.area,
endpoint.method.name(),
endpoint.path,
endpoint.input.as_deref().unwrap_or("never"),
);
text
}
fn type_of(shape: &Shape) -> String {
match shape {
Shape::Scalar(Scalar::Integer | Scalar::Number) => "number".to_owned(),
Shape::Scalar(Scalar::Boolean) => "boolean".to_owned(),
Shape::Scalar(_) => "string".to_owned(),
Shape::Value(name) => (*name).to_owned(),
Shape::Named(name) => name.clone(),
Shape::Committed => "Committed".to_owned(),
Shape::List(item) => match item.as_ref() {
Shape::Optional(_) | Shape::Tested(_) => format!("({})[]", type_of(item)),
_ => format!("{}[]", type_of(item)),
},
Shape::Optional(inner) => format!("{} | null", type_of(inner)),
Shape::Tested(inner) => format!("{} | boolean", type_of(inner)),
Shape::Unknown(written) => format!("unknown /* {written} */"),
}
}
#[cfg(test)]
mod tests {
use super::typescript;
use crate::api::tests::shop;
#[test]
fn types_the_endpoints_their_schemas_and_values() {
let text = typescript(&shop());
assert_eq!(text, include_str!("../../tests/golden/shop.ts"), "{text}");
}
}