1use std::collections::HashMap;
23use std::fmt::Write;
24
25#[derive(Debug, Clone, PartialEq)]
27pub enum TsType {
28 String,
30 Number,
32 Boolean,
34 Null,
36 Optional(Box<TsType>),
38 Array(Box<TsType>),
40 Object(Vec<TsField>),
42 Named(String),
44 Void,
46 Raw(String),
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct TsField {
53 pub name: String,
55 pub ty: TsType,
57 pub optional: bool,
59}
60
61impl TsField {
62 pub fn new(name: &str, ty: TsType) -> Self {
64 Self {
65 name: name.to_string(),
66 ty,
67 optional: false,
68 }
69 }
70
71 pub fn optional(name: &str, ty: TsType) -> Self {
73 Self {
74 name: name.to_string(),
75 ty,
76 optional: true,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct HandlerMeta {
84 pub args: Vec<TsField>,
86 pub returns: TsType,
88}
89
90impl TsType {
91 pub fn render(&self) -> String {
93 match self {
94 TsType::String => "string".to_string(),
95 TsType::Number => "number".to_string(),
96 TsType::Boolean => "boolean".to_string(),
97 TsType::Null => "null".to_string(),
98 TsType::Void => "void".to_string(),
99 TsType::Optional(inner) => format!("{} | null", inner.render()),
100 TsType::Array(inner) => {
101 let inner_str = inner.render();
102 if inner_str.contains('|') {
103 format!("({inner_str})[]")
104 } else {
105 format!("{inner_str}[]")
106 }
107 }
108 TsType::Object(fields) => {
109 if fields.is_empty() {
110 return "Record<string, never>".to_string();
111 }
112 let mut s = "{ ".to_string();
113 for (i, field) in fields.iter().enumerate() {
114 if i > 0 {
115 s.push_str("; ");
116 }
117 if field.optional {
118 write!(s, "{}?: {}", field.name, field.ty.render()).unwrap();
119 } else {
120 write!(s, "{}: {}", field.name, field.ty.render()).unwrap();
121 }
122 }
123 s.push_str(" }");
124 s
125 }
126 TsType::Named(name) => name.clone(),
127 TsType::Raw(raw) => raw.clone(),
128 }
129 }
130}
131
132fn to_camel_case(s: &str) -> String {
137 let lower = s.to_lowercase();
138 let mut result = String::new();
139 let mut capitalize_next = false;
140
141 for c in lower.chars() {
142 if c == '_' {
143 capitalize_next = true;
144 } else if capitalize_next {
145 result.push(c.to_uppercase().next().unwrap_or(c));
146 capitalize_next = false;
147 } else {
148 result.push(c);
149 }
150 }
151
152 result
153}
154
155fn to_pascal_case(s: &str) -> String {
157 s.split('_')
158 .map(|word| {
159 let mut chars = word.chars();
160 match chars.next() {
161 None => String::new(),
162 Some(c) => {
163 let upper: String = c.to_uppercase().collect();
164 let rest: String = chars.flat_map(|ch| ch.to_lowercase()).collect();
165 format!("{upper}{rest}")
166 }
167 }
168 })
169 .collect()
170}
171
172pub fn generate_ts_client(handler_metas: &HashMap<String, HandlerMeta>) -> String {
174 let mut output = String::new();
175
176 output.push_str("// Auto-generated by AllFrame. Do not edit manually.\n");
178 output.push_str("// Regenerate with: allframe generate-ts-client\n\n");
179 output.push_str("import { invoke } from \"@tauri-apps/api/core\";\n\n");
180
181 output.push_str("/** @internal Unwrap CallResponse and parse the JSON result. */\n");
183 output.push_str("async function callHandler<T>(handler: string, args: Record<string, unknown> = {}): Promise<T> {\n");
184 output.push_str(" const response = await invoke<{ result: string }>(\"plugin:allframe|allframe_call\", { handler, args });\n");
185 output.push_str(" return JSON.parse(response.result) as T;\n");
186 output.push_str("}\n\n");
187
188 let mut interfaces: Vec<(String, &[TsField])> = Vec::new();
190
191 let mut sorted_handlers: Vec<_> = handler_metas.iter().collect();
193 sorted_handlers.sort_by_key(|(name, _)| (*name).clone());
194
195 for (handler_name, meta) in &sorted_handlers {
197 let pascal = to_pascal_case(handler_name);
198
199 if !meta.args.is_empty() {
201 interfaces.push((format!("{pascal}Args"), &meta.args));
202 }
203
204 if let TsType::Object(fields) = &meta.returns {
206 interfaces.push((format!("{pascal}Response"), fields));
207 }
208 }
209
210 for (name, fields) in &interfaces {
212 writeln!(output, "export interface {name} {{").unwrap();
213 for field in *fields {
214 if field.optional {
215 writeln!(output, " {}?: {};", field.name, field.ty.render()).unwrap();
216 } else {
217 writeln!(output, " {}: {};", field.name, field.ty.render()).unwrap();
218 }
219 }
220 output.push_str("}\n\n");
221 }
222
223 for (handler_name, meta) in &sorted_handlers {
225 let fn_name = to_camel_case(handler_name);
226 let pascal = to_pascal_case(handler_name);
227
228 let return_type = if let TsType::Object(_) = &meta.returns {
230 format!("{pascal}Response")
231 } else {
232 meta.returns.render()
233 };
234
235 if meta.args.is_empty() {
236 writeln!(
237 output,
238 "export async function {fn_name}(): Promise<{return_type}> {{",
239 )
240 .unwrap();
241 writeln!(
242 output,
243 " return callHandler<{return_type}>(\"{handler_name}\");",
244 )
245 .unwrap();
246 } else {
247 let args_type = format!("{pascal}Args");
248 writeln!(
249 output,
250 "export async function {fn_name}(args: {args_type}): Promise<{return_type}> {{",
251 )
252 .unwrap();
253 writeln!(
254 output,
255 " return callHandler<{return_type}>(\"{handler_name}\", args);",
256 )
257 .unwrap();
258 }
259
260 output.push_str("}\n\n");
261 }
262
263 output.trim_end().to_string()
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_to_camel_case() {
272 assert_eq!(to_camel_case("get_user"), "getUser");
273 assert_eq!(to_camel_case("create_new_item"), "createNewItem");
274 assert_eq!(to_camel_case("hello"), "hello");
275 assert_eq!(to_camel_case("GET_USER"), "getUser");
276 }
277
278 #[test]
279 fn test_to_pascal_case() {
280 assert_eq!(to_pascal_case("get_user"), "GetUser");
281 assert_eq!(to_pascal_case("create_new_item"), "CreateNewItem");
282 assert_eq!(to_pascal_case("hello"), "Hello");
283 assert_eq!(to_pascal_case("GET_USER"), "GetUser");
284 }
285
286 #[test]
287 fn test_ts_type_render_primitives() {
288 assert_eq!(TsType::String.render(), "string");
289 assert_eq!(TsType::Number.render(), "number");
290 assert_eq!(TsType::Boolean.render(), "boolean");
291 assert_eq!(TsType::Null.render(), "null");
292 assert_eq!(TsType::Void.render(), "void");
293 }
294
295 #[test]
296 fn test_ts_type_render_optional() {
297 let opt = TsType::Optional(Box::new(TsType::String));
298 assert_eq!(opt.render(), "string | null");
299 }
300
301 #[test]
302 fn test_ts_type_render_array() {
303 let arr = TsType::Array(Box::new(TsType::Number));
304 assert_eq!(arr.render(), "number[]");
305
306 let arr_opt = TsType::Array(Box::new(TsType::Optional(Box::new(TsType::String))));
308 assert_eq!(arr_opt.render(), "(string | null)[]");
309 }
310
311 #[test]
312 fn test_ts_type_render_object() {
313 let obj = TsType::Object(vec![
314 TsField::new("id", TsType::Number),
315 TsField::new("name", TsType::String),
316 ]);
317 assert_eq!(obj.render(), "{ id: number; name: string }");
318 }
319
320 #[test]
321 fn test_ts_type_render_named() {
322 assert_eq!(TsType::Named("UserResponse".to_string()).render(), "UserResponse");
323 }
324
325 #[test]
326 fn test_generate_no_args_handler() {
327 let mut metas = HashMap::new();
328 metas.insert(
329 "get_status".to_string(),
330 HandlerMeta {
331 args: vec![],
332 returns: TsType::String,
333 },
334 );
335
336 let ts = generate_ts_client(&metas);
337 assert!(ts.contains("export async function getStatus(): Promise<string>"));
338 assert!(ts.contains("callHandler<string>(\"get_status\")"));
339 }
340
341 #[test]
342 fn test_generate_with_args_handler() {
343 let mut metas = HashMap::new();
344 metas.insert(
345 "greet".to_string(),
346 HandlerMeta {
347 args: vec![
348 TsField::new("name", TsType::String),
349 TsField::new("age", TsType::Number),
350 ],
351 returns: TsType::Object(vec![TsField::new("greeting", TsType::String)]),
352 },
353 );
354
355 let ts = generate_ts_client(&metas);
356
357 assert!(ts.contains("export interface GreetArgs {"));
359 assert!(ts.contains(" name: string;"));
360 assert!(ts.contains(" age: number;"));
361
362 assert!(ts.contains("export interface GreetResponse {"));
364 assert!(ts.contains(" greeting: string;"));
365
366 assert!(ts.contains("export async function greet(args: GreetArgs): Promise<GreetResponse>"));
368 assert!(ts.contains("callHandler<GreetResponse>(\"greet\", args)"));
369 }
370
371 #[test]
372 fn test_generate_optional_field() {
373 let mut metas = HashMap::new();
374 metas.insert(
375 "search".to_string(),
376 HandlerMeta {
377 args: vec![
378 TsField::new("query", TsType::String),
379 TsField::optional("limit", TsType::Number),
380 ],
381 returns: TsType::Array(Box::new(TsType::String)),
382 },
383 );
384
385 let ts = generate_ts_client(&metas);
386 assert!(ts.contains(" query: string;"));
387 assert!(ts.contains(" limit?: number;"));
388 assert!(ts.contains("Promise<string[]>"));
389 }
390
391 #[test]
392 fn test_generate_multiple_handlers_sorted() {
393 let mut metas = HashMap::new();
394 metas.insert(
395 "delete_user".to_string(),
396 HandlerMeta {
397 args: vec![TsField::new("id", TsType::Number)],
398 returns: TsType::Void,
399 },
400 );
401 metas.insert(
402 "create_user".to_string(),
403 HandlerMeta {
404 args: vec![TsField::new("name", TsType::String)],
405 returns: TsType::Object(vec![TsField::new("id", TsType::Number)]),
406 },
407 );
408
409 let ts = generate_ts_client(&metas);
410
411 let create_pos = ts.find("createUser").unwrap();
413 let delete_pos = ts.find("deleteUser").unwrap();
414 assert!(create_pos < delete_pos);
415 }
416
417 #[test]
418 fn test_generate_named_return_type() {
419 let mut metas = HashMap::new();
420 metas.insert(
421 "get_user".to_string(),
422 HandlerMeta {
423 args: vec![TsField::new("id", TsType::Number)],
424 returns: TsType::Named("User".to_string()),
425 },
426 );
427
428 let ts = generate_ts_client(&metas);
429 assert!(ts.contains("Promise<User>"));
430 assert!(!ts.contains("export interface GetUserResponse"));
432 }
433
434 #[test]
435 fn test_generate_header_and_helper() {
436 let metas = HashMap::new();
437 let ts = generate_ts_client(&metas);
438 assert!(ts.contains("Auto-generated by AllFrame"));
439 assert!(ts.contains("import { invoke }"));
440 assert!(ts.contains("async function callHandler<T>"));
441 assert!(ts.contains("JSON.parse(response.result)"));
442 }
443
444 #[test]
445 fn test_generate_idempotent() {
446 let mut metas = HashMap::new();
447 metas.insert(
448 "greet".to_string(),
449 HandlerMeta {
450 args: vec![TsField::new("name", TsType::String)],
451 returns: TsType::String,
452 },
453 );
454
455 let ts1 = generate_ts_client(&metas);
456 let ts2 = generate_ts_client(&metas);
457 assert_eq!(ts1, ts2);
458 }
459
460 #[test]
461 fn test_full_example_output() {
462 let mut metas = HashMap::new();
463 metas.insert(
464 "get_user".to_string(),
465 HandlerMeta {
466 args: vec![TsField::new("id", TsType::Number)],
467 returns: TsType::Object(vec![
468 TsField::new("id", TsType::Number),
469 TsField::new("name", TsType::String),
470 TsField::optional("email", TsType::String),
471 ]),
472 },
473 );
474
475 let ts = generate_ts_client(&metas);
476
477 assert!(ts.contains("export interface GetUserArgs {\n id: number;\n}"));
479 assert!(ts.contains("export interface GetUserResponse {\n id: number;\n name: string;\n email?: string;\n}"));
480 assert!(ts.contains(
481 "export async function getUser(args: GetUserArgs): Promise<GetUserResponse>"
482 ));
483 assert!(ts.contains("callHandler<GetUserResponse>(\"get_user\", args)"));
484 }
485}