Skip to main content

alux_shape_typescript/
emit.rs

1//! Interpreting a shape as TypeScript declarations.
2
3use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
4use std::collections::BTreeMap;
5
6/// A shape, as this interpretation carries one: what to write where it is used, the declarations that
7/// use requires, and — when it is a product — the members another product would merge.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct TsType {
10    /// What stands for this shape at a use site.
11    expr: String,
12    /// Every declaration this use depends on, by the name it declares.
13    declarations: BTreeMap<String, String>,
14    /// The members of a product, kept so that merging one into another is expressible.
15    product: Option<Product>,
16}
17
18/// The two ways a product states its members.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20struct Product {
21    /// Members written here, as `name: type`.
22    members: Vec<String>,
23    /// Products merged in, as the types they are written by.
24    merges: Vec<String>,
25}
26
27/// One member of a product.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum TsMember {
30    /// A member written under its own name.
31    Named { text: String, declarations: BTreeMap<String, String> },
32    /// Another product, observed as this one's members.
33    Merged { expr: String, declarations: BTreeMap<String, String> },
34}
35
36impl TsMember {
37    /// The declarations this member's shape depends on.
38    fn declarations(&self) -> &BTreeMap<String, String> {
39        match self {
40            Self::Named { declarations, .. } | Self::Merged { declarations, .. } => declarations,
41        }
42    }
43}
44
45impl TsType {
46    /// What stands for this shape where it is used.
47    #[must_use]
48    pub fn expr(&self) -> &str {
49        &self.expr
50    }
51
52    /// Every declaration this shape depends on, as a name and the declaration it states.
53    pub fn declarations(&self) -> impl Iterator<Item = (&str, &str)> {
54        self.declarations.iter().map(|(name, declaration)| (name.as_str(), declaration.as_str()))
55    }
56
57    /// The module this shape needs: every declaration it depends on, in name order.
58    #[must_use]
59    pub fn module(&self) -> String {
60        let declarations: Vec<&str> = self.declarations.values().map(String::as_str).collect();
61
62        declarations.join("\n\n")
63    }
64
65    /// A shape written by an expression, depending on the declarations gathered from `parts`.
66    fn of(expr: impl Into<String>, parts: &[&Self]) -> Self {
67        let mut declarations = BTreeMap::new();
68
69        for part in parts {
70            declarations.extend(part.declarations.clone());
71        }
72
73        Self { expr: expr.into(), declarations, product: None }
74    }
75}
76
77/// Wraps an expression where TypeScript reads it as more than one type.
78fn grouped(expr: &str) -> String {
79    if expr.contains(['|', '&']) { format!("({expr})") } else { expr.to_owned() }
80}
81
82/// Interprets a shape as TypeScript, spelling member names as the surface spells them.
83///
84/// A type's own name is always written in pascal case, which is TypeScript's convention rather than
85/// this shape's statement.
86#[derive(Debug, Clone, Copy)]
87pub struct TsShape {
88    members: Spelling,
89}
90
91impl TsShape {
92    /// Emits declarations whose members are spelled this way.
93    #[must_use]
94    pub fn new(members: Spelling) -> Self {
95        Self { members }
96    }
97}
98
99impl Sorts for TsShape {
100    type Ty = TsType;
101    type Field = TsMember;
102}
103
104impl ShapeAlg for TsShape {
105    fn truth(&self) -> TsType {
106        TsType::of("boolean", &[])
107    }
108
109    fn unit(&self) -> TsType {
110        TsType::of("null", &[])
111    }
112
113    fn text(&self) -> TsType {
114        TsType::of("string", &[])
115    }
116
117    fn literal(&self, text: &str) -> TsType {
118        TsType::of(format!("\"{text}\""), &[])
119    }
120
121    fn name_word(&self, words: Words<'_>) -> TsType {
122        TsType::of(format!("\"{}\"", self.members.spell(words)), &[])
123    }
124
125    fn int(&self, _signed: bool, _bits: u16) -> TsType {
126        // A JSON number is a `number` at every width. A width beyond what that holds exactly is why
127        // a domain writes such a quantity as text instead, which reads as `string` here.
128        TsType::of("number", &[])
129    }
130
131    fn float(&self, _bits: u16) -> TsType {
132        TsType::of("number", &[])
133    }
134
135    fn bytes(&self, _len: Option<usize>) -> TsType {
136        // Bytes alone are written no way at all, so no value inhabits them.
137        TsType::of("never", &[])
138    }
139
140    fn hex(&self, item: TsType) -> TsType {
141        TsType::of("string", &[&item])
142    }
143
144    fn decimal(&self, item: TsType) -> TsType {
145        TsType::of("string", &[&item])
146    }
147
148    fn base64(&self, item: TsType) -> TsType {
149        TsType::of("string", &[&item])
150    }
151
152    fn opt(&self, item: TsType) -> TsType {
153        TsType::of(format!("{} | null", item.expr), &[&item])
154    }
155
156    fn seq(&self, item: TsType) -> TsType {
157        TsType::of(format!("{}[]", grouped(&item.expr)), &[&item])
158    }
159
160    fn map(&self, key: TsType, value: TsType) -> TsType {
161        // A JSON key is text however the shape describes it, so the key states the value's type.
162        TsType::of(format!("Record<string, {}>", value.expr), &[&key, &value])
163    }
164
165    fn product(&self, fields: Vec<TsMember>) -> TsType {
166        let mut product = Product::default();
167        let mut declarations = BTreeMap::new();
168
169        for field in &fields {
170            declarations.extend(field.declarations().clone());
171
172            match field {
173                TsMember::Named { text, .. } => product.members.push(text.clone()),
174                TsMember::Merged { expr, .. } => product.merges.push(expr.clone()),
175            }
176        }
177
178        let expr = intersection(&product);
179
180        TsType { expr, declarations, product: Some(product) }
181    }
182
183    fn choice(&self, alternatives: Vec<TsType>) -> TsType {
184        let expr = alternatives.iter().map(|alternative| alternative.expr.clone()).collect::<Vec<_>>().join(" | ");
185        let parts: Vec<&TsType> = alternatives.iter().collect();
186
187        TsType::of(expr, &parts)
188    }
189
190    fn named(&self, words: Words<'_>, body: TsType) -> TsType {
191        let name = Spelling::UpperCamel.spell(words);
192        let declaration = declare(&name, &body);
193        let mut shape = TsType::of(name.clone(), &[&body]);
194        shape.declarations.insert(name, declaration);
195        // A named product is still a product, so another product can merge it.
196        shape.product = body.product;
197
198        shape
199    }
200
201    fn reference(&self, words: Words<'_>) -> TsType {
202        // The name alone, since whatever introduced it declares it.
203        TsType::of(Spelling::UpperCamel.spell(words), &[])
204    }
205}
206
207/// Writes a product as one type: its own members, and whatever is merged into it.
208fn intersection(product: &Product) -> String {
209    let own = format!("{{ {} }}", product.members.join("; "));
210
211    match (product.members.is_empty(), product.merges.is_empty()) {
212        (_, true) => own,
213        (true, false) => product.merges.join(" & "),
214        (false, false) => format!("{own} & {}", product.merges.join(" & ")),
215    }
216}
217
218/// Declares a name for a shape, as an interface where TypeScript has one and an alias otherwise.
219fn declare(name: &str, body: &TsType) -> String {
220    match &body.product {
221        // An interface reads better than an alias, and only a product with nothing merged in is one.
222        Some(product) if product.merges.is_empty() => {
223            let members = product.members.iter().map(|member| format!("  {member}")).collect::<Vec<_>>().join("\n");
224
225            format!("export interface {name} {{\n{members}\n}}")
226        }
227        _ => format!("export type {name} = {}", body.expr),
228    }
229}
230
231impl FieldAlg for TsShape {
232    fn field(&self, words: Words<'_>, shape: TsType) -> TsMember {
233        TsMember::Named {
234            text: format!("{}: {}", self.members.spell(words), shape.expr),
235            declarations: shape.declarations,
236        }
237    }
238
239    fn merge(&self, shape: TsType) -> TsMember {
240        TsMember::Merged { expr: shape.expr, declarations: shape.declarations }
241    }
242}