use std::collections::BTreeMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct Artifact {
pub(crate) format: String,
pub(crate) pages: BTreeMap<String, Page>,
}
impl Artifact {
pub(crate) fn to_typescript(&self) -> Vec<u8> {
let mut out = String::new();
out.push_str("// Arcature Rust-to-TypeScript client contract.\n");
out.push_str("// Generated by `arcature-contract` from the registered page contracts.\n");
out.push_str("// Do not edit by hand; run `arc check` to regenerate and verify.\n");
for (page, schema) in &self.pages {
out.push_str(&format!("export interface {page}Props {{\n"));
for (field, prop) in &schema.props.fields {
let optional = if prop.required { "" } else { "?" };
out.push_str(&format!(
" {field}{optional}: {};\n",
render_type(&prop.ty)
));
}
out.push_str("}\n\n");
}
out.into_bytes()
}
}
fn render_type(ty: &Type) -> String {
match ty {
Type::Boolean => "boolean".to_owned(),
Type::Number => "number".to_owned(),
Type::String => "string".to_owned(),
Type::Array { item } => format!("{}[]", render_type(item)),
Type::Nullable { item } => format!("{} | null", render_type(item)),
Type::Object { fields } => {
let mut out = String::from("{ ");
for (index, (name, prop)) in fields.iter().enumerate() {
if index > 0 {
out.push_str("; ");
}
let optional = if prop.required { "" } else { "?" };
out.push_str(&format!("{name}{optional}: {}", render_type(&prop.ty)));
}
out.push_str(" }");
out
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct Page {
pub(crate) props: Props,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Props {
pub(crate) fields: BTreeMap<String, Prop>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct Prop {
pub(crate) required: bool,
#[serde(rename = "type")]
pub(crate) ty: Type,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum Type {
Boolean,
Number,
String,
Array { item: Box<Self> },
Object { fields: BTreeMap<String, Prop> },
Nullable { item: Box<Self> },
}