Skip to main content

tropel_engine/
input.rs

1//! Input resolution — Driver or Scenario dispatch.
2//!
3//! Moved out of the former `engine.rs` god-file.
4
5use std::collections::HashMap;
6use std::sync::Arc;
7use tropel_ext::registry::ExtensionRegistry;
8use tropel_sdk::scenario::Scenario;
9use tropel_sdk::traits::{Driver, InputAdapter};
10use tropel_sdk::{Result, TropelError};
11
12pub(crate) enum ResolvedInput {
13    /// A declarative scenario, plus the id of the [`InputAdapter`] that
14    /// produced it (`postman` / `har` / `openapi` / `bru` / `insomnia` /
15    /// `http` / `k6`).
16    ///
17    /// TR-501: the adapter id is the ONLY reliable statement of the input
18    /// format, and it decides which JS shims each VU has to materialise
19    /// (`ShimBundle::for_format`). It used to be logged and thrown away, so
20    /// `run_scenario_vus` had nothing but a keyword scan of the file bytes to
21    /// go on.
22    Scenario(Arc<Scenario>, String),
23    Driver(Box<dyn Driver>),
24}
25
26pub(crate) fn resolve_input_or_driver(
27    input_path: &str,
28    format_hint: Option<&str>,
29    registry: &ExtensionRegistry,
30    base_env: &HashMap<String, String>,
31    pre_read: Option<&[u8]>,
32) -> Result<ResolvedInput> {
33    let input_p = std::path::Path::new(input_path);
34    // TR-313: reuse the bytes already read by the caller (engine startup
35    // reads the file once for `declared_options`; this function used to
36    // re-read it on EVERY call — twice per run, back-to-back with the
37    // caller's own read). `None` → read here (the standalone path).
38    let bytes: Vec<u8> = match pre_read {
39        Some(b) => b.to_vec(),
40        None => std::fs::read(input_path)
41            .map_err(|e| TropelError::Parse(format!("Failed to read '{}': {}", input_path, e)))?,
42    };
43
44    // 1. Try drivers first
45    let driver: Option<Box<dyn Driver>> = if let Some(fmt) = format_hint {
46        registry.resolve_driver_by_id(fmt)
47    } else {
48        registry.resolve_driver(&bytes)
49    };
50
51    if let Some(driver) = driver {
52        tracing::info!(
53            "Input '{}' resolved by driver '{}'",
54            input_path,
55            driver.id()
56        );
57        return Ok(ResolvedInput::Driver(driver));
58    }
59
60    // 2. Fall back to input adapters
61    let adapter: Box<dyn InputAdapter> = if let Some(fmt) = format_hint {
62        registry.resolve_input_by_id(fmt).ok_or_else(|| {
63            let available = registry.list_inputs();
64            TropelError::Config(format!(
65                "Unknown input format '{}'. Available formats: {}",
66                fmt,
67                available.join(", ")
68            ))
69        })?
70    } else {
71        registry.resolve_input(&bytes).ok_or_else(|| {
72            let available = registry.list_inputs();
73            TropelError::Parse(format!(
74                "No input adapter recognized '{}'. Available adapters: {}",
75                input_path,
76                if available.is_empty() {
77                    "(none registered — check build configuration)".to_string()
78                } else {
79                    available.join(", ")
80                }
81            ))
82        })?
83    };
84
85    tracing::info!(
86        "Input '{}' resolved by adapter '{}'",
87        input_path,
88        adapter.id()
89    );
90
91    let format_id = adapter.id().to_string();
92    let mut scenario = adapter.parse_with_path(&bytes, Some(input_p))?;
93    for (key, val) in base_env {
94        scenario
95            .variables
96            .entry(key.clone())
97            .or_insert_with(|| serde_json::Value::String(val.clone()));
98    }
99
100    Ok(ResolvedInput::Scenario(Arc::new(scenario), format_id))
101}