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.set_trim_blocks(true);
17 env.add_filter("escape_jsdoc", escape_jsdoc);
18 env.add_template("client.ts.j2", include_str!("../../templates/client.ts.j2"))
19 .expect("template should be valid");
20 let tmpl = env.get_template("client.ts.j2").unwrap();
21
22 let mut seen_methods = HashSet::new();
24 let mut used_op_indices = HashSet::new();
25 let operations: Vec<minijinja::Value> = ir
26 .operations
27 .iter()
28 .enumerate()
29 .flat_map(|(idx, op)| {
30 build_operation_contexts(op)
31 .into_iter()
32 .map(move |ctx| (idx, ctx))
33 })
34 .filter(|(idx, ctx)| {
35 let name = ctx
36 .get_attr("method_name")
37 .ok()
38 .and_then(|v| v.as_str().map(String::from));
39 match name {
40 Some(n) => {
41 if seen_methods.insert(n) {
42 used_op_indices.insert(*idx);
43 true
44 } else {
45 false
46 }
47 }
48 None => true,
49 }
50 })
51 .map(|(_, ctx)| ctx)
52 .collect();
53
54 let imported_types = collect_imported_types(
56 ir.operations
57 .iter()
58 .enumerate()
59 .filter(|(i, _)| used_op_indices.contains(i))
60 .map(|(_, op)| op),
61 );
62
63 let has_sse = operations.iter().any(|op| {
64 op.get_attr("kind")
65 .ok()
66 .is_some_and(|v| v.as_str() == Some("sse"))
67 });
68
69 tmpl.render(context! {
70 title => ir.info.title.clone(),
71 imported_types => imported_types,
72 operations => operations,
73 has_sse => has_sse,
74 no_jsdoc => _no_jsdoc,
75 })
76 .expect("render should succeed")
77}
78
79fn build_operation_contexts(op: &IrOperation) -> Vec<minijinja::Value> {
80 let mut results = Vec::new();
81
82 match &op.return_type {
83 IrReturnType::Standard(resp) => {
84 results.push(build_standard_op(op, &ir_type_to_ts(&resp.response_type)));
85 }
86 IrReturnType::Void => {
87 results.push(build_void_op(op));
88 }
89 IrReturnType::Sse(sse) => {
90 let return_type = if let Some(ref name) = sse.event_type_name {
91 name.clone()
92 } else {
93 ir_type_to_ts(&sse.event_type)
94 };
95 let sse_name = if sse.also_has_json {
96 format!("{}Stream", op.name.camel_case)
97 } else {
98 op.name.camel_case.clone()
99 };
100 results.push(build_sse_op(op, &return_type, &sse_name));
101
102 if let Some(ref json_resp) = sse.json_response {
103 results.push(build_standard_op(
104 op,
105 &ir_type_to_ts(&json_resp.response_type),
106 ));
107 }
108 }
109 }
110
111 results
112}
113
114fn build_standard_op(op: &IrOperation, return_type: &str) -> minijinja::Value {
115 let (params_sig, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
116 build_params(op);
117
118 context! {
119 kind => "standard",
120 method_name => op.name.camel_case.clone(),
121 http_method => op.method.as_str(),
122 path => op.path.clone(),
123 params_signature => params_sig,
124 return_type => return_type,
125 path_params => path_params,
126 query_params_obj => query_params_obj,
127 has_body => has_body,
128 has_path_params => has_path_params,
129 has_query_params => has_query_params,
130 summary => op.summary.clone(),
131 description => op.description.clone(),
132 deprecated => op.deprecated,
133 }
134}
135
136fn build_void_op(op: &IrOperation) -> minijinja::Value {
137 let (params_sig, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
138 build_params(op);
139
140 context! {
141 kind => "void",
142 method_name => op.name.camel_case.clone(),
143 http_method => op.method.as_str(),
144 path => op.path.clone(),
145 params_signature => params_sig,
146 return_type => "void",
147 path_params => path_params,
148 query_params_obj => query_params_obj,
149 has_body => has_body,
150 has_path_params => has_path_params,
151 has_query_params => has_query_params,
152 summary => op.summary.clone(),
153 description => op.description.clone(),
154 deprecated => op.deprecated,
155 }
156}
157
158fn build_sse_op(op: &IrOperation, return_type: &str, method_name: &str) -> minijinja::Value {
159 let (mut parts, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
160 build_params_raw(op);
161
162 if let Some(last) = parts.last_mut()
164 && last.starts_with("options?")
165 {
166 *last = "options?: SSEOptions".to_string();
167 }
168 let params_sig = parts.join(", ");
169
170 context! {
171 kind => "sse",
172 method_name => method_name,
173 http_method => op.method.as_str(),
174 path => op.path.clone(),
175 params_signature => params_sig,
176 return_type => return_type,
177 path_params => path_params,
178 query_params_obj => query_params_obj,
179 has_body => has_body,
180 has_path_params => has_path_params,
181 has_query_params => has_query_params,
182 summary => op.summary.clone(),
183 description => op.description.clone(),
184 deprecated => op.deprecated,
185 }
186}
187
188fn build_params(op: &IrOperation) -> (String, Vec<minijinja::Value>, String, bool, bool, bool) {
189 let (parts, path_params, query_params_obj, has_body, has_path_params, has_query_params) =
190 build_params_raw(op);
191 (
192 parts.join(", "),
193 path_params,
194 query_params_obj,
195 has_body,
196 has_path_params,
197 has_query_params,
198 )
199}
200
201fn build_params_raw(
202 op: &IrOperation,
203) -> (Vec<String>, Vec<minijinja::Value>, String, bool, bool, bool) {
204 let mut parts = Vec::new();
205 let mut path_params = Vec::new();
206 let mut query_parts = Vec::new();
207
208 for param in &op.parameters {
209 if param.location == IrParameterLocation::Path {
210 let ts_type = ir_type_to_ts(¶m.param_type);
211 parts.push(format!("{}: {}", param.name.camel_case, ts_type));
212 path_params.push(context! {
213 name => param.name.camel_case.clone(),
214 original_name => param.original_name.clone(),
215 });
216 }
217 }
218
219 for param in &op.parameters {
220 if param.location == IrParameterLocation::Query {
221 let ts_type = ir_type_to_ts(¶m.param_type);
222 if param.required {
223 parts.push(format!("{}: {}", param.name.camel_case, ts_type));
224 } else {
225 parts.push(format!("{}?: {}", param.name.camel_case, ts_type));
226 }
227 query_parts.push(format!(
228 "{}: String({})",
229 param.original_name, param.name.camel_case
230 ));
231 }
232 }
233
234 let has_body = op.request_body.is_some();
235 if let Some(ref body) = op.request_body {
236 let ts_type = ir_type_to_ts(&body.body_type);
237 if body.required {
238 parts.push(format!("body: {ts_type}"));
239 } else {
240 parts.push(format!("body?: {ts_type}"));
241 }
242 }
243
244 parts.push("options?: RequestOptions".to_string());
245
246 let has_path_params = !path_params.is_empty();
247 let has_query_params = !query_parts.is_empty();
248 let query_params_obj = query_parts.join(", ");
249
250 (
251 parts,
252 path_params,
253 query_params_obj,
254 has_body,
255 has_path_params,
256 has_query_params,
257 )
258}
259
260fn collect_imported_types<'a>(ops: impl Iterator<Item = &'a IrOperation>) -> Vec<String> {
261 let mut types = HashSet::new();
262
263 for op in ops {
264 collect_types_from_return(&op.return_type, &mut types);
265
266 if let Some(ref body) = op.request_body {
267 collect_types_from_ir_type(&body.body_type, &mut types);
268 }
269
270 for param in &op.parameters {
271 collect_types_from_ir_type(¶m.param_type, &mut types);
272 }
273 }
274
275 let mut sorted: Vec<String> = types.into_iter().collect();
276 sorted.sort();
277 sorted
278}
279
280fn collect_types_from_return(ret: &IrReturnType, types: &mut HashSet<String>) {
281 match ret {
282 IrReturnType::Standard(resp) => {
283 collect_types_from_ir_type(&resp.response_type, types);
284 }
285 IrReturnType::Sse(sse) => {
286 if let Some(ref name) = sse.event_type_name {
287 types.insert(name.clone());
288 } else {
289 collect_types_from_ir_type(&sse.event_type, types);
290 }
291 if let Some(ref json) = sse.json_response {
294 collect_types_from_ir_type(&json.response_type, types);
295 }
296 }
297 IrReturnType::Void => {}
298 }
299}
300
301fn collect_types_from_ir_type(ir_type: &IrType, types: &mut HashSet<String>) {
302 match ir_type {
303 IrType::Ref(name) => {
304 types.insert(name.clone());
305 }
306 IrType::Array(inner) | IrType::Map(inner) => collect_types_from_ir_type(inner, types),
307 IrType::Union(variants) => {
308 for v in variants {
309 collect_types_from_ir_type(v, types);
310 }
311 }
312 IrType::Object(fields) => {
313 for (_, ty, _) in fields {
314 collect_types_from_ir_type(ty, types);
315 }
316 }
317 _ => {}
318 }
319}