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