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