1use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct TsType {
10 expr: String,
12 declarations: BTreeMap<String, String>,
14 product: Option<Product>,
16}
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
20struct Product {
21 members: Vec<String>,
23 merges: Vec<String>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum TsMember {
30 Named { text: String, declarations: BTreeMap<String, String> },
32 Merged { expr: String, declarations: BTreeMap<String, String> },
34}
35
36impl TsMember {
37 fn declarations(&self) -> &BTreeMap<String, String> {
39 match self {
40 Self::Named { declarations, .. } | Self::Merged { declarations, .. } => declarations,
41 }
42 }
43}
44
45impl TsType {
46 #[must_use]
48 pub fn expr(&self) -> &str {
49 &self.expr
50 }
51
52 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 #[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 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
77fn grouped(expr: &str) -> String {
79 if expr.contains(['|', '&']) { format!("({expr})") } else { expr.to_owned() }
80}
81
82#[derive(Debug, Clone, Copy)]
87pub struct TsShape {
88 members: Spelling,
89}
90
91impl TsShape {
92 #[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 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 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 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 shape.product = body.product;
197
198 shape
199 }
200
201 fn reference(&self, words: Words<'_>) -> TsType {
202 TsType::of(Spelling::UpperCamel.spell(words), &[])
204 }
205}
206
207fn 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
218fn declare(name: &str, body: &TsType) -> String {
220 match &body.product {
221 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}