use crate::model::{DistNetwork, DistSourceFormat};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Conversion {
pub text: String,
pub warnings: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DistTargetFormat {
Dss,
BmopfJson,
PmdJson,
}
pub fn dist_target_from_name(name: &str) -> Option<DistTargetFormat> {
match name.to_ascii_lowercase().as_str() {
"dss" | "opendss" => Some(DistTargetFormat::Dss),
"bmopf" | "bmopf-json" | "bmopf_json" => Some(DistTargetFormat::BmopfJson),
"pmd" | "pmd-json" | "pmd_json" | "engineering" => Some(DistTargetFormat::PmdJson),
_ => None,
}
}
impl std::str::FromStr for DistTargetFormat {
type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Self> {
dist_target_from_name(s).ok_or_else(|| crate::Error::UnknownFormat(s.to_string()))
}
}
impl DistTargetFormat {
pub fn name(self) -> &'static str {
match self {
DistTargetFormat::Dss => "dss",
DistTargetFormat::PmdJson => "pmd-json",
DistTargetFormat::BmopfJson => "bmopf-json",
}
}
}
fn read(path: &std::path::Path) -> crate::Result<String> {
std::fs::read_to_string(path).map_err(|source| crate::Error::Io {
path: path.display().to_string(),
source,
})
}
fn is_pmd_json(text: &str) -> bool {
#[derive(serde::Deserialize)]
struct Shape {
data_model: Option<serde::de::IgnoredAny>,
}
serde_json::from_str::<Shape>(text).is_ok_and(|s| s.data_model.is_some())
}
pub fn parse_str(text: &str, format: &str) -> crate::Result<DistNetwork> {
match format.parse::<DistTargetFormat>()? {
DistTargetFormat::Dss => Ok(crate::dss::parse_dss_str(text)),
DistTargetFormat::BmopfJson => crate::bmopf::parse_bmopf_str(text),
DistTargetFormat::PmdJson => crate::pmd::parse_pmd_str(text),
}
}
pub fn parse_file(
path: impl AsRef<std::path::Path>,
from: Option<&str>,
) -> crate::Result<DistNetwork> {
let path = path.as_ref();
let format = if let Some(from) = from {
from.parse::<DistTargetFormat>()?
} else {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
match ext.as_str() {
"dss" => DistTargetFormat::Dss,
"json" => {
let text = read(path)?;
return if is_pmd_json(&text) {
crate::pmd::parse_pmd_str(&text)
} else {
crate::bmopf::parse_bmopf_str(&text)
};
}
other => return Err(crate::Error::UnknownFormat(other.to_string())),
}
};
match format {
DistTargetFormat::Dss => crate::dss::parse_dss_file(path),
DistTargetFormat::BmopfJson => crate::bmopf::parse_bmopf_str(&read(path)?),
DistTargetFormat::PmdJson => crate::pmd::parse_pmd_str(&read(path)?),
}
}
fn convert(net: &DistNetwork, target: DistTargetFormat) -> Conversion {
let conv = net.to_format(target);
let mut warnings = net.warnings.clone();
warnings.extend(conv.warnings);
Conversion {
text: conv.text,
warnings,
}
}
pub fn convert_str(text: &str, to: DistTargetFormat, format: &str) -> crate::Result<Conversion> {
Ok(convert(&parse_str(text, format)?, to))
}
pub fn convert_file(
path: impl AsRef<std::path::Path>,
to: DistTargetFormat,
from: Option<&str>,
) -> crate::Result<Conversion> {
Ok(convert(&parse_file(path, from)?, to))
}
impl DistTargetFormat {
fn matches(self, source: DistSourceFormat) -> bool {
matches!(
(self, source),
(DistTargetFormat::Dss, DistSourceFormat::Dss)
| (DistTargetFormat::BmopfJson, DistSourceFormat::BmopfJson)
| (DistTargetFormat::PmdJson, DistSourceFormat::PmdJson)
)
}
}
impl DistNetwork {
pub fn to_format(&self, format: DistTargetFormat) -> Conversion {
if let (Some(source), Some(source_format)) = (&self.source, self.source_format) {
if format.matches(source_format) {
return Conversion {
text: source.as_ref().clone(),
warnings: Vec::new(),
};
}
}
match format {
DistTargetFormat::Dss => crate::dss::write_dss(self),
DistTargetFormat::BmopfJson => crate::bmopf::write_bmopf_json(self),
DistTargetFormat::PmdJson => crate::pmd::write_pmd_json(self),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sniff_requires_top_level_data_model() {
assert!(is_pmd_json(r#"{"data_model": "ENGINEERING"}"#));
assert!(!is_pmd_json(r#"{"bus": {"data_model": {}}}"#));
assert!(!is_pmd_json(r#"{"name": "data_model"}"#));
assert!(!is_pmd_json("{not json"));
}
#[test]
fn unknown_format_names_fail_before_any_work() {
assert!(matches!(
parse_str("", "matpower"),
Err(crate::Error::UnknownFormat(_))
));
assert!(matches!(
"matpower".parse::<DistTargetFormat>(),
Err(crate::Error::UnknownFormat(_))
));
assert!(matches!(
parse_file("missing.dss", Some("matpower")),
Err(crate::Error::UnknownFormat(_))
));
}
#[test]
fn one_shot_convert_carries_parse_warnings() {
let dss = "clear\nnew circuit.w basekv=12.47 bus1=src\n\
new line.l1 bus1=src bus2=b2 length=1 units=furlong\n";
let conv = convert_str(dss, DistTargetFormat::BmopfJson, "dss").unwrap();
assert!(
conv.warnings.iter().any(|w| w.contains("furlong")),
"parse warnings must surface through the one-shot converter: {:?}",
conv.warnings
);
}
}