Skip to main content

oag_node_client/emitters/
tests.rs

1use minijinja::{Environment, context};
2use oag_core::ir::{IrOperation, IrParameterLocation, IrReturnType, IrSpec, IrType};
3
4use crate::type_mapper::ir_type_to_ts;
5
6/// Emit `client.test.ts` — vitest tests for the API client.
7pub fn emit_client_tests(ir: &IrSpec) -> String {
8    let mut env = Environment::new();
9    env.add_template(
10        "client.test.ts.j2",
11        include_str!("../../templates/client.test.ts.j2"),
12    )
13    .expect("template should be valid");
14    let tmpl = env.get_template("client.test.ts.j2").unwrap();
15
16    // Collect type names referenced in mock values for imports
17    let type_imports: Vec<String> = collect_type_imports(ir);
18
19    let operations: Vec<minijinja::Value> = ir
20        .operations
21        .iter()
22        .flat_map(build_test_operation_contexts)
23        .collect();
24
25    tmpl.render(context! {
26        operations => operations,
27        type_imports => type_imports,
28    })
29    .expect("render should succeed")
30}
31
32/// Collect unique type names used in mock values across all operations.
33fn collect_type_imports(ir: &IrSpec) -> Vec<String> {
34    let mut names = std::collections::BTreeSet::new();
35
36    for op in &ir.operations {
37        // Request body refs
38        if let Some(ref body) = op.request_body {
39            collect_ref_names(&body.body_type, &mut names);
40        }
41        // Return type refs (used in mock_response via guess_mock_type)
42        match &op.return_type {
43            IrReturnType::Standard(resp) => {
44                collect_ref_names(&resp.response_type, &mut names);
45            }
46            IrReturnType::Sse(sse) => {
47                collect_ref_names(&sse.event_type, &mut names);
48                if let Some(ref json_resp) = sse.json_response {
49                    collect_ref_names(&json_resp.response_type, &mut names);
50                }
51            }
52            IrReturnType::Void => {}
53        }
54    }
55
56    names.into_iter().collect()
57}
58
59fn collect_ref_names(ir_type: &IrType, names: &mut std::collections::BTreeSet<String>) {
60    match ir_type {
61        IrType::Ref(name) => {
62            names.insert(name.clone());
63        }
64        IrType::Array(inner) => collect_ref_names(inner, names),
65        IrType::Union(variants) => {
66            for v in variants {
67                collect_ref_names(v, names);
68            }
69        }
70        _ => {}
71    }
72}
73
74fn build_test_operation_contexts(op: &IrOperation) -> Vec<minijinja::Value> {
75    let mut results = Vec::new();
76
77    match &op.return_type {
78        IrReturnType::Standard(resp) => {
79            let return_type = ir_type_to_ts(&resp.response_type);
80            results.push(build_test_context(
81                op,
82                "standard",
83                &op.name.camel_case,
84                &return_type,
85            ));
86        }
87        IrReturnType::Void => {
88            results.push(build_test_context(op, "void", &op.name.camel_case, "void"));
89        }
90        IrReturnType::Sse(sse) => {
91            let sse_name = if sse.also_has_json {
92                format!("{}Stream", op.name.camel_case)
93            } else {
94                op.name.camel_case.clone()
95            };
96            let return_type = if let Some(ref name) = sse.event_type_name {
97                name.clone()
98            } else {
99                ir_type_to_ts(&sse.event_type)
100            };
101            results.push(build_test_context(op, "sse", &sse_name, &return_type));
102
103            if let Some(ref json_resp) = sse.json_response {
104                let rt = ir_type_to_ts(&json_resp.response_type);
105                results.push(build_test_context(op, "standard", &op.name.camel_case, &rt));
106            }
107        }
108    }
109
110    results
111}
112
113fn build_test_context(
114    op: &IrOperation,
115    kind: &str,
116    method_name: &str,
117    return_type: &str,
118) -> minijinja::Value {
119    let has_body = op.request_body.is_some();
120    let test_call_args = build_test_call_args(op);
121    let expected_url_pattern = build_expected_url_pattern(op);
122    let mock_response = mock_value_ts(&if return_type == "void" {
123        IrType::Void
124    } else {
125        // Use a simple mock for the response
126        guess_mock_type(return_type)
127    });
128
129    context! {
130        kind => kind,
131        method_name => method_name,
132        http_method => op.method.as_str(),
133        return_type => return_type,
134        has_body => has_body,
135        test_call_args => test_call_args,
136        expected_url_pattern => expected_url_pattern,
137        mock_response => mock_response,
138    }
139}
140
141/// Build test call arguments for an operation.
142fn build_test_call_args(op: &IrOperation) -> String {
143    let mut args = Vec::new();
144
145    for param in &op.parameters {
146        if param.location == IrParameterLocation::Path {
147            args.push(mock_value_ts(&param.param_type));
148        }
149    }
150
151    for param in &op.parameters {
152        if param.location == IrParameterLocation::Query && param.required {
153            args.push(mock_value_ts(&param.param_type));
154        }
155    }
156
157    if let Some(ref body) = op.request_body {
158        args.push(mock_value_ts(&body.body_type));
159    }
160
161    args.join(", ")
162}
163
164/// Build the expected URL pattern for assertions.
165fn build_expected_url_pattern(op: &IrOperation) -> String {
166    let mut path = op.path.clone();
167    for param in &op.parameters {
168        if param.location == IrParameterLocation::Path {
169            let placeholder = format!("{{{}}}", param.original_name);
170            path = path.replace(&placeholder, &mock_path_value_ts(&param.param_type));
171        }
172    }
173    path
174}
175
176/// Generate a mock TypeScript value for a given IrType.
177fn mock_value_ts(ir_type: &IrType) -> String {
178    match ir_type {
179        IrType::String | IrType::DateTime => "\"test\"".to_string(),
180        IrType::Number | IrType::Integer => "1".to_string(),
181        IrType::Boolean => "true".to_string(),
182        IrType::Null | IrType::Void => "undefined".to_string(),
183        IrType::Array(_) => "[]".to_string(),
184        IrType::Object(_) | IrType::Map(_) | IrType::Any => "{}".to_string(),
185        IrType::Ref(name) => format!("{{}} as {}", name),
186        IrType::Binary => "new Blob()".to_string(),
187        IrType::Union(variants) => {
188            if let Some(first) = variants.first() {
189                mock_value_ts(first)
190            } else {
191                "{}".to_string()
192            }
193        }
194    }
195}
196
197/// Mock path parameter value as a string for URL patterns.
198fn mock_path_value_ts(ir_type: &IrType) -> String {
199    match ir_type {
200        IrType::Integer | IrType::Number => "1".to_string(),
201        _ => "test".to_string(),
202    }
203}
204
205/// Guess a mock IrType from a return type string for simple response mocking.
206fn guess_mock_type(return_type: &str) -> IrType {
207    match return_type {
208        "string" => IrType::String,
209        "number" => IrType::Number,
210        "boolean" => IrType::Boolean,
211        "void" => IrType::Void,
212        t if t.ends_with("[]") => IrType::Array(Box::new(IrType::Any)),
213        _ => IrType::Ref(return_type.to_string()),
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_mock_value_ts() {
223        assert_eq!(mock_value_ts(&IrType::String), "\"test\"");
224        assert_eq!(mock_value_ts(&IrType::Integer), "1");
225        assert_eq!(mock_value_ts(&IrType::Boolean), "true");
226        assert_eq!(mock_value_ts(&IrType::Void), "undefined");
227        assert_eq!(mock_value_ts(&IrType::Ref("Pet".to_string())), "{} as Pet");
228    }
229
230    #[test]
231    fn test_mock_path_value() {
232        assert_eq!(mock_path_value_ts(&IrType::Integer), "1");
233        assert_eq!(mock_path_value_ts(&IrType::String), "test");
234    }
235}