use std::borrow::Cow;
use std::collections::BTreeMap;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct ScenarioId(Cow<'static, str>);
impl ScenarioId {
pub const fn from_static(s: &'static str) -> Self {
ScenarioId(Cow::Borrowed(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ScenarioId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
const BUILTIN_KINDS: &[crate::api::ScenarioKind] = {
use crate::api::ScenarioKind::*;
&[
Clock,
Inertial,
Integrity,
TimeTransfer,
QuantumTimeTransfer,
QuantumGnssFreeNav,
QuantumAnomalyDetect,
Hybrid,
Fusion,
HybridUkf,
GnssIns,
GnssSim,
Jamming,
Spoof,
SpoofDetect,
Sweep,
SweepNd,
Orbit,
Ephemeris,
LunarIntegrity,
LunarTime,
LunarVlbi,
LunarCombination,
LunarFrameRealise,
LunarService,
LunarDpnt,
LunarInterop,
GravityMap,
Terrain,
TerrainSlam,
CombinedAltPnt,
Pvt,
MarsPnt,
ImpairmentEval,
QuantumTrade,
SpaceWeather,
OemInterop,
LaunchWindow,
Reentry,
EoCoverage,
SpacePacket,
AttitudeBudget,
Passes,
LinkBudget,
LunarTimeBudget,
RealtimeFrameEop,
HybridOpticalRf,
CislunarObservability,
ConflictResilience,
]
};
pub mod ids {
use super::ScenarioId;
pub const CLOCK: ScenarioId = ScenarioId::from_static("clock");
pub const INERTIAL: ScenarioId = ScenarioId::from_static("inertial");
pub const INTEGRITY: ScenarioId = ScenarioId::from_static("integrity");
pub const TIMETRANSFER: ScenarioId = ScenarioId::from_static("timetransfer");
pub const QUANTUM_TIME_TRANSFER: ScenarioId = ScenarioId::from_static("quantum-time-transfer");
pub const QUANTUM_GNSS_FREE_NAV: ScenarioId = ScenarioId::from_static("quantum-gnss-free-nav");
pub const QUANTUM_ANOMALY_DETECT: ScenarioId =
ScenarioId::from_static("quantum-anomaly-detect");
pub const HYBRID: ScenarioId = ScenarioId::from_static("hybrid");
pub const FUSION: ScenarioId = ScenarioId::from_static("fusion");
pub const HYBRID_UKF: ScenarioId = ScenarioId::from_static("hybrid-ukf");
pub const GNSS_INS: ScenarioId = ScenarioId::from_static("gnss-ins");
pub const GNSS_SIM: ScenarioId = ScenarioId::from_static("gnss-sim");
pub const JAMMING: ScenarioId = ScenarioId::from_static("jamming");
pub const SPOOF: ScenarioId = ScenarioId::from_static("spoof");
pub const SPOOF_DETECT: ScenarioId = ScenarioId::from_static("spoof-detect");
pub const SWEEP: ScenarioId = ScenarioId::from_static("sweep");
pub const SWEEP_ND: ScenarioId = ScenarioId::from_static("sweep-nd");
pub const ORBIT: ScenarioId = ScenarioId::from_static("orbit");
pub const EPHEMERIS: ScenarioId = ScenarioId::from_static("ephemeris");
pub const LUNAR_INTEGRITY: ScenarioId = ScenarioId::from_static("lunar-integrity");
pub const LUNAR_TIME_OFFSET: ScenarioId = ScenarioId::from_static("lunar-time-offset");
pub const LUNAR_VLBI: ScenarioId = ScenarioId::from_static("lunar-vlbi");
pub const LUNAR_JOINT_OD_CLOCK: ScenarioId = ScenarioId::from_static("lunar-joint-od-clock");
pub const LUNAR_FRAME_REALISATION: ScenarioId =
ScenarioId::from_static("lunar-frame-realisation");
pub const MOONLIGHT_SERVICE_VOLUME: ScenarioId =
ScenarioId::from_static("moonlight-service-volume");
pub const LUNAR_DIFFERENTIAL_PNT: ScenarioId =
ScenarioId::from_static("lunar-differential-pnt");
pub const LUNAR_INTEROP_EXPORT: ScenarioId = ScenarioId::from_static("lunar-interop-export");
pub const GRAVITY_MAP: ScenarioId = ScenarioId::from_static("gravity-map");
pub const TERRAIN_NAV: ScenarioId = ScenarioId::from_static("terrain-nav");
pub const TERRAIN_SLAM: ScenarioId = ScenarioId::from_static("terrain-slam");
pub const COMBINED_ALTPNT: ScenarioId = ScenarioId::from_static("combined-altpnt");
pub const PVT: ScenarioId = ScenarioId::from_static("pvt");
pub const MARS_PNT: ScenarioId = ScenarioId::from_static("mars-pnt");
pub const IMPAIRMENT_EVAL: ScenarioId = ScenarioId::from_static("impairment-eval");
pub const QUANTUM_TRADE: ScenarioId = ScenarioId::from_static("quantum-trade");
pub const SPACE_WEATHER: ScenarioId = ScenarioId::from_static("space-weather");
pub const OEM_INTEROP: ScenarioId = ScenarioId::from_static("oem-interop");
pub const LAUNCH_WINDOW: ScenarioId = ScenarioId::from_static("launch-window");
pub const REENTRY: ScenarioId = ScenarioId::from_static("reentry");
pub const EO_COVERAGE: ScenarioId = ScenarioId::from_static("eo-coverage");
pub const SPACE_PACKET: ScenarioId = ScenarioId::from_static("space-packet");
pub const ATTITUDE_BUDGET: ScenarioId = ScenarioId::from_static("attitude-budget");
pub const PASSES: ScenarioId = ScenarioId::from_static("passes");
pub const LINK_BUDGET: ScenarioId = ScenarioId::from_static("link-budget");
pub const LUNAR_TIME_BUDGET: ScenarioId = ScenarioId::from_static("lunar-time-budget");
pub const REALTIME_FRAME_EOP: ScenarioId = ScenarioId::from_static("realtime-frame-eop");
pub const HYBRID_OPTICAL_RF: ScenarioId = ScenarioId::from_static("hybrid-optical-rf");
pub const CISLUNAR_OBSERVABILITY: ScenarioId =
ScenarioId::from_static("cislunar-observability");
pub const CONFLICT_RESILIENCE: ScenarioId = ScenarioId::from_static("conflict-resilience");
pub fn all() -> Vec<ScenarioId> {
super::BUILTIN_KINDS
.iter()
.map(|k| ScenarioId::from_static(k.as_str()))
.collect()
}
}
pub trait ScenarioFactory: Send + Sync {
fn id(&self) -> &ScenarioId;
fn build(&self, src: &str) -> Result<Box<dyn crate::api::Scenario>, String>;
fn metadata(&self) -> crate::api::ScenarioMeta {
crate::api::ScenarioMeta {
name: "",
description: "",
required_fields: &[],
optional_fields: &[],
}
}
}
#[derive(Default)]
pub struct PackRegistry {
scenarios: BTreeMap<ScenarioId, Box<dyn ScenarioFactory>>,
}
impl PackRegistry {
pub fn register(&mut self, factory: Box<dyn ScenarioFactory>) -> &mut Self {
let id = factory.id().clone();
debug_assert!(
!self.scenarios.contains_key(&id),
"duplicate scenario id registered: {id}"
);
self.scenarios.insert(id, factory);
self
}
pub fn build(
&self,
id: &ScenarioId,
src: &str,
) -> Result<Box<dyn crate::api::Scenario>, String> {
match self.scenarios.get(id) {
Some(factory) => factory.build(src),
None => Err(format!("unknown scenario id: {id}")),
}
}
pub fn contains(&self, id: &ScenarioId) -> bool {
self.scenarios.contains_key(id)
}
pub fn ids(&self) -> impl Iterator<Item = &ScenarioId> {
self.scenarios.keys()
}
pub fn with_builtins() -> Self {
let mut reg = PackRegistry::default();
for &kind in BUILTIN_KINDS {
reg.register(Box::new(BuiltinFactory {
id: ScenarioId::from_static(kind.as_str()),
kind,
}));
}
reg
}
}
struct BuiltinScenario {
kind: crate::api::ScenarioKind,
src: String,
}
impl crate::api::Scenario for BuiltinScenario {
fn run(&self) -> Result<crate::api::RunOutput, crate::api::KshanaError> {
crate::api::run_builtin_kind(self.kind, &self.src)
.map_err(crate::api::KshanaError::InvalidInput)
}
}
struct BuiltinFactory {
id: ScenarioId,
kind: crate::api::ScenarioKind,
}
impl ScenarioFactory for BuiltinFactory {
fn id(&self) -> &ScenarioId {
&self.id
}
fn build(&self, src: &str) -> Result<Box<dyn crate::api::Scenario>, String> {
Ok(Box::new(BuiltinScenario {
kind: self.kind,
src: src.to_string(),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_all_matches_as_str_table() {
let all = ids::all();
assert_eq!(all.len(), BUILTIN_KINDS.len());
for (id, kind) in all.iter().zip(BUILTIN_KINDS.iter()) {
assert_eq!(id.as_str(), kind.as_str());
}
}
#[test]
fn id_const_round_trips() {
assert_eq!(ids::CLOCK.as_str(), "clock");
assert_eq!(ids::LUNAR_TIME_OFFSET.as_str(), "lunar-time-offset");
assert_eq!(ids::JAMMING.as_str(), "jamming");
}
#[test]
fn with_builtins_registers_every_kind() {
let reg = PackRegistry::with_builtins();
assert_eq!(reg.ids().count(), BUILTIN_KINDS.len());
for kind in BUILTIN_KINDS {
assert!(reg.contains(&ScenarioId::from_static(kind.as_str())));
}
}
#[test]
fn ids_are_sorted_and_unique() {
let reg = PackRegistry::with_builtins();
let ids: Vec<&ScenarioId> = reg.ids().collect();
for w in ids.windows(2) {
assert!(
w[0] < w[1],
"ids must be strictly increasing: {} !< {}",
w[0],
w[1]
);
}
}
#[test]
fn build_unknown_id_errors() {
let reg = PackRegistry::with_builtins();
let err = match reg.build(&ScenarioId::from_static("does-not-exist"), "") {
Ok(_) => panic!("expected unknown-id error"),
Err(e) => e,
};
assert_eq!(err, "unknown scenario id: does-not-exist");
}
#[test]
fn default_is_empty() {
let reg = PackRegistry::default();
assert_eq!(reg.ids().count(), 0);
assert!(!reg.contains(&ids::CLOCK));
}
#[test]
fn build_is_lazy_parse_deferred_to_run() {
let reg = PackRegistry::with_builtins();
assert!(reg.build(&ids::CLOCK, "not = valid = toml").is_ok());
}
}