oag_node_client/emitters/
client.rs1use std::collections::HashSet;
2
3use minijinja::{Environment, context};
4use oag_core::ir::{IrOperation, IrParameterLocation, IrReturnType, IrSpec, IrType};
5
6use crate::type_mapper::ir_type_to_ts;
7
8fn escape_jsdoc(value: String) -> String {
10 value.replace("*/", "*\\/")
11}
12
13pub fn emit_client(ir: &IrSpec, _no_jsdoc: bool) -> String {
15 let mut env = Environment::new();
16 env.add_filter("escape_jsdoc", escape_jsdoc);
17 env.add_template("client.ts.j2", include_str!("../../templates/client.ts.j2"))
18 .expect("template should be valid");
19 let tmpl = env.get_template("client.ts.j2").unwrap();
20
21 let imported_types = collect_imported_types(ir);
22 let operations: Vec<minijinja::Value> = ir
23 .operations
24 .iter()
25 .flat_map(build_operation_contexts)
26 .collect();
27
28 tmpl.render(context! {
29 title => ir.info.title.clone(),
30 imported_types => imported_types,
31 operations => operations,
32 no_jsdoc => _no_jsdoc,
33 })
34 .expect("render should succeed")
35}
36
37fn build_operation_contexts(op: &IrOperation) -> Vec<minijinja::Value> {
38 let mut results = Vec::new();
39
40 match &op.return_type {
41 IrReturnType::Standard(resp) => {
42 results.push(build_standard_op(op, &ir_type_to_ts(&resp.response_type)));
43 }
44 IrReturnType::Void => {
45 results.push(build_void_op(op));
46 }
47 IrReturnType::Sse(sse) => {
48 let return_type = if let Some(ref name) = sse.event_type_name {
49 name.clone()
50 } else {
51 ir_type_to_ts(&sse.event_type)
52 };
53 let sse_name = if sse.also_has_json {
54 format!("{}Stream", op.name.camel_case)
55 } else {
56 op.name.camel_case.clone()
57 };
58 results.push(build_sse_op(op, &return_type, &sse_name));
59
60 if let Some(ref json_resp) = sse.json_response {
61 results.push(build_standard_op(
62 op,
63 &ir_type_to_ts(&json_resp.response_type),
64 ));
65 }
66 }
67 }
68
69 results
70}
71
72fn build_standard_op(op: &IrOperation, return_type: &str) -> minijinja::Value {
73 let (params_sig, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
74 build_params(op);
75
76 context! {
77 kind => "standard",
78 method_name => op.name.camel_case.clone(),
79 http_method => op.method.as_str(),
80 path => op.path.clone(),
81 params_signature => params_sig,
82 return_type => return_type,
83 path_params => path_params,
84 query_params_obj => query_params_obj,
85 has_body => has_body,
86 has_path_params => has_path_params,
87 has_query_params => has_query_params,
88 summary => op.summary.clone(),
89 description => op.description.clone(),
90 deprecated => op.deprecated,
91 }
92}
93
94fn build_void_op(op: &IrOperation) -> minijinja::Value {
95 let (params_sig, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
96 build_params(op);
97
98 context! {
99 kind => "void",
100 method_name => op.name.camel_case.clone(),
101 http_method => op.method.as_str(),
102 path => op.path.clone(),
103 params_signature => params_sig,
104 return_type => "void",
105 path_params => path_params,
106 query_params_obj => query_params_obj,
107 has_body => has_body,
108 has_path_params => has_path_params,
109 has_query_params => has_query_params,
110 summary => op.summary.clone(),
111 description => op.description.clone(),
112 deprecated => op.deprecated,
113 }
114}
115
116fn build_sse_op(op: &IrOperation, return_type: &str, method_name: &str) -> minijinja::Value {
117 let (mut parts, path_params, _query, has_body, has_path_params, _has_query) =
118 build_params_raw(op);
119
120 if let Some(last) = parts.last_mut()
122 && last.starts_with("options?")
123 {
124 *last = "options?: SSEOptions".to_string();
125 }
126 let params_sig = parts.join(", ");
127
128 context! {
129 kind => "sse",
130 method_name => method_name,
131 http_method => op.method.as_str(),
132 path => op.path.clone(),
133 params_signature => params_sig,
134 return_type => return_type,
135 path_params => path_params,
136 has_body => has_body,
137 has_path_params => has_path_params,
138 summary => op.summary.clone(),
139 description => op.description.clone(),
140 deprecated => op.deprecated,
141 }
142}
143
144fn build_params(op: &IrOperation) -> (String, Vec<minijinja::Value>, String, bool, bool, bool) {
145 let (parts, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
146 build_params_raw(op);
147 (
148 parts.join(", "),
149 path_params,
150 query_params_obj,
151 has_body,
152 has_path_params,
153 has_query_params,
154 )
155}
156
157fn build_params_raw(
158 op: &IrOperation,
159) -> (Vec<String>, Vec<minijinja::Value>, String, bool, bool, bool) {
160 let mut parts = Vec::new();
161 let mut path_params = Vec::new();
162 let mut query_parts = Vec::new();
163
164 for param in &op.parameters {
165 if param.location == IrParameterLocation::Path {
166 let ts_type = ir_type_to_ts(¶m.param_type);
167 parts.push(format!("{}: {}", param.name.camel_case, ts_type));
168 path_params.push(context! {
169 name => param.name.camel_case.clone(),
170 original_name => param.original_name.clone(),
171 });
172 }
173 }
174
175 for param in &op.parameters {
176 if param.location == IrParameterLocation::Query {
177 let ts_type = ir_type_to_ts(¶m.param_type);
178 if param.required {
179 parts.push(format!("{}: {}", param.name.camel_case, ts_type));
180 } else {
181 parts.push(format!("{}?: {}", param.name.camel_case, ts_type));
182 }
183 query_parts.push(format!(
184 "{}: String({})",
185 param.original_name, param.name.camel_case
186 ));
187 }
188 }
189
190 let has_body = op.request_body.is_some();
191 if let Some(ref body) = op.request_body {
192 let ts_type = ir_type_to_ts(&body.body_type);
193 if body.required {
194 parts.push(format!("body: {ts_type}"));
195 } else {
196 parts.push(format!("body?: {ts_type}"));
197 }
198 }
199
200 parts.push("options?: RequestOptions".to_string());
201
202 let has_path_params = !path_params.is_empty();
203 let has_query_params = !query_parts.is_empty();
204 let query_params_obj = query_parts.join(", ");
205
206 (
207 parts,
208 path_params,
209 query_params_obj,
210 has_body,
211 has_path_params,
212 has_query_params,
213 )
214}
215
216fn collect_imported_types(ir: &IrSpec) -> Vec<String> {
217 let mut types = HashSet::new();
218
219 for op in &ir.operations {
220 collect_types_from_return(&op.return_type, &mut types);
221
222 if let Some(ref body) = op.request_body {
223 collect_types_from_ir_type(&body.body_type, &mut types);
224 }
225
226 for param in &op.parameters {
227 collect_types_from_ir_type(¶m.param_type, &mut types);
228 }
229 }
230
231 let mut sorted: Vec<String> = types.into_iter().collect();
232 sorted.sort();
233 sorted
234}
235
236fn collect_types_from_return(ret: &IrReturnType, types: &mut HashSet<String>) {
237 match ret {
238 IrReturnType::Standard(resp) => {
239 collect_types_from_ir_type(&resp.response_type, types);
240 }
241 IrReturnType::Sse(sse) => {
242 if let Some(ref name) = sse.event_type_name {
243 types.insert(name.clone());
244 } else {
245 collect_types_from_ir_type(&sse.event_type, types);
246 }
247 for v in &sse.variants {
248 collect_types_from_ir_type(v, types);
249 }
250 if let Some(ref json) = sse.json_response {
251 collect_types_from_ir_type(&json.response_type, types);
252 }
253 }
254 IrReturnType::Void => {}
255 }
256}
257
258fn collect_types_from_ir_type(ir_type: &IrType, types: &mut HashSet<String>) {
259 match ir_type {
260 IrType::Ref(name) => {
261 types.insert(name.clone());
262 }
263 IrType::Array(inner) | IrType::Map(inner) => collect_types_from_ir_type(inner, types),
264 IrType::Union(variants) => {
265 for v in variants {
266 collect_types_from_ir_type(v, types);
267 }
268 }
269 IrType::Object(fields) => {
270 for (_, ty, _) in fields {
271 collect_types_from_ir_type(ty, types);
272 }
273 }
274 _ => {}
275 }
276}