1use serde::Deserialize;
8
9const POWERIO_IR_SCHEMA: &str = "pio-ir";
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Detection<T> {
17 Known(T),
18 Unknown,
19 Ambiguous,
20}
21
22impl<T> Detection<T> {
23 pub fn known(self) -> Option<T> {
24 match self {
25 Self::Known(value) => Some(value),
26 Self::Unknown | Self::Ambiguous => None,
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum Domain {
34 Transmission,
35 Distribution,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum TransmissionFormat {
41 Matpower,
42 PowerModelsJson,
43 EgretJson,
44 Psse,
45 Psse34,
46 Psse35,
47 PsseRawx,
48 PowerWorld,
49 PandapowerJson,
50 PypsaCsv,
51 Pslf,
52 Pwb,
53 Gridfm,
54 Goc3Json,
55 SurgeJson,
56 DeepMindOpfDataJson,
57 Xiidm,
58 Jiidm,
59 Cgmes,
60 Ucte,
61 IeeeCdf,
63}
64
65impl TransmissionFormat {
66 pub fn name(self) -> &'static str {
67 match self {
68 Self::Matpower => "matpower",
69 Self::PowerModelsJson => "powermodels-json",
70 Self::EgretJson => "egret-json",
71 Self::Psse => "psse",
72 Self::Psse34 => "psse34",
73 Self::Psse35 => "psse35",
74 Self::PsseRawx => "psse-rawx",
75 Self::PowerWorld => "powerworld",
76 Self::PandapowerJson => "pandapower-json",
77 Self::PypsaCsv => "pypsa-csv",
78 Self::Pslf => "pslf",
79 Self::Pwb => "pwb",
80 Self::Gridfm => "gridfm",
81 Self::Goc3Json => "goc3-json",
82 Self::SurgeJson => "surge-json",
83 Self::DeepMindOpfDataJson => "opfdata-json",
84 Self::Xiidm => "xiidm",
85 Self::Jiidm => "jiidm",
86 Self::Cgmes => "cgmes",
87 Self::Ucte => "ucte",
88 Self::IeeeCdf => "ieee-cdf",
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum DistributionFormat {
96 Dss,
97 PmdJson,
98 BmopfJson,
99}
100
101impl DistributionFormat {
102 pub fn name(self) -> &'static str {
103 match self {
104 Self::Dss => "dss",
105 Self::PmdJson => "pmd-json",
106 Self::BmopfJson => "bmopf-json",
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum SourceFormat {
114 Transmission(TransmissionFormat),
115 Distribution(DistributionFormat),
116}
117
118impl SourceFormat {
119 pub fn domain(self) -> Domain {
120 match self {
121 Self::Transmission(_) => Domain::Transmission,
122 Self::Distribution(_) => Domain::Distribution,
123 }
124 }
125
126 pub fn name(self) -> &'static str {
127 match self {
128 Self::Transmission(format) => format.name(),
129 Self::Distribution(format) => format.name(),
130 }
131 }
132}
133
134pub type JsonFormat = SourceFormat;
135
136pub fn classify_format_name(name: &str) -> Detection<SourceFormat> {
138 if let Some(format) = parse_transmission_format(name) {
139 return Detection::Known(SourceFormat::Transmission(format));
140 }
141 if let Some(format) = parse_distribution_format(name) {
142 return Detection::Known(SourceFormat::Distribution(format));
143 }
144 Detection::Unknown
145}
146
147pub fn parse_transmission_format(name: &str) -> Option<TransmissionFormat> {
148 let key = canonical_key(name);
149 match key.as_str() {
150 "matpower" | "m" => Some(TransmissionFormat::Matpower),
151 "powermodelsjson" | "powermodels" | "pm" => Some(TransmissionFormat::PowerModelsJson),
152 "egretjson" | "egret" => Some(TransmissionFormat::EgretJson),
153 "psse" | "psse33" | "raw" | "raw33" => Some(TransmissionFormat::Psse),
154 "psse34" | "raw34" => Some(TransmissionFormat::Psse34),
155 "psse35" | "raw35" => Some(TransmissionFormat::Psse35),
156 "psserawx" | "rawx" => Some(TransmissionFormat::PsseRawx),
157 "powerworld" | "aux" => Some(TransmissionFormat::PowerWorld),
158 "pandapowerjson" | "pandapower" | "pp" => Some(TransmissionFormat::PandapowerJson),
159 "pypsacsv" | "pypsa" => Some(TransmissionFormat::PypsaCsv),
160 "pslf" | "epc" | "pslfepc" => Some(TransmissionFormat::Pslf),
161 "pwb" => Some(TransmissionFormat::Pwb),
162 "gridfm" => Some(TransmissionFormat::Gridfm),
163 "goc3" | "goc3json" | "go3" | "gochallenge3" | "c3" => Some(TransmissionFormat::Goc3Json),
164 "surge" | "surgejson" => Some(TransmissionFormat::SurgeJson),
165 "xiidm" | "iidm" => Some(TransmissionFormat::Xiidm),
166 "jiidm" => Some(TransmissionFormat::Jiidm),
167 "cgmes" => Some(TransmissionFormat::Cgmes),
168 "ucte" | "uct" | "uctedef" => Some(TransmissionFormat::Ucte),
169 "ieeecdf" | "cdf" => Some(TransmissionFormat::IeeeCdf),
170 "opfdata"
171 | "opfdatajson"
172 | "deepmindopfdata"
173 | "deepmindopfdatajson"
174 | "gridopt"
175 | "gridoptjson" => Some(TransmissionFormat::DeepMindOpfDataJson),
176 _ => None,
177 }
178}
179
180pub fn parse_distribution_format(name: &str) -> Option<DistributionFormat> {
181 let key = canonical_key(name);
182 match key.as_str() {
183 "dss" | "opendss" => Some(DistributionFormat::Dss),
184 "pmd" | "pmdjson" | "engineering" => Some(DistributionFormat::PmdJson),
185 "bmopf" | "bmopfjson" => Some(DistributionFormat::BmopfJson),
186 _ => None,
187 }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194pub enum JsonClass {
195 Module,
199 Case(Detection<JsonFormat>),
201}
202
203pub const JSON_CLASSES: [&str; 5] = [
213 "transmission",
214 "distribution",
215 "module",
216 "ambiguous",
217 "unknown",
218];
219
220impl JsonClass {
221 #[must_use]
224 pub fn family(self) -> &'static str {
225 match self {
226 Self::Module => "module",
227 Self::Case(Detection::Known(format)) => match format.domain() {
228 Domain::Transmission => "transmission",
229 Domain::Distribution => "distribution",
230 },
231 Self::Case(Detection::Ambiguous) => "ambiguous",
232 Self::Case(Detection::Unknown) => "unknown",
233 }
234 }
235}
236
237pub fn classify_json_text(text: &str) -> JsonClass {
245 if super::xiidm::looks_like_jiidm(text) {
248 return JsonClass::Case(Detection::Known(SourceFormat::Transmission(
249 TransmissionFormat::Jiidm,
250 )));
251 }
252 let Ok(header) = serde_json::from_str::<JsonHeader>(text.trim_start_matches('\u{feff}')) else {
255 return JsonClass::Case(Detection::Unknown);
256 };
257 if header.schema.as_deref() == Some(POWERIO_IR_SCHEMA) {
259 return JsonClass::Module;
260 }
261 header.classify()
262}
263
264#[must_use]
270pub fn classify_json_bytes(bytes: &[u8]) -> JsonClass {
271 let Ok(text) = std::str::from_utf8(bytes) else {
272 return JsonClass::Case(Detection::Unknown);
273 };
274 classify_json_text(text)
275}
276
277fn canonical_key(name: &str) -> String {
278 name.to_ascii_lowercase()
279 .chars()
280 .filter(|c| *c != '-' && *c != '_')
281 .collect()
282}
283
284fn present<'de, D>(deserializer: D) -> Result<bool, D::Error>
291where
292 D: serde::Deserializer<'de>,
293{
294 serde::de::IgnoredAny::deserialize(deserializer)?;
295 Ok(true)
296}
297
298fn maybe_str<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
303where
304 D: serde::Deserializer<'de>,
305{
306 Ok(serde_json::Value::deserialize(deserializer)?
307 .as_str()
308 .map(str::to_owned))
309}
310
311fn maybe_object<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
316where
317 D: serde::Deserializer<'de>,
318 T: Deserialize<'de>,
319{
320 let value = serde_json::Value::deserialize(deserializer)?;
321 if value.is_object() {
322 T::deserialize(value)
323 .map(Some)
324 .map_err(serde::de::Error::custom)
325 } else {
326 Ok(None)
327 }
328}
329
330#[derive(Default)]
336struct NetworkField {
337 present: bool,
338 header: NetworkHeader,
339}
340
341impl<'de> Deserialize<'de> for NetworkField {
342 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
343 where
344 D: serde::Deserializer<'de>,
345 {
346 let value = serde_json::Value::deserialize(deserializer)?;
347 let header = if value.is_object() {
348 NetworkHeader::deserialize(value).map_err(serde::de::Error::custom)?
349 } else {
350 NetworkHeader::default()
351 };
352 Ok(Self {
353 present: true,
354 header,
355 })
356 }
357}
358
359#[derive(Default, Deserialize)]
360#[expect(clippy::struct_excessive_bools)]
361struct NetworkHeader {
362 #[serde(default, deserialize_with = "present")]
363 caseid: bool,
364 #[serde(default, deserialize_with = "present")]
365 simple_dispatchable_device: bool,
366 #[serde(default, deserialize_with = "present")]
367 ac_line: bool,
368 #[serde(default, deserialize_with = "present")]
369 two_winding_transformer: bool,
370}
371
372#[derive(Deserialize)]
373struct GridHeader {
374 #[serde(default, deserialize_with = "present")]
375 nodes: bool,
376 #[serde(default, deserialize_with = "present")]
377 edges: bool,
378 #[serde(default, deserialize_with = "present")]
379 context: bool,
380}
381
382#[derive(Deserialize)]
383struct SolutionHeader {
384 #[serde(default, deserialize_with = "present")]
385 nodes: bool,
386 #[serde(default, deserialize_with = "present")]
387 edges: bool,
388}
389
390#[derive(Deserialize)]
391struct MetadataHeader {
392 #[serde(default, deserialize_with = "present")]
393 objective: bool,
394}
395
396#[allow(clippy::struct_excessive_bools)]
404#[derive(Default, Deserialize)]
405struct JsonHeader {
406 #[serde(default, deserialize_with = "maybe_str")]
407 schema: Option<String>,
408 #[serde(default, deserialize_with = "maybe_str", rename = "_class")]
409 pandapower_class: Option<String>,
410 #[serde(default, deserialize_with = "present")]
411 elements: bool,
412 #[serde(default, deserialize_with = "present")]
413 system: bool,
414 #[serde(default)]
415 network: NetworkField,
416 #[serde(default, deserialize_with = "present")]
417 time_series_input: bool,
418 #[serde(default, deserialize_with = "present")]
419 time_series_output: bool,
420 #[serde(default, deserialize_with = "present")]
421 reliability: bool,
422 #[serde(default, deserialize_with = "maybe_str")]
423 format: Option<String>,
424 #[serde(default, deserialize_with = "present")]
425 schema_version: bool,
426 #[serde(default, deserialize_with = "maybe_object")]
427 grid: Option<GridHeader>,
428 #[serde(default, deserialize_with = "maybe_object")]
429 solution: Option<SolutionHeader>,
430 #[serde(default, deserialize_with = "maybe_object")]
431 metadata: Option<MetadataHeader>,
432 #[serde(default, deserialize_with = "present", rename = "baseMVA")]
433 base_mva_camel: bool,
434 #[serde(default, deserialize_with = "present")]
435 branch: bool,
436 #[serde(default, deserialize_with = "present")]
437 r#gen: bool,
438 #[serde(default, deserialize_with = "present")]
439 gencost: bool,
440 #[serde(default, deserialize_with = "present")]
441 data_model: bool,
442 #[serde(default, deserialize_with = "present")]
443 line: bool,
444 #[serde(default, deserialize_with = "present")]
445 linecode: bool,
446 #[serde(default, deserialize_with = "present")]
447 transformer: bool,
448 #[serde(default, deserialize_with = "present")]
449 voltage_source: bool,
450 #[serde(default, deserialize_with = "present")]
451 bus: bool,
452 #[serde(default, deserialize_with = "present")]
453 load: bool,
454 #[serde(default, deserialize_with = "present")]
455 generator: bool,
456 #[serde(default, deserialize_with = "present")]
457 shunt: bool,
458 #[serde(default, deserialize_with = "present")]
459 switch: bool,
460}
461
462impl JsonHeader {
463 fn classify(&self) -> JsonClass {
464 let is_pandapower = self.pandapower_class.as_deref() == Some("pandapowerNet");
465 let is_egret = self.elements && self.system;
466 let is_goc3 = self.time_series_output
467 || ((self.time_series_input || self.reliability)
468 && (self.network.header.simple_dispatchable_device
469 || self.network.header.ac_line
470 || self.network.header.two_winding_transformer));
471 let is_rawx = self.network.header.caseid;
472 let is_surge = self.format.as_deref() == Some("surge-json")
473 && self.schema_version
474 && self.network.present;
475 let is_opfdata = self
476 .grid
477 .as_ref()
478 .is_some_and(|grid| grid.nodes && grid.edges && grid.context)
479 && self
480 .solution
481 .as_ref()
482 .is_some_and(|solution| solution.nodes && solution.edges)
483 && self
484 .metadata
485 .as_ref()
486 .is_some_and(|metadata| metadata.objective);
487 let is_power_models = self.base_mva_camel || self.branch || self.r#gen || self.gencost;
488 let transmission = is_pandapower
489 || is_egret
490 || is_goc3
491 || is_rawx
492 || is_surge
493 || is_opfdata
494 || is_power_models;
495
496 let is_pmd = self.data_model;
497 let strong_bmopf = self.line || self.linecode || self.transformer || self.voltage_source;
498 let weak_bmopf = self.bus || self.load || self.generator || self.shunt || self.switch;
499 let distribution = is_pmd || strong_bmopf || (weak_bmopf && !transmission);
500
501 match (transmission, distribution) {
502 (true, true) => JsonClass::Case(Detection::Ambiguous),
503 (true, false) => JsonClass::Case(Detection::Known(SourceFormat::Transmission(
504 if is_pandapower {
505 TransmissionFormat::PandapowerJson
506 } else if is_egret {
507 TransmissionFormat::EgretJson
508 } else if is_rawx {
509 TransmissionFormat::PsseRawx
510 } else if is_goc3 {
511 TransmissionFormat::Goc3Json
512 } else if is_surge {
513 TransmissionFormat::SurgeJson
514 } else if is_opfdata {
515 TransmissionFormat::DeepMindOpfDataJson
516 } else {
517 TransmissionFormat::PowerModelsJson
518 },
519 ))),
520 (false, true) => {
521 JsonClass::Case(Detection::Known(SourceFormat::Distribution(if is_pmd {
522 DistributionFormat::PmdJson
523 } else {
524 DistributionFormat::BmopfJson
525 })))
526 }
527 (false, false) => JsonClass::Case(Detection::Unknown),
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::{
535 Detection, DistributionFormat, JsonClass, SourceFormat, TransmissionFormat,
536 classify_json_bytes, classify_json_text,
537 };
538
539 #[test]
540 fn classifies_powerio_ir() {
541 assert_eq!(
542 classify_json_text(r#"{"schema":"pio-ir","version":2}"#),
543 JsonClass::Module
544 );
545 for version in ["1", "3", r#""0.11.0""#, "null"] {
549 assert_eq!(
550 classify_json_text(&format!(r#"{{"schema":"pio-ir","version":{version}}}"#)),
551 JsonClass::Module,
552 "version {version}"
553 );
554 }
555 assert_eq!(
556 classify_json_text(r#"{"buses":[],"linecodes":[]}"#),
557 JsonClass::Case(Detection::Unknown)
558 );
559 assert_eq!(
560 classify_json_text(r#"{"baseMVA":100.0,"bus":{},"model":"ACP","model_kind":"opf"}"#),
561 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
562 TransmissionFormat::PowerModelsJson
563 )))
564 );
565 assert_eq!(
566 classify_json_text("not json"),
567 JsonClass::Case(Detection::Unknown)
568 );
569 }
570
571 #[test]
572 fn classifies_pmd_json() {
573 assert_eq!(
574 classify_json_text(r#"{"data_model":"ENGINEERING","bus":{}}"#),
575 JsonClass::Case(Detection::Known(SourceFormat::Distribution(
576 DistributionFormat::PmdJson
577 )))
578 );
579 }
580
581 #[test]
582 fn classifies_full_bmopf_json() {
583 assert_eq!(
584 classify_json_text(r#"{"bus":{},"linecode":{},"voltage_source":{}}"#),
585 JsonClass::Case(Detection::Known(SourceFormat::Distribution(
586 DistributionFormat::BmopfJson
587 )))
588 );
589 }
590
591 #[test]
592 fn classifies_minimal_bmopf_json() {
593 assert_eq!(
594 classify_json_text(r#"{"bus":{"a":{"terminal_names":["1"]}}}"#),
595 JsonClass::Case(Detection::Known(SourceFormat::Distribution(
596 DistributionFormat::BmopfJson
597 )))
598 );
599 }
600
601 #[test]
602 fn classifies_power_models_with_bus_and_base_mva_as_transmission() {
603 assert_eq!(
604 classify_json_text(
605 r#"{"baseMVA":100.0,"bus":{},"branch":{},"gen":{},"load":{},"switch":{}}"#
606 ),
607 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
608 TransmissionFormat::PowerModelsJson
609 )))
610 );
611 }
612
613 #[test]
614 fn a_bare_network_object_is_not_a_case_or_powerio_ir() {
615 assert_eq!(
616 classify_json_text(r#"{"base_mva":100.0,"buses":[],"branches":[]}"#),
617 JsonClass::Case(Detection::Unknown)
618 );
619 }
620
621 #[test]
622 fn every_family_is_in_the_closed_set() {
623 for class in [
624 JsonClass::Module,
625 JsonClass::Case(Detection::Ambiguous),
626 JsonClass::Case(Detection::Unknown),
627 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
628 TransmissionFormat::Matpower,
629 ))),
630 JsonClass::Case(Detection::Known(SourceFormat::Distribution(
631 DistributionFormat::Dss,
632 ))),
633 ] {
634 assert!(
635 super::JSON_CLASSES.contains(&class.family()),
636 "{class:?} answers with a family outside the closed set"
637 );
638 }
639 }
640
641 #[test]
642 fn classifies_pandapower_json() {
643 assert_eq!(
644 classify_json_text(r#"{"_class":"pandapowerNet","_object":{}}"#),
645 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
646 TransmissionFormat::PandapowerJson
647 )))
648 );
649 }
650
651 #[test]
652 fn classifies_egret_json() {
653 assert_eq!(
654 classify_json_text(r#"{"elements":{},"system":{}}"#),
655 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
656 TransmissionFormat::EgretJson
657 )))
658 );
659 }
660
661 #[test]
662 fn classifies_goc3_json() {
663 assert_eq!(
664 classify_json_text(
665 r#"{"network":{"bus":[],"simple_dispatchable_device":[]},"time_series_input":{}}"#
666 ),
667 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
668 TransmissionFormat::Goc3Json
669 )))
670 );
671 assert_eq!(
672 classify_json_text(r#"{"time_series_output":{"bus":[]}}"#),
673 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
674 TransmissionFormat::Goc3Json
675 )))
676 );
677 }
678
679 #[test]
680 fn classifies_rawx_and_normalizes_its_alias() {
681 assert_eq!(
682 classify_json_text(
683 r#"{"network":{"caseid":{"fields":["rev"],"data":[35]},"bus":{"fields":[],"data":[]}}}"#
684 ),
685 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
686 TransmissionFormat::PsseRawx
687 )))
688 );
689 for alias in ["psse-rawx", "rawx", "PSSERAWX"] {
690 assert_eq!(
691 super::parse_transmission_format(alias),
692 Some(TransmissionFormat::PsseRawx)
693 );
694 }
695 assert_eq!(TransmissionFormat::PsseRawx.name(), "psse-rawx");
696 }
697
698 #[test]
699 fn resolves_goc3_aliases() {
700 for alias in ["goc3-json", "goc3", "go3", "go-challenge-3", "c3"] {
701 assert_eq!(
702 super::parse_transmission_format(alias),
703 Some(TransmissionFormat::Goc3Json),
704 "{alias}"
705 );
706 }
707 }
708
709 #[test]
710 fn classifies_surge_json() {
711 assert_eq!(
712 classify_json_text(
713 r#"{"format":"surge-json","schema_version":"0.1.0","network":{"buses":[]}}"#
714 ),
715 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
716 TransmissionFormat::SurgeJson
717 )))
718 );
719 }
720
721 #[test]
722 fn resolves_ieee_cdf_aliases() {
723 for alias in ["ieee-cdf", "ieee_cdf", "IEEECDF", "cdf"] {
724 assert_eq!(
725 super::parse_transmission_format(alias),
726 Some(TransmissionFormat::IeeeCdf),
727 "{alias}"
728 );
729 }
730 assert_eq!(TransmissionFormat::IeeeCdf.name(), "ieee-cdf");
731 }
732
733 #[test]
734 fn resolves_surge_aliases() {
735 for alias in ["surge-json", "surge", "surgejson"] {
736 assert_eq!(
737 super::parse_transmission_format(alias),
738 Some(TransmissionFormat::SurgeJson),
739 "{alias}"
740 );
741 }
742 }
743
744 #[test]
745 fn classifies_opfdata_json() {
746 assert_eq!(
747 classify_json_text(
748 r#"{
749 "grid":{"nodes":{},"edges":{},"context":[]},
750 "solution":{"nodes":{},"edges":{}},
751 "metadata":{"objective":0.0}
752 }"#
753 ),
754 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
755 TransmissionFormat::DeepMindOpfDataJson
756 )))
757 );
758 assert_eq!(
759 classify_json_text(r#"{"grid":{},"solution":{},"metadata":{}}"#),
760 JsonClass::Case(Detection::Unknown)
761 );
762 }
763
764 #[test]
765 fn resolves_opfdata_aliases() {
766 for alias in [
767 "opfdata-json",
768 "opfdata",
769 "OPFData",
770 "deepmind-opfdata-json",
771 "deepmind-opfdata",
772 "gridopt-json",
773 "gridopt",
774 ] {
775 assert_eq!(
776 super::parse_transmission_format(alias),
777 Some(TransmissionFormat::DeepMindOpfDataJson),
778 "{alias}"
779 );
780 }
781 }
782
783 #[test]
784 fn classifies_json_with_leading_byte_order_mark() {
785 assert_eq!(
786 classify_json_text("\u{feff}{\"baseMVA\":100.0,\"bus\":{},\"branch\":{}}"),
787 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
788 TransmissionFormat::PowerModelsJson
789 )))
790 );
791 }
792
793 #[test]
794 fn classifies_json_bytes_without_lossy_utf8_replacement() {
795 assert_eq!(
796 classify_json_bytes(b"\xef\xbb\xbf{\"baseMVA\":100.0,\"bus\":{},\"branch\":{}}"),
797 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
798 TransmissionFormat::PowerModelsJson
799 )))
800 );
801 assert_eq!(
802 classify_json_bytes(b"{\"base_mva\":100.0,\"buses\":[],\"branches\":[]}"),
803 JsonClass::Case(Detection::Unknown)
804 );
805 assert_eq!(
806 classify_json_bytes(b"{\"baseMVA\":100.0,\"bus\":{}\xff}"),
807 JsonClass::Case(Detection::Unknown)
808 );
809 }
810
811 #[test]
812 fn unknown_json_has_no_signal() {
813 assert_eq!(
814 classify_json_text(r#"{"name":"case"}"#),
815 JsonClass::Case(Detection::Unknown)
816 );
817 }
818
819 #[test]
820 fn mixed_transmission_and_distribution_markers_are_ambiguous() {
821 assert_eq!(
822 classify_json_text(r#"{"baseMVA":100.0,"voltage_source":{}}"#),
823 JsonClass::Case(Detection::Ambiguous)
824 );
825 }
826
827 #[test]
833 fn a_non_string_value_at_a_string_marker_key_is_read_as_absent() {
834 assert_eq!(
835 classify_json_text(r#"{"schema":123,"baseMVA":100.0,"bus":{},"branch":{}}"#),
836 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
837 TransmissionFormat::PowerModelsJson
838 )))
839 );
840 }
841
842 #[test]
846 fn a_non_object_network_value_does_not_stop_the_surge_marker() {
847 assert_eq!(
848 classify_json_text(
849 r#"{"format":"surge-json","schema_version":"0.1.0","network":"opaque"}"#
850 ),
851 JsonClass::Case(Detection::Known(SourceFormat::Transmission(
852 TransmissionFormat::SurgeJson
853 )))
854 );
855 }
856}