use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TsType {
expr: String,
declarations: BTreeMap<String, String>,
product: Option<Product>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct Product {
members: Vec<String>,
merges: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TsMember {
Named { text: String, declarations: BTreeMap<String, String> },
Merged { expr: String, declarations: BTreeMap<String, String> },
}
impl TsMember {
fn declarations(&self) -> &BTreeMap<String, String> {
match self {
Self::Named { declarations, .. } | Self::Merged { declarations, .. } => declarations,
}
}
}
impl TsType {
#[must_use]
pub fn expr(&self) -> &str {
&self.expr
}
pub fn declarations(&self) -> impl Iterator<Item = (&str, &str)> {
self.declarations.iter().map(|(name, declaration)| (name.as_str(), declaration.as_str()))
}
#[must_use]
pub fn module(&self) -> String {
let declarations: Vec<&str> = self.declarations.values().map(String::as_str).collect();
declarations.join("\n\n")
}
fn of(expr: impl Into<String>, parts: &[&Self]) -> Self {
let mut declarations = BTreeMap::new();
for part in parts {
declarations.extend(part.declarations.clone());
}
Self { expr: expr.into(), declarations, product: None }
}
}
fn grouped(expr: &str) -> String {
if expr.contains(['|', '&']) { format!("({expr})") } else { expr.to_owned() }
}
#[derive(Debug, Clone, Copy)]
pub struct TsShape {
members: Spelling,
}
impl TsShape {
#[must_use]
pub fn new(members: Spelling) -> Self {
Self { members }
}
}
impl Sorts for TsShape {
type Ty = TsType;
type Field = TsMember;
}
impl ShapeAlg for TsShape {
fn truth(&self) -> TsType {
TsType::of("boolean", &[])
}
fn unit(&self) -> TsType {
TsType::of("null", &[])
}
fn text(&self) -> TsType {
TsType::of("string", &[])
}
fn literal(&self, text: &str) -> TsType {
TsType::of(format!("\"{text}\""), &[])
}
fn name_word(&self, words: Words<'_>) -> TsType {
TsType::of(format!("\"{}\"", self.members.spell(words)), &[])
}
fn int(&self, _signed: bool, _bits: u16) -> TsType {
TsType::of("number", &[])
}
fn float(&self, _bits: u16) -> TsType {
TsType::of("number", &[])
}
fn bytes(&self, _len: Option<usize>) -> TsType {
TsType::of("never", &[])
}
fn hex(&self, item: TsType) -> TsType {
TsType::of("string", &[&item])
}
fn decimal(&self, item: TsType) -> TsType {
TsType::of("string", &[&item])
}
fn base64(&self, item: TsType) -> TsType {
TsType::of("string", &[&item])
}
fn opt(&self, item: TsType) -> TsType {
TsType::of(format!("{} | null", item.expr), &[&item])
}
fn seq(&self, item: TsType) -> TsType {
TsType::of(format!("{}[]", grouped(&item.expr)), &[&item])
}
fn map(&self, key: TsType, value: TsType) -> TsType {
TsType::of(format!("Record<string, {}>", value.expr), &[&key, &value])
}
fn product(&self, fields: Vec<TsMember>) -> TsType {
let mut product = Product::default();
let mut declarations = BTreeMap::new();
for field in &fields {
declarations.extend(field.declarations().clone());
match field {
TsMember::Named { text, .. } => product.members.push(text.clone()),
TsMember::Merged { expr, .. } => product.merges.push(expr.clone()),
}
}
let expr = intersection(&product);
TsType { expr, declarations, product: Some(product) }
}
fn choice(&self, alternatives: Vec<TsType>) -> TsType {
let expr = alternatives.iter().map(|alternative| alternative.expr.clone()).collect::<Vec<_>>().join(" | ");
let parts: Vec<&TsType> = alternatives.iter().collect();
TsType::of(expr, &parts)
}
fn named(&self, words: Words<'_>, body: TsType) -> TsType {
let name = Spelling::UpperCamel.spell(words);
let declaration = declare(&name, &body);
let mut shape = TsType::of(name.clone(), &[&body]);
shape.declarations.insert(name, declaration);
shape.product = body.product;
shape
}
fn reference(&self, words: Words<'_>) -> TsType {
TsType::of(Spelling::UpperCamel.spell(words), &[])
}
}
fn intersection(product: &Product) -> String {
let own = format!("{{ {} }}", product.members.join("; "));
match (product.members.is_empty(), product.merges.is_empty()) {
(_, true) => own,
(true, false) => product.merges.join(" & "),
(false, false) => format!("{own} & {}", product.merges.join(" & ")),
}
}
fn declare(name: &str, body: &TsType) -> String {
match &body.product {
Some(product) if product.merges.is_empty() => {
let members = product.members.iter().map(|member| format!(" {member}")).collect::<Vec<_>>().join("\n");
format!("export interface {name} {{\n{members}\n}}")
}
_ => format!("export type {name} = {}", body.expr),
}
}
impl FieldAlg for TsShape {
fn field(&self, words: Words<'_>, shape: TsType) -> TsMember {
TsMember::Named {
text: format!("{}: {}", self.members.spell(words), shape.expr),
declarations: shape.declarations,
}
}
fn merge(&self, shape: TsType) -> TsMember {
TsMember::Merged { expr: shape.expr, declarations: shape.declarations }
}
}