Skip to main content

sofie_cli/api/
typescript.rs

1use 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
9pub(crate) const TYPESCRIPT_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        "{TYPESCRIPT_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}