Skip to main content

apif_execution/
model.rs

1use apif_ast::{GctfDocument, SectionContent, SectionType};
2use apif_optimizer::{self, OptimizeLevel};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ExecutionPlan {
9    pub file_path: String,
10    pub connection: ConnectionInfo,
11    pub target: TargetInfo,
12    pub headers: Option<HeadersInfo>,
13    pub requests: Vec<RequestInfo>,
14    pub expectations: Vec<ExpectationInfo>,
15    pub assertions: Vec<AssertionInfo>,
16    pub extractions: Vec<ExtractionInfo>,
17    pub rpc_mode: RpcMode,
18    pub summary: ExecutionSummary,
19}
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ConnectionInfo {
22    pub address: String,
23    pub source: String,
24    pub backend: String,
25}
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TargetInfo {
28    pub endpoint: String,
29    pub package: Option<String>,
30    pub service: Option<String>,
31    pub method: Option<String>,
32}
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct HeadersInfo {
35    pub count: usize,
36    pub headers: HashMap<String, String>,
37}
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct RequestInfo {
40    pub index: usize,
41    pub content: Value,
42    pub content_type: String,
43    pub line_start: usize,
44    pub line_end: usize,
45}
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ExpectationInfo {
48    pub index: usize,
49    pub expectation_type: String,
50    pub content: Option<Value>,
51    pub message_count: Option<usize>,
52    pub comparison_options: ComparisonOptions,
53    pub line_start: usize,
54    pub line_end: usize,
55}
56#[derive(Debug, Clone, Serialize, Deserialize, Default)]
57pub struct ComparisonOptions {
58    pub partial: bool,
59    pub redact: Vec<String>,
60    pub tolerance: Option<f64>,
61    pub unordered_arrays: bool,
62    pub with_asserts: bool,
63}
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct AssertionInfo {
66    pub index: usize,
67    pub assertions: Vec<String>,
68    pub line_start: usize,
69    pub line_end: usize,
70    pub response_index: Option<usize>,
71}
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ExtractionInfo {
74    pub index: usize,
75    pub variables: HashMap<String, String>,
76    pub line_start: usize,
77    pub line_end: usize,
78    pub response_index: Option<usize>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum RpcMode {
84    Unary,
85    UnaryError,
86    ServerStreaming {
87        response_count: usize,
88    },
89    ClientStreaming {
90        request_count: usize,
91    },
92    BidirectionalStreaming {
93        request_count: usize,
94        response_count: usize,
95    },
96    Unknown,
97}
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum RpcModeInfo {
100    Unary,
101    ServerStreaming,
102    ClientStreaming,
103    BidirectionalStreaming,
104    Unknown,
105}
106#[derive(Debug, Clone, Serialize, Deserialize, Default)]
107pub struct ExecutionSummary {
108    pub total_requests: usize,
109    pub total_responses: usize,
110    pub total_errors: usize,
111    pub error_expected: bool,
112    pub assertion_blocks: usize,
113    pub variable_extractions: usize,
114    pub rpc_mode_name: String,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum TestExecutionStatus {
119    Pass,
120    Fail(String),
121}
122#[derive(Debug, Clone, PartialEq)]
123pub struct TestExecutionResult {
124    pub status: TestExecutionStatus,
125    pub call_duration_ms: Option<u64>,
126    pub captured_response: Option<apif_grpc_transport::types::GrpcResponse>,
127    pub meta: apif_state::TestMeta,
128}
129impl TestExecutionResult {
130    pub fn pass(call_duration_ms: Option<u64>) -> Self {
131        Self {
132            status: TestExecutionStatus::Pass,
133            call_duration_ms,
134            captured_response: None,
135            meta: apif_state::TestMeta::default(),
136        }
137    }
138    pub fn fail(message: String, call_duration_ms: Option<u64>) -> Self {
139        Self {
140            status: TestExecutionStatus::Fail(message),
141            call_duration_ms,
142            captured_response: None,
143            meta: apif_state::TestMeta::default(),
144        }
145    }
146    pub fn with_response(mut self, r: apif_grpc_transport::types::GrpcResponse) -> Self {
147        self.captured_response = Some(r);
148        self
149    }
150    pub fn with_meta(mut self, m: apif_state::TestMeta) -> Self {
151        self.meta = m;
152        self
153    }
154}
155
156impl ExecutionPlan {
157    pub fn from_document(doc: &GctfDocument) -> Self {
158        let conn = doc
159            .first_section(SectionType::Address)
160            .map(|s| match &s.content {
161                SectionContent::Single(a) => ConnectionInfo {
162                    address: a.clone(),
163                    source: format!("ADDRESS [L{}-{}]", s.start_line, s.end_line),
164                    backend: "default".into(),
165                },
166                _ => ConnectionInfo {
167                    address: "<env:GRPCTESTIFY_ADDRESS>".into(),
168                    source: "Environment".into(),
169                    backend: "default".into(),
170                },
171            })
172            .unwrap_or_else(|| ConnectionInfo {
173                address: "<env:GRPCTESTIFY_ADDRESS>".into(),
174                source: "Environment".into(),
175                backend: "default".into(),
176            });
177
178        let target = doc
179            .first_section(SectionType::Endpoint)
180            .map(|s| match &s.content {
181                SectionContent::Single(e) => {
182                    let (p, sv, m) = doc
183                        .parse_endpoint()
184                        .map(|(p, s, m)| (Some(p), Some(s), Some(m)))
185                        .unwrap_or((None, None, None));
186                    TargetInfo {
187                        endpoint: e.clone(),
188                        package: p,
189                        service: sv,
190                        method: m,
191                    }
192                }
193                _ => TargetInfo {
194                    endpoint: "<missing>".into(),
195                    package: None,
196                    service: None,
197                    method: None,
198                },
199            })
200            .unwrap_or_else(|| TargetInfo {
201                endpoint: "<missing>".into(),
202                package: None,
203                service: None,
204                method: None,
205            });
206
207        let headers = doc
208            .first_section(SectionType::RequestHeaders)
209            .and_then(|s| match &s.content {
210                SectionContent::KeyValues(h) => Some(HeadersInfo {
211                    count: h.len(),
212                    headers: h.clone(),
213                }),
214                _ => None,
215            });
216
217        let requests: Vec<RequestInfo> = doc
218            .sections_by_type(SectionType::Request)
219            .iter()
220            .enumerate()
221            .map(|(i, s)| {
222                let (c, ct) = match &s.content {
223                    SectionContent::Json(j) => (j.clone(), "json"),
224                    SectionContent::JsonLines(_) => (Value::Array(vec![]), "json-lines"),
225                    SectionContent::Empty => (Value::Object(serde_json::Map::new()), "empty"),
226                    _ => (Value::Null, "unknown"),
227                };
228                RequestInfo {
229                    index: i + 1,
230                    content: c,
231                    content_type: ct.into(),
232                    line_start: s.start_line,
233                    line_end: s.end_line,
234                }
235            })
236            .collect();
237
238        let resp_sects = doc.sections_by_type(SectionType::Response);
239        let err_sect = doc.first_section(SectionType::Error);
240        let expectations = if !resp_sects.is_empty() {
241            resp_sects
242                .iter()
243                .enumerate()
244                .map(|(i, s)| {
245                    let (c, mc) = match &s.content {
246                        SectionContent::Json(j) => (Some(j.clone()), None),
247                        SectionContent::JsonLines(v) => (None, Some(v.len())),
248                        _ => (None, None),
249                    };
250                    ExpectationInfo {
251                        index: i + 1,
252                        expectation_type: "response".into(),
253                        content: c,
254                        message_count: mc,
255                        comparison_options: ComparisonOptions {
256                            partial: s.inline_options.partial,
257                            redact: s.inline_options.redact.clone(),
258                            tolerance: s.inline_options.tolerance,
259                            unordered_arrays: s.inline_options.unordered_arrays,
260                            with_asserts: s.inline_options.with_asserts,
261                        },
262                        line_start: s.start_line,
263                        line_end: s.end_line,
264                    }
265                })
266                .collect()
267        } else if let Some(s) = err_sect {
268            let c = match &s.content {
269                SectionContent::Json(j) => Some(j.clone()),
270                _ => None,
271            };
272            vec![ExpectationInfo {
273                index: 1,
274                expectation_type: "error".into(),
275                content: c,
276                message_count: None,
277                comparison_options: ComparisonOptions {
278                    partial: s.inline_options.partial,
279                    redact: s.inline_options.redact.clone(),
280                    tolerance: s.inline_options.tolerance,
281                    unordered_arrays: s.inline_options.unordered_arrays,
282                    with_asserts: s.inline_options.with_asserts,
283                },
284                line_start: s.start_line,
285                line_end: s.end_line,
286            }]
287        } else {
288            vec![]
289        };
290
291        let assertions: Vec<AssertionInfo> = doc.sections_by_type(SectionType::Asserts).iter().enumerate().map(|(i, s)| {
292            let asserts = match &s.content { SectionContent::Assertions(lines) => lines.iter().map(|line| apif_optimizer::rewrite_assertion_expression_fixed_point_if_changed_with_level(line, OptimizeLevel::Safe).unwrap_or_else(|| line.clone())).collect(), _ => vec![] };
293            AssertionInfo { index: i + 1, assertions: asserts, line_start: s.start_line, line_end: s.end_line, response_index: None }
294        }).collect();
295
296        let extractions: Vec<ExtractionInfo> = doc
297            .sections_by_type(SectionType::Extract)
298            .iter()
299            .enumerate()
300            .map(|(i, s)| {
301                let vars = match &s.content {
302                    SectionContent::Extract(v) => v.clone(),
303                    _ => HashMap::new(),
304                };
305                ExtractionInfo {
306                    index: i + 1,
307                    variables: vars,
308                    line_start: s.start_line,
309                    line_end: s.end_line,
310                    response_index: None,
311                }
312            })
313            .collect();
314
315        let has_jl = resp_sects
316            .iter()
317            .any(|s| matches!(&s.content, SectionContent::JsonLines(v) if v.len() > 1));
318        let rpc = infer_rpc_mode(&requests, &expectations, err_sect.is_some(), has_jl);
319        let rpc_name = match &rpc {
320            RpcMode::Unary => "Unary",
321            RpcMode::UnaryError => "Unary Error",
322            RpcMode::ServerStreaming { .. } => "Server Streaming",
323            RpcMode::ClientStreaming { .. } => "Client Streaming",
324            RpcMode::BidirectionalStreaming { .. } => "Bidirectional Streaming",
325            RpcMode::Unknown => "Unknown",
326        };
327
328        let req_count = requests.len();
329        let resp_count = expectations
330            .iter()
331            .filter(|e| e.expectation_type == "response")
332            .count();
333        let err_count = expectations
334            .iter()
335            .filter(|e| e.expectation_type == "error")
336            .count();
337        let has_error = expectations.iter().any(|e| e.expectation_type == "error");
338        let asrt_count = assertions.len();
339        let ext_count = extractions.len();
340        ExecutionPlan {
341            file_path: doc.file_path.clone(),
342            connection: conn,
343            target,
344            headers,
345            requests,
346            expectations,
347            assertions,
348            extractions,
349            rpc_mode: rpc,
350            summary: ExecutionSummary {
351                total_requests: req_count,
352                total_responses: resp_count,
353                total_errors: err_count,
354                error_expected: has_error,
355                assertion_blocks: asrt_count,
356                variable_extractions: ext_count,
357                rpc_mode_name: rpc_name.into(),
358            },
359        }
360    }
361}
362
363fn infer_rpc_mode(
364    reqs: &[RequestInfo],
365    exps: &[ExpectationInfo],
366    has_err: bool,
367    has_jl: bool,
368) -> RpcMode {
369    let rc = reqs.len();
370    let ec = exps
371        .iter()
372        .filter(|e| e.expectation_type == "response")
373        .count();
374    if has_err {
375        RpcMode::UnaryError
376    } else if has_jl || ec > 1 {
377        if rc > 1 {
378            RpcMode::BidirectionalStreaming {
379                request_count: rc,
380                response_count: ec,
381            }
382        } else {
383            RpcMode::ServerStreaming { response_count: ec }
384        }
385    } else if rc > 1 {
386        RpcMode::ClientStreaming { request_count: rc }
387    } else if rc == 1 && ec == 1 {
388        RpcMode::Unary
389    } else {
390        RpcMode::Unknown
391    }
392}
393
394pub fn infer_rpc_mode_for_section_types(doc: &GctfDocument) -> RpcMode {
395    let rc = doc.sections_by_type(SectionType::Request).len();
396    let rs = doc.sections_by_type(SectionType::Response);
397    let has_jl = rs
398        .iter()
399        .any(|s| matches!(&s.content, SectionContent::JsonLines(v) if v.len() > 1));
400    let has_err = doc.first_section(SectionType::Error).is_some();
401    if has_err {
402        RpcMode::Unary
403    } else if has_jl || rs.len() > 1 {
404        if rc > 1 {
405            RpcMode::BidirectionalStreaming {
406                request_count: rc,
407                response_count: rs.len(),
408            }
409        } else {
410            RpcMode::ServerStreaming {
411                response_count: rs.len(),
412            }
413        }
414    } else if rc > 1 {
415        RpcMode::ClientStreaming { request_count: rc }
416    } else {
417        RpcMode::Unary
418    }
419}