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