sofie_cli/api/
typescript.rs1use std::fmt::Write as _;
2
3use sofie_model::App;
4
5use super::{
6 Answer, Api, Endpoint, Property, Scalar, Schema, Shape, VALUE_SHAPES, value_properties,
7};
8
9const HEADER: &str = "// @generated by `sofie typescript`; do not edit.\n";
10
11#[must_use]
12pub fn typescript(app: &App) -> String {
13 let api = Api::of(app);
14 let mut text = format!(
15 "{HEADER}// The JSON endpoints of {}.\n\n/** What a committed record answers. */\nexport interface Committed {{\n \
16 id: string;\n version: number;\n}}\n",
17 api.title
18 );
19 for (name, shape) in VALUE_SHAPES {
20 if api.mentions_value(name) {
21 text.push_str(&interface(name, &value_properties(shape)));
22 }
23 }
24 for (name, schema) in &api.schemas {
25 match schema {
26 Schema::Enum(names) => {
27 let quoted: Vec<String> = names.iter().map(|name| format!("\"{name}\"")).collect();
28 let _ = write!(text, "\nexport type {name} = {};\n", quoted.join(" | "));
29 }
30 Schema::Object(properties) => text.push_str(&interface(name, properties)),
31 }
32 }
33 text.push_str("\nexport interface Endpoints {\n");
34 for endpoint in &api.endpoints {
35 text.push_str(&endpoint_entry(endpoint));
36 }
37 text.push_str("}\n");
38 text
39}
40
41fn interface(name: &str, properties: &[Property]) -> String {
42 let mut text = format!("\nexport interface {name} {{\n");
43 for property in properties {
44 let optional = if property.required { "" } else { "?" };
45 let _ = writeln!(
46 text,
47 " {}{optional}: {};",
48 property.name,
49 type_of(&property.shape)
50 );
51 }
52 text.push_str("}\n");
53 text
54}
55
56fn endpoint_entry(endpoint: &Endpoint) -> String {
57 let named: Vec<String> = endpoint
58 .parameters
59 .iter()
60 .map(|name| format!("{name}: string"))
61 .collect();
62 let parameters = if named.is_empty() {
63 "{}".to_owned()
64 } else {
65 format!("{{ {} }}", named.join("; "))
66 };
67 let output = match &endpoint.output {
68 Answer::Empty => "void".to_owned(),
69 Answer::Data(shape) => type_of(shape),
70 };
71 let query = endpoint
72 .query
73 .as_ref()
74 .map(|query| format!("\n query: {};", query.name))
75 .unwrap_or_default();
76 let mut text = String::new();
77 if let Some(summary) = &endpoint.summary {
78 let _ = writeln!(text, " /** {} */", summary.replace("*/", "* /"));
79 }
80 let _ = writeln!(
81 text,
82 " \"{}\": {{\n area: \"{}\";\n method: \"{}\";\n path: \"{}\";\n parameters: {parameters};{query}\n \
83 input: {};\n output: {output};\n }};",
84 endpoint.name,
85 endpoint.area,
86 endpoint.method.name(),
87 endpoint.path,
88 endpoint.input.as_deref().unwrap_or("never"),
89 );
90 text
91}
92
93fn type_of(shape: &Shape) -> String {
94 match shape {
95 Shape::Scalar(Scalar::Integer | Scalar::Number) => "number".to_owned(),
96 Shape::Scalar(Scalar::Boolean) => "boolean".to_owned(),
97 Shape::Scalar(_) => "string".to_owned(),
98 Shape::Value(name) => (*name).to_owned(),
99 Shape::Named(name) => name.clone(),
100 Shape::Committed => "Committed".to_owned(),
101 Shape::List(item) => match item.as_ref() {
102 Shape::Optional(_) | Shape::Tested(_) => format!("({})[]", type_of(item)),
103 _ => format!("{}[]", type_of(item)),
104 },
105 Shape::Optional(inner) => format!("{} | null", type_of(inner)),
106 Shape::Tested(inner) => format!("{} | boolean", type_of(inner)),
107 Shape::Unknown(written) => format!("unknown /* {written} */"),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::typescript;
114 use crate::api::tests::shop;
115
116 #[test]
117 fn types_the_endpoints_their_schemas_and_values() {
118 let text = typescript(&shop());
119
120 assert_eq!(text, include_str!("../../tests/golden/shop.ts"), "{text}");
121 }
122}