oag_node_client/emitters/
tests.rs1use minijinja::{Environment, context};
2use oag_core::ir::{IrOperation, IrParameterLocation, IrReturnType, IrSpec, IrType};
3
4use crate::type_mapper::ir_type_to_ts;
5
6pub fn emit_client_tests(ir: &IrSpec) -> String {
8 let mut env = Environment::new();
9 env.set_trim_blocks(true);
10 env.add_template(
11 "client.test.ts.j2",
12 include_str!("../../templates/client.test.ts.j2"),
13 )
14 .expect("template should be valid");
15 let tmpl = env.get_template("client.test.ts.j2").unwrap();
16
17 let mut seen_methods = std::collections::HashSet::new();
18 let mut used_op_indices = std::collections::HashSet::new();
19 let operations: Vec<minijinja::Value> = ir
20 .operations
21 .iter()
22 .enumerate()
23 .flat_map(|(idx, op)| {
24 build_test_operation_contexts(op)
25 .into_iter()
26 .map(move |ctx| (idx, ctx))
27 })
28 .filter(|(idx, op)| {
29 let name = op
30 .get_attr("method_name")
31 .ok()
32 .and_then(|v| v.as_str().map(String::from));
33 match name {
34 Some(n) => {
35 if seen_methods.insert(n) {
36 used_op_indices.insert(*idx);
37 true
38 } else {
39 false
40 }
41 }
42 None => true,
43 }
44 })
45 .map(|(_, ctx)| ctx)
46 .collect();
47
48 let type_imports: Vec<String> = collect_type_imports(
50 ir.operations
51 .iter()
52 .enumerate()
53 .filter(|(i, _)| used_op_indices.contains(i))
54 .map(|(_, op)| op),
55 );
56
57 tmpl.render(context! {
58 operations => operations,
59 type_imports => type_imports,
60 })
61 .expect("render should succeed")
62}
63
64fn collect_type_imports<'a>(ops: impl Iterator<Item = &'a IrOperation>) -> Vec<String> {
66 let mut names = std::collections::BTreeSet::new();
67
68 for op in ops {
69 if let Some(ref body) = op.request_body {
71 collect_ref_names(&body.body_type, &mut names);
72 }
73 match &op.return_type {
75 IrReturnType::Standard(resp) => {
76 collect_ref_names(&resp.response_type, &mut names);
77 }
78 IrReturnType::Sse(sse) => {
79 collect_ref_names(&sse.event_type, &mut names);
80 if let Some(ref json_resp) = sse.json_response {
81 collect_ref_names(&json_resp.response_type, &mut names);
82 }
83 }
84 IrReturnType::Void => {}
85 }
86 }
87
88 names.into_iter().collect()
89}
90
91fn collect_ref_names(ir_type: &IrType, names: &mut std::collections::BTreeSet<String>) {
92 match ir_type {
93 IrType::Ref(name) => {
94 names.insert(name.clone());
95 }
96 IrType::Array(inner) => collect_ref_names(inner, names),
97 IrType::Union(variants) | IrType::Intersection(variants) => {
98 for v in variants {
99 collect_ref_names(v, names);
100 }
101 }
102 _ => {}
103 }
104}
105
106fn build_test_operation_contexts(op: &IrOperation) -> Vec<minijinja::Value> {
107 let mut results = Vec::new();
108
109 match &op.return_type {
110 IrReturnType::Standard(resp) => {
111 let return_type = ir_type_to_ts(&resp.response_type);
112 results.push(build_test_context(
113 op,
114 "standard",
115 &op.name.camel_case,
116 &return_type,
117 ));
118 }
119 IrReturnType::Void => {
120 results.push(build_test_context(op, "void", &op.name.camel_case, "void"));
121 }
122 IrReturnType::Sse(sse) => {
123 let sse_name = if sse.also_has_json {
124 format!("{}Stream", op.name.camel_case)
125 } else {
126 op.name.camel_case.clone()
127 };
128 let return_type = if let Some(ref name) = sse.event_type_name {
129 name.clone()
130 } else {
131 ir_type_to_ts(&sse.event_type)
132 };
133 results.push(build_test_context(op, "sse", &sse_name, &return_type));
134
135 if let Some(ref json_resp) = sse.json_response {
136 let rt = ir_type_to_ts(&json_resp.response_type);
137 results.push(build_test_context(op, "standard", &op.name.camel_case, &rt));
138 }
139 }
140 }
141
142 results
143}
144
145fn build_test_context(
146 op: &IrOperation,
147 kind: &str,
148 method_name: &str,
149 return_type: &str,
150) -> minijinja::Value {
151 let has_body = op.request_body.is_some();
152 let test_call_args = build_test_call_args(op);
153 let expected_url_pattern = build_expected_url_pattern(op);
154 let mock_response = mock_value_ts(&if return_type == "void" {
155 IrType::Void
156 } else {
157 guess_mock_type(return_type)
159 });
160
161 context! {
162 kind => kind,
163 method_name => method_name,
164 http_method => op.method.as_str(),
165 return_type => return_type,
166 has_body => has_body,
167 test_call_args => test_call_args,
168 expected_url_pattern => expected_url_pattern,
169 mock_response => mock_response,
170 }
171}
172
173fn build_test_call_args(op: &IrOperation) -> String {
176 let mut args = Vec::new();
177
178 for param in &op.parameters {
179 match param.location {
180 IrParameterLocation::Path => {
181 args.push(mock_value_ts(¶m.param_type));
182 }
183 IrParameterLocation::Query | IrParameterLocation::Header => {
184 if param.required {
185 args.push(mock_value_ts(¶m.param_type));
186 }
187 }
188 _ => {}
189 }
190 }
191
192 if let Some(ref body) = op.request_body {
193 args.push(mock_value_ts(&body.body_type));
194 }
195
196 args.join(", ")
197}
198
199fn build_expected_url_pattern(op: &IrOperation) -> String {
201 let mut path = op.path.clone();
202 for param in &op.parameters {
203 if param.location == IrParameterLocation::Path {
204 let placeholder = format!("{{{}}}", param.original_name);
205 path = path.replace(&placeholder, &mock_path_value_ts(¶m.param_type));
206 }
207 }
208 path
209}
210
211fn mock_value_ts(ir_type: &IrType) -> String {
213 match ir_type {
214 IrType::String | IrType::DateTime => "\"test\"".to_string(),
215 IrType::StringLiteral(s) => format!("\"{s}\""),
216 IrType::Number | IrType::Integer => "1".to_string(),
217 IrType::Boolean => "true".to_string(),
218 IrType::Null | IrType::Void => "undefined".to_string(),
219 IrType::Array(_) => "[]".to_string(),
220 IrType::Object(_) | IrType::Map(_) | IrType::Any => "{}".to_string(),
221 IrType::Ref(name) => format!("{{}} as {}", name),
222 IrType::Binary => "new Blob()".to_string(),
223 IrType::Union(variants) | IrType::Intersection(variants) => {
224 if let Some(first) = variants.first() {
225 mock_value_ts(first)
226 } else {
227 "{}".to_string()
228 }
229 }
230 }
231}
232
233fn mock_path_value_ts(ir_type: &IrType) -> String {
235 match ir_type {
236 IrType::Integer | IrType::Number => "1".to_string(),
237 _ => "test".to_string(),
238 }
239}
240
241fn guess_mock_type(return_type: &str) -> IrType {
243 match return_type {
244 "string" => IrType::String,
245 "number" => IrType::Number,
246 "boolean" => IrType::Boolean,
247 "void" => IrType::Void,
248 t if t.ends_with("[]") => IrType::Array(Box::new(IrType::Any)),
249 _ => IrType::Ref(return_type.to_string()),
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_mock_value_ts() {
259 assert_eq!(mock_value_ts(&IrType::String), "\"test\"");
260 assert_eq!(mock_value_ts(&IrType::Integer), "1");
261 assert_eq!(mock_value_ts(&IrType::Boolean), "true");
262 assert_eq!(mock_value_ts(&IrType::Void), "undefined");
263 assert_eq!(mock_value_ts(&IrType::Ref("Pet".to_string())), "{} as Pet");
264 }
265
266 #[test]
267 fn test_mock_path_value() {
268 assert_eq!(mock_path_value_ts(&IrType::Integer), "1");
269 assert_eq!(mock_path_value_ts(&IrType::String), "test");
270 }
271}