Skip to main content

apif_execution/
events.rs

1use crate::model::{ComparisonOptions, ExecutionPlan};
2use apif_ast::GctfDocument;
3use apif_optimizer as optimizer;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum StreamingPattern {
8    Sequential,
9    Parallel,
10    Interleaved,
11}
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct ValidationResult {
14    pub passed: bool,
15    pub errors: Vec<String>,
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum WorkflowEvent {
20    TestLoaded {
21        file_path: String,
22    },
23    Connect {
24        backend: String,
25        address: String,
26    },
27    Connected {
28        backend: String,
29        address: String,
30    },
31    LoadDescriptors {
32        backend: String,
33        service: String,
34    },
35    DescriptorsLoaded {
36        backend: String,
37        service: String,
38        method_count: usize,
39    },
40    SendRequest {
41        backend: String,
42        request_index: usize,
43        content_type: String,
44        line_range: (usize, usize),
45    },
46    RequestSent {
47        backend: String,
48        request_index: usize,
49    },
50    ReceiveResponse {
51        backend: String,
52        response_index: usize,
53        expectation_type: String,
54    },
55    ResponseReceived {
56        backend: String,
57        response_index: usize,
58    },
59    Assertion {
60        backend: String,
61        expression: String,
62        passed: bool,
63        line: usize,
64    },
65    Extraction {
66        backend: String,
67        variable: String,
68        value: String,
69        line: usize,
70    },
71    Error {
72        backend: String,
73        message: String,
74    },
75    TrailersReceived {
76        backend: String,
77        trailers: std::collections::HashMap<String, String>,
78    },
79    Done {
80        backend: String,
81        duration_ms: u64,
82    },
83    SemanticAnalysis {
84        type_mismatches: Vec<SemanticError>,
85        unknown_plugins: Vec<String>,
86    },
87    OptimizationFound {
88        hints: Vec<OptimizationHint>,
89    },
90    ValidationResult {
91        passed: bool,
92        errors: Vec<String>,
93    },
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct SemanticError {
98    pub line: usize,
99    pub rule_id: String,
100    pub message: String,
101    pub expression: Option<String>,
102    pub plugin_name: Option<String>,
103}
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct OptimizationHint {
106    pub line: usize,
107    pub rule_id: String,
108    pub before: String,
109    pub after: String,
110}
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ResponseOptions {
113    pub partial: bool,
114    pub redact: Vec<String>,
115    pub has_tolerance: bool,
116    pub unordered_arrays: bool,
117    pub with_asserts: bool,
118}
119
120impl From<&ComparisonOptions> for ResponseOptions {
121    fn from(o: &ComparisonOptions) -> Self {
122        Self {
123            partial: o.partial,
124            redact: o.redact.clone(),
125            has_tolerance: o.tolerance.is_some(),
126            unordered_arrays: o.unordered_arrays,
127            with_asserts: o.with_asserts,
128        }
129    }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Workflow {
134    pub file_path: String,
135    pub events: Vec<WorkflowEvent>,
136    pub summary: WorkflowSummary,
137}
138impl Workflow {
139    pub fn has_streaming(&self) -> bool {
140        let rc = self
141            .events
142            .iter()
143            .filter(|e| matches!(e, WorkflowEvent::RequestSent { .. }))
144            .count();
145        let ec = self
146            .events
147            .iter()
148            .filter(|e| matches!(e, WorkflowEvent::ResponseReceived { .. }))
149            .count();
150        rc > 1 || ec > 1
151    }
152    pub fn rpc_mode_name(&self) -> &str {
153        let rc = self
154            .events
155            .iter()
156            .filter(|e| matches!(e, WorkflowEvent::RequestSent { .. }))
157            .count();
158        let ec = self
159            .events
160            .iter()
161            .filter(|e| matches!(e, WorkflowEvent::ResponseReceived { .. }))
162            .count();
163        if rc > 1 && ec == 1 {
164            return "Client Streaming";
165        }
166        if rc == 1 && ec > 1 {
167            return "Server Streaming";
168        }
169        if rc > 1 && ec > 1 {
170            return "Bidi Streaming";
171        }
172        "Unary"
173    }
174    pub fn from_plan(plan: &ExecutionPlan) -> Self {
175        let mut events = vec![
176            WorkflowEvent::TestLoaded {
177                file_path: plan.file_path.clone(),
178            },
179            WorkflowEvent::Connect {
180                backend: plan.connection.backend.clone(),
181                address: plan.connection.address.clone(),
182            },
183            WorkflowEvent::Connected {
184                backend: plan.connection.backend.clone(),
185                address: plan.connection.address.clone(),
186            },
187        ];
188        for r in &plan.requests {
189            events.push(WorkflowEvent::SendRequest {
190                backend: plan.connection.backend.clone(),
191                request_index: r.index,
192                content_type: r.content_type.clone(),
193                line_range: (r.line_start, r.line_end),
194            });
195            events.push(WorkflowEvent::RequestSent {
196                backend: plan.connection.backend.clone(),
197                request_index: r.index,
198            });
199        }
200        for e in &plan.expectations {
201            events.push(WorkflowEvent::ReceiveResponse {
202                backend: plan.connection.backend.clone(),
203                response_index: e.index,
204                expectation_type: e.expectation_type.clone(),
205            });
206            events.push(WorkflowEvent::ResponseReceived {
207                backend: plan.connection.backend.clone(),
208                response_index: e.index,
209            });
210        }
211        for a in &plan.assertions {
212            events.push(WorkflowEvent::Assertion {
213                backend: plan.connection.backend.clone(),
214                expression: a.assertions.first().cloned().unwrap_or_default(),
215                passed: true,
216                line: a.line_start,
217            });
218        }
219        for ex in &plan.extractions {
220            for (k, v) in &ex.variables {
221                events.push(WorkflowEvent::Extraction {
222                    backend: plan.connection.backend.clone(),
223                    variable: k.clone(),
224                    value: v.clone(),
225                    line: ex.line_start,
226                });
227            }
228        }
229        events.push(WorkflowEvent::Done {
230            backend: plan.connection.backend.clone(),
231            duration_ms: 0,
232        });
233
234        let summary = WorkflowSummary {
235            total_requests: plan.requests.len(),
236            total_responses: plan
237                .expectations
238                .iter()
239                .filter(|e| e.expectation_type == "response")
240                .count(),
241            total_extractions: plan.extractions.len(),
242            total_assertions: plan.assertions.len(),
243            backends: vec![plan.connection.backend.clone()],
244            rpc_mode: plan.summary.rpc_mode_name.clone(),
245            has_streaming: plan.requests.len() > 1 || plan.expectations.len() > 1,
246            has_bidi_streaming: plan.requests.len() > 1 && plan.expectations.len() > 1,
247        };
248        Self {
249            file_path: plan.file_path.clone(),
250            events,
251            summary,
252        }
253    }
254
255    pub fn from_document_with_analysis(doc: &GctfDocument) -> Self {
256        let file_path = doc.file_path.clone();
257        let mut events = vec![WorkflowEvent::TestLoaded {
258            file_path: file_path.clone(),
259        }];
260
261        events.push(WorkflowEvent::Connect {
262            backend: "default".into(),
263            address: String::new(),
264        });
265        events.push(WorkflowEvent::Connected {
266            backend: "default".into(),
267            address: String::new(),
268        });
269
270        if let Some(s) = doc.first_section(apif_ast::SectionType::Endpoint)
271            && let apif_ast::SectionContent::Single(e) = &s.content
272        {
273            events.push(WorkflowEvent::LoadDescriptors {
274                backend: "default".into(),
275                service: e.clone(),
276            });
277        }
278        events.push(WorkflowEvent::DescriptorsLoaded {
279            backend: "default".into(),
280            service: String::new(),
281            method_count: 0,
282        });
283
284        for (i, s) in doc
285            .sections_by_type(apif_ast::SectionType::Request)
286            .iter()
287            .enumerate()
288        {
289            let ct = match &s.content {
290                apif_ast::SectionContent::Json(_) => "json",
291                apif_ast::SectionContent::JsonLines(_) => "json-lines",
292                _ => "unknown",
293            };
294            events.push(WorkflowEvent::SendRequest {
295                backend: "default".into(),
296                request_index: i + 1,
297                content_type: ct.into(),
298                line_range: (s.start_line, s.end_line),
299            });
300            events.push(WorkflowEvent::RequestSent {
301                backend: "default".into(),
302                request_index: i + 1,
303            });
304        }
305        for (i, _s) in doc
306            .sections_by_type(apif_ast::SectionType::Response)
307            .iter()
308            .enumerate()
309        {
310            events.push(WorkflowEvent::ReceiveResponse {
311                backend: "default".into(),
312                response_index: i + 1,
313                expectation_type: "response".into(),
314            });
315            events.push(WorkflowEvent::ResponseReceived {
316                backend: "default".into(),
317                response_index: i + 1,
318            });
319        }
320        if let Some(s) = doc.first_section(apif_ast::SectionType::Error) {
321            events.push(WorkflowEvent::Error {
322                backend: "default".into(),
323                message: format!("Error at line {}", s.start_line),
324            });
325        }
326        for s in doc.sections_by_type(apif_ast::SectionType::Asserts) {
327            if let apif_ast::SectionContent::Assertions(lines) = &s.content {
328                for line in lines {
329                    events.push(WorkflowEvent::Assertion {
330                        backend: "default".into(),
331                        expression: line.clone(),
332                        passed: true,
333                        line: s.start_line,
334                    });
335                }
336            }
337        }
338        for s in doc.sections_by_type(apif_ast::SectionType::Extract) {
339            if let apif_ast::SectionContent::Extract(vars) = &s.content {
340                for (k, v) in vars {
341                    events.push(WorkflowEvent::Extraction {
342                        backend: "default".into(),
343                        variable: k.clone(),
344                        value: v.clone(),
345                        line: s.start_line,
346                    });
347                }
348            }
349        }
350
351        if let Some(mismatches) = doc
352            .first_section(apif_ast::SectionType::Meta)
353            .and_then(|_| {
354                let tm = apif_semantics::collect_assertion_type_mismatches(doc);
355                let up = apif_semantics::collect_unknown_plugin_calls(doc);
356                if tm.is_empty() && up.is_empty() {
357                    None
358                } else {
359                    Some((
360                        tm.into_iter()
361                            .map(|u| SemanticError {
362                                line: u.line,
363                                rule_id: u.rule_id.clone(),
364                                message: u.message,
365                                expression: Some(u.expression),
366                                plugin_name: Some(u.rule_id.clone()),
367                            })
368                            .collect(),
369                        up.into_iter().map(|u| u.plugin_name).collect(),
370                    ))
371                }
372            })
373        {
374            events.push(WorkflowEvent::SemanticAnalysis {
375                type_mismatches: mismatches.0,
376                unknown_plugins: mismatches.1,
377            });
378        }
379
380        let hints: Vec<OptimizationHint> = apif_optimizer::collect_assertion_optimizations(
381            doc,
382            optimizer::OptimizeLevel::Advisory,
383        )
384        .into_iter()
385        .map(|h| OptimizationHint {
386            line: h.line,
387            rule_id: h.rule_id.to_string(),
388            before: h.before,
389            after: h.after,
390        })
391        .collect();
392        if !hints.is_empty() {
393            events.push(WorkflowEvent::OptimizationFound { hints });
394        }
395
396        let validation = apif_parser::validate_document(doc);
397        events.push(WorkflowEvent::ValidationResult {
398            passed: validation.is_ok(),
399            errors: validation
400                .err()
401                .into_iter()
402                .map(|e| e.to_string())
403                .collect(),
404        });
405
406        let plan = ExecutionPlan::from_document(doc);
407        events.push(WorkflowEvent::Done {
408            backend: "default".into(),
409            duration_ms: 0,
410        });
411
412        let summary = WorkflowSummary {
413            total_requests: plan.requests.len(),
414            total_responses: plan
415                .expectations
416                .iter()
417                .filter(|e| e.expectation_type == "response")
418                .count(),
419            total_extractions: plan.extractions.len(),
420            total_assertions: plan.assertions.len(),
421            backends: vec!["default".into()],
422            rpc_mode: plan.summary.rpc_mode_name.clone(),
423            has_streaming: plan.requests.len() > 1 || plan.expectations.len() > 1,
424            has_bidi_streaming: plan.requests.len() > 1 && plan.expectations.len() > 1,
425        };
426        Self {
427            file_path,
428            events,
429            summary,
430        }
431    }
432}
433
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
435pub struct WorkflowSummary {
436    pub total_requests: usize,
437    pub total_responses: usize,
438    pub total_extractions: usize,
439    pub total_assertions: usize,
440    pub backends: Vec<String>,
441    pub rpc_mode: String,
442    pub has_streaming: bool,
443    pub has_bidi_streaming: bool,
444}