Skip to main content

rescript_openapi/codegen/
types.rs

1// SPDX-License-Identifier: PMPL-1.0-or-later
2// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3
4//! ReScript type generation
5
6use 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    // Header
16    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    // Topologically sort types, grouping strongly connected components (SCCs)
21    let sccs = topological_sort_scc(&spec.types);
22
23    // Generate each SCC
24    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        // Check for self-recursion
44        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        // Mutually recursive types
50        // type rec a = ...
51        // and b = ...
52        for (i, type_def) in scc.iter().enumerate() {
53            if i == 0 {
54                output.push_str(&generate_type(type_def, true, config)); // type rec ...
55            } else {
56                // Replace "type " with "and "
57                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                // Use @as for JSON field mapping if different
86                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                // oneOf/anyOf with payloads - generate regular variant type
108                // type pet = Cat(cat) | Dog(dog)
109                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                // String enum - generate as standard variant with @as
123                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                // String enum - generate as polymorphic variant for better JSON interop
130                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}