1use serde_json::Value;
6
7use crate::engine::utils::is_js_ident;
8
9const INDENT: &str = " ";
10
11pub fn schema_to_ts(v: &Value, indent: usize) -> String {
12 let kind = match v.get("kind").and_then(|k| k.as_str()) {
13 Some(k) => k,
14 None => return "unknown".into(),
15 };
16
17 match kind {
18 "string" | "isodate" | "path" | "slug" | "unique" | "file" | "image" | "raw" | "markdown" | "mdx" | "excerpt" => {
19 "string".into()
20 },
21 "number" => "number".into(),
22 "boolean" => "boolean".into(),
23 "metadata" => "{ readingTime: number; wordCount: number }".into(),
24 "toc" => "TocItem[]".into(),
25 "array" => {
26 let item = v.get("item").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into());
27 format!("{item}[]")
28 },
29 "object" => render_object(v, indent),
30 "record" => {
31 let val = v.get("value").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into());
32 format!("{{ [k: string]: {val} }}")
33 },
34 "tuple" => {
35 let items: Vec<String> = v
36 .get("items")
37 .and_then(|a| a.as_array())
38 .map(|a| a.iter().map(|i| schema_to_ts(i, indent)).collect())
39 .unwrap_or_default();
40 format!("[{}]", items.join(", "))
41 },
42 "enum" => {
43 let parts: Vec<String> = v
44 .get("variants")
45 .and_then(|a| a.as_array())
46 .map(|a| a.iter().filter_map(literal_value).collect())
47 .unwrap_or_default();
48 if parts.is_empty() { "string".into() } else { parts.join(" | ") }
49 },
50 "literal" => v.get("expected").and_then(literal_value).unwrap_or_else(|| "unknown".into()),
51 "union" => {
52 let parts: Vec<String> = v
53 .get("variants")
54 .and_then(|a| a.as_array())
55 .map(|a| a.iter().map(|i| schema_to_ts(i, indent)).collect())
56 .unwrap_or_default();
57 if parts.is_empty() { "unknown".into() } else { parts.join(" | ") }
58 },
59 "discriminatedUnion" => {
60 let parts: Vec<String> = v
61 .get("variants")
62 .and_then(|a| a.as_array())
63 .map(|a| a.iter().map(|i| schema_to_ts(i, indent)).collect())
64 .unwrap_or_default();
65 if parts.is_empty() { "unknown".into() } else { parts.join(" | ") }
66 },
67 "intersection" => {
68 let l = v.get("left").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into());
69 let r = v.get("right").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into());
70 format!("{l} & {r}")
71 },
72 "optional" | "default" | "transform" | "refine" | "superRefine" | "super_refine" => {
73 v.get("inner").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into())
74 },
75 "nullable" => {
76 let inner = v.get("inner").map(|i| schema_to_ts(i, indent)).unwrap_or_else(|| "unknown".into());
77 format!("{inner} | null")
78 },
79 "coerce.string" => "string".into(),
80 "coerce.number" => "number".into(),
81 "coerce.boolean" => "boolean".into(),
82 "coerce.date" => "Date".into(),
83 _ => "unknown".into(),
84 }
85}
86
87pub fn schema_to_ts_object(v: &Value) -> String {
88 render_object(v, 0)
89}
90
91fn render_object(v: &Value, indent: usize) -> String {
92 let pad_outer = INDENT.repeat(indent);
93 let pad_inner = INDENT.repeat(indent + 1);
94
95 let fields = match v.get("fields").and_then(|f| f.as_object()) {
96 Some(f) => f,
97 None => return "{}".into(),
98 };
99
100 let mut out = String::from("{\n");
101 for (key, sub) in fields {
102 let optional = matches!(sub.get("kind").and_then(|k| k.as_str()), Some("optional") | Some("default"),);
103 let opt = if optional { "?" } else { "" };
104 let ty = schema_to_ts(sub, indent + 1);
105 let safe_key = if is_js_ident(key) { key.clone() } else { format!("'{}'", key.replace('\'', "\\'")) };
106 out.push_str(&format!("{pad_inner}{safe_key}{opt}: {ty}\n"));
107 }
108 let passthrough = v.get("passthrough").and_then(|b| b.as_bool()).unwrap_or(false);
109 if passthrough {
110 out.push_str(&format!("{pad_inner}[k: string]: unknown\n"));
111 }
112 out.push_str(&format!("{pad_outer}}}"));
113 out
114}
115
116fn literal_value(v: &Value) -> Option<String> {
117 match v {
118 Value::String(s) => Some(format!("'{}'", s.replace('\'', "\\'"))),
119 Value::Number(n) => Some(n.to_string()),
120 Value::Bool(b) => Some(b.to_string()),
121 Value::Null => Some("null".into()),
122 _ => None,
123 }
124}