Skip to main content

oag_node_client/emitters/
client.rs

1use 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
8/// Emit `client.ts` — the API client class with REST and SSE methods.
9pub 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    // For SSE, use SSEOptions instead of RequestOptions
115    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(&param.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(&param.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(&param.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}