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}