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}