rescript_openapi/codegen/
types.rs1use crate::ir::{ApiSpec, TypeDef};
7use super::{Config, VariantMode};
8use super::schema::{topological_sort_scc, get_dependencies};
9use anyhow::Result;
10use heck::ToLowerCamelCase;
11
12pub fn generate(spec: &ApiSpec, config: &Config) -> Result<String> {
13 let mut output = String::new();
14
15 output.push_str("// SPDX-License-Identifier: PMPL-1.0-or-later\n");
17 output.push_str("// Generated by rescript-openapi - DO NOT EDIT\n");
18 output.push_str(&format!("// Source: {} v{}\n\n", spec.title, spec.version));
19
20 let sccs = topological_sort_scc(&spec.types);
22
23 for scc in sccs {
25 output.push_str(&generate_scc(&scc, config));
26 output.push('\n');
27 }
28
29 Ok(output)
30}
31
32pub fn generate_scc(scc: &[&TypeDef], config: &Config) -> String {
33 let mut output = String::new();
34
35 if scc.len() == 1 {
36 let type_def = scc[0];
37 let name = match type_def {
38 TypeDef::Record { name, .. } => name.to_lower_camel_case(),
39 TypeDef::Variant { name, .. } => name.to_lower_camel_case(),
40 TypeDef::Alias { name, .. } => name.to_lower_camel_case(),
41 };
42
43 let deps = get_dependencies(type_def);
45 let is_recursive = deps.contains(&name);
46
47 output.push_str(&generate_type(type_def, is_recursive, config));
48 } else {
49 for (i, type_def) in scc.iter().enumerate() {
53 if i == 0 {
54 output.push_str(&generate_type(type_def, true, config)); } else {
56 let def = generate_type(type_def, false, config);
58 let def = def.replacen("type ", "and ", 1);
59 output.push_str(&def);
60 }
61 }
62 }
63
64 output
65}
66
67pub fn generate_type(type_def: &TypeDef, is_rec: bool, config: &Config) -> String {
68 let mut output = String::new();
69 let keyword = if is_rec { "type rec" } else { "type" };
70
71 match type_def {
72 TypeDef::Record { name, doc, fields } => {
73 if let Some(doc) = doc {
74 output.push_str(&format!("/** {} */\n", doc));
75 }
76
77 let type_name = name.to_lower_camel_case();
78 output.push_str(&format!("{} {} = {{\n", keyword, type_name));
79
80 for field in fields {
81 if let Some(doc) = &field.doc {
82 output.push_str(&format!(" /** {} */\n", doc));
83 }
84
85 if field.name != field.original_name {
87 output.push_str(&format!(" @as(\"{}\") ", field.original_name));
88 } else {
89 output.push_str(" ");
90 }
91
92 output.push_str(&format!("{}: {},\n", field.name, field.ty.to_rescript()));
93 }
94
95 output.push_str("}\n");
96 }
97
98 TypeDef::Variant { name, doc, cases } => {
99 if let Some(doc) = doc {
100 output.push_str(&format!("/** {} */\n", doc));
101 }
102
103 let type_name = name.to_lower_camel_case();
104 let has_payloads = cases.iter().any(|c| c.payload.is_some());
105
106 if has_payloads {
107 output.push_str(&format!("{} {} =\n", keyword, type_name));
110
111 for case in cases {
112 match &case.payload {
113 Some(ty) => {
114 output.push_str(&format!(" | {}({})\n", case.name, ty.to_rescript()));
115 }
116 None => {
117 output.push_str(&format!(" | {}\n", case.name));
118 }
119 }
120 }
121 } else if config.variant_mode == VariantMode::Standard {
122 output.push_str(&format!("{} {} =\n", keyword, type_name));
124
125 for case in cases {
126 output.push_str(&format!(" | @as(\"{}\") {}\n", case.original_name, case.name));
127 }
128 } else {
129 output.push_str(&format!("{} {} = [\n", keyword, type_name));
131
132 for case in cases {
133 output.push_str(&format!(" | #{}\n", case.name));
134 }
135
136 output.push_str("]\n");
137 }
138 }
139
140 TypeDef::Alias { name, doc, target } => {
141 if let Some(doc) = doc {
142 output.push_str(&format!("/** {} */\n", doc));
143 }
144
145 let type_name = name.to_lower_camel_case();
146 output.push_str(&format!("{} {} = {}\n", keyword, type_name, target.to_rescript()));
147 }
148 }
149
150 output
151}