Skip to main content

powerio/
formats.rs

1//! Facade format metadata.
2//!
3//! Component format enums select parsers and emitters inside their owning
4//! crates. The facade exposes one small descriptor instead, so applications
5//! can name an emitted artifact without depending on those implementation
6//! enums or copying their alias tables.
7
8use powerio_tx::format::routing::TransmissionFormat;
9
10/// The canonical identity and destination shape of a PowerIO format.
11///
12/// `extension` is the conventional filename suffix without a leading dot; it
13/// may be compound. It is `None` for directory formats with no primary case
14/// file. `can_emit` reports whether a fresh universal emitter
15/// exists for the format. It does not promise that every concrete module value can
16/// emit that format, and it is not a build feature probe. A false value neither
17/// promises nor forbids a same format retained source echo.
18#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
19#[non_exhaustive]
20pub struct FormatInfo {
21    /// Canonical stable token used by parse and emit operations when the
22    /// current build and concrete value support the format.
23    pub token: &'static str,
24    /// Conventional filename suffix without a leading dot.
25    pub extension: Option<&'static str>,
26    /// Whether a path destination names an output directory rather than one
27    /// file.
28    pub is_directory: bool,
29    /// Whether a fresh universal emitter exists for this format.
30    pub can_emit: bool,
31}
32
33const fn info(
34    token: &'static str,
35    extension: Option<&'static str>,
36    is_directory: bool,
37    can_emit: bool,
38) -> FormatInfo {
39    FormatInfo {
40        token,
41        extension,
42        is_directory,
43        can_emit,
44    }
45}
46
47/// Resolve a format token or common alias to facade owned metadata.
48///
49/// This includes transmission and distribution grid exchange formats, the
50/// standalone geographic layer document, and the three PSS/E contingency
51/// analysis files. PowerIO IR is not a grid exchange format and therefore is
52/// not returned here.
53///
54#[must_use]
55pub fn resolve_format(name: &str) -> Option<FormatInfo> {
56    match name {
57        "bmopf-json@0.1.0" => return Some(info("bmopf-json@0.1.0", Some("json"), false, true)),
58        "bmopf-json@0.2.0" => return Some(info("bmopf-json@0.2.0", Some("json"), false, true)),
59        _ => {}
60    }
61
62    if crate::is_geo_layer_token(name) {
63        return Some(info(
64            "geo-json",
65            Some(powerio_tx::geo::GEO_LAYER_EXTENSION),
66            false,
67            true,
68        ));
69    }
70    if crate::is_pwd_display_token(name) {
71        return Some(info("powerworld-pwd", Some("pwd"), false, false));
72    }
73    if let Some(kind) = crate::contingency_file_of_token(name) {
74        return Some(info(kind.token(), Some(kind.extension()), false, true));
75    }
76    if let Some(format) = powerio_tx::format::parse_target_format(name) {
77        let is_cgmes = format == powerio_tx::TargetFormat::Cgmes;
78        return Some(info(
79            format.token(),
80            (!is_cgmes).then_some(format.extension()),
81            is_cgmes,
82            !matches!(format, powerio_tx::TargetFormat::DeepMindOpfDataJson),
83        ));
84    }
85
86    if let Some(format) = powerio_dist::parse_dist_target_format(name) {
87        return Some(match format.name() {
88            "dss" => info("dss", Some("dss"), true, true),
89            "pmd-json" => info("pmd-json", Some("json"), false, true),
90            "bmopf-json" => info("bmopf-json", Some("json"), false, true),
91            _ => return None,
92        });
93    }
94
95    match powerio_tx::format::routing::parse_transmission_format(name) {
96        Some(TransmissionFormat::PypsaCsv) => Some(info("pypsa-csv", None, true, true)),
97        Some(TransmissionFormat::Pwb) => Some(info("pwb", Some("pwb"), false, false)),
98        Some(TransmissionFormat::Gridfm) => Some(info("gridfm", None, true, true)),
99        // The public IEEE archives name their CDF cases `.txt`; the reader also
100        // recognizes `.cdf` and any name with the declared format.
101        Some(TransmissionFormat::IeeeCdf) => Some(info("ieee-cdf", Some("txt"), false, false)),
102        _ => None,
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn aliases_resolve_to_one_canonical_descriptor() {
112        assert_eq!(resolve_format("m"), resolve_format("MATPOWER"));
113        assert_eq!(resolve_format("pm").unwrap().token, "powermodels-json");
114        assert_eq!(resolve_format("engineering").unwrap().token, "pmd-json");
115        assert_eq!(resolve_format("xiidm").unwrap().token, "xiidm");
116        assert_eq!(resolve_format("jiidm").unwrap().token, "jiidm");
117        assert_eq!(resolve_format("jiidm").unwrap().extension, Some("jiidm"));
118        assert!(resolve_format("cgmes").unwrap().is_directory);
119        assert_eq!(resolve_format("iidm"), None);
120        assert_eq!(resolve_format("rawx"), None);
121        assert_eq!(resolve_format("psse-rawx").unwrap().token, "psse-rawx");
122        assert_eq!(resolve_format("con"), resolve_format("PSSE_CON"));
123        assert_eq!(resolve_format("subsystem").unwrap().token, "psse-sub");
124        assert_eq!(resolve_format("mon").unwrap().extension, Some("mon"));
125    }
126
127    #[test]
128    fn destination_and_read_only_shapes_are_explicit() {
129        let dss = resolve_format("opendss").unwrap();
130        assert!(dss.is_directory);
131        assert!(dss.can_emit);
132        assert_eq!(dss.extension, Some("dss"));
133
134        let pypsa = resolve_format("pypsa").unwrap();
135        assert!(pypsa.is_directory);
136        assert_eq!(pypsa.extension, None);
137
138        let pwb = resolve_format("pwb").unwrap();
139        assert!(!pwb.is_directory);
140        assert!(!pwb.can_emit);
141        assert_eq!(pwb.extension, Some("pwb"));
142
143        let cdf = resolve_format("cdf").unwrap();
144        assert_eq!(cdf.token, "ieee-cdf");
145        assert!(!cdf.is_directory);
146        assert!(!cdf.can_emit);
147        assert_eq!(cdf.extension, Some("txt"));
148    }
149
150    #[test]
151    fn nonformats_do_not_resolve() {
152        assert_eq!(resolve_format("not-a-format"), None);
153        assert_eq!(resolve_format("json"), None);
154        assert_eq!(resolve_format("pio-json"), None);
155    }
156}