use anyhow::bail;
use figment::{
Figment,
providers::{Format, Json, Toml, Yaml},
};
use probe_rs::probe::WireProtocol;
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, path::PathBuf, time::Duration};
use crate::util::{
logging::LevelFilter,
rtt::{ChannelMode, DataFormat},
};
use super::rttui::tab::TabConfig;
#[derive(Debug, Clone)]
pub struct Configs {
figment: Figment,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub general: General,
pub flashing: Flashing,
pub reset: Reset,
pub probe: Probe,
pub rtt: Rtt,
pub gdb: Gdb,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Probe {
pub usb_vid: Option<String>,
pub usb_pid: Option<String>,
pub serial: Option<String>,
pub protocol: WireProtocol,
pub speed: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Flashing {
pub enabled: bool,
pub restore_unwritten_bytes: bool,
pub flash_layout_output_path: Option<String>,
pub do_chip_erase: bool,
pub disable_double_buffering: bool,
pub preverify: bool,
pub verify: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Reset {
pub enabled: bool,
pub halt_afterwards: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct General {
pub chip: Option<String>,
pub chip_descriptions: Vec<String>,
pub log_level: Option<LevelFilter>,
pub derives: Option<String>,
pub connect_under_reset: bool,
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct UpChannelConfig {
pub channel: u32,
#[serde(default)]
pub mode: Option<ChannelMode>,
#[serde(default)]
pub format: Option<DataFormat>,
#[serde(default)]
pub show_location: Option<bool>,
#[serde(default)]
pub socket: Option<SocketAddr>,
#[serde(default)]
pub show_timestamps: Option<bool>,
#[serde(default)]
pub log_format: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DownChannelConfig {
pub channel: u32,
#[serde(default)]
pub mode: Option<ChannelMode>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Rtt {
pub enabled: bool,
pub up_mode: Option<ChannelMode>,
pub up_channels: Vec<UpChannelConfig>,
pub down_channels: Vec<DownChannelConfig>,
pub tabs: Vec<TabConfig>,
#[serde(with = "duration_ms")]
pub timeout: Duration,
pub log_enabled: bool,
pub log_path: PathBuf,
}
impl Rtt {
pub fn up_channel_config(&self, channel_number: u32) -> Option<&UpChannelConfig> {
self.up_channels
.iter()
.find(|ch| ch.channel == channel_number)
}
}
mod duration_ms {
use std::time::Duration;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u128(duration.as_millis())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
u64::deserialize(deserializer).map(Duration::from_millis)
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Gdb {
pub enabled: bool,
pub gdb_connection_string: Option<String>,
}
impl Configs {
pub fn new(conf_dir: PathBuf) -> Configs {
let mut figments =
Figment::new().merge(Toml::string(include_str!("default.toml")).nested());
let config_files = [
".embed",
"Embed",
".embed.local",
"Embed.local",
];
for file in &config_files {
let mut toml_path: std::path::PathBuf = conf_dir.clone();
toml_path.push(format!("{file}.toml"));
let mut json_path = conf_dir.clone();
json_path.push(format!("{file}.json"));
let mut yaml_path = conf_dir.clone();
yaml_path.push(format!("{file}.yaml"));
let mut yml_path = conf_dir.clone();
yml_path.push(format!("{file}.yml"));
figments = Figment::from(figments)
.merge(Toml::file(toml_path).nested())
.merge(Json::file(json_path).nested())
.merge(Yaml::file(yaml_path).nested())
.merge(Yaml::file(yml_path).nested());
}
Configs { figment: figments }
}
pub fn merge(&mut self, conf_file: PathBuf) -> anyhow::Result<()> {
let original = self.figment.clone();
self.figment = match conf_file.extension().and_then(|e| e.to_str()) {
Some("toml") => original.merge(Toml::file(conf_file).nested()),
Some("json") => original.merge(Json::file(conf_file).nested()),
Some("yml" | "yaml") => original.merge(Yaml::file(conf_file).nested()),
_ => {
return Err(anyhow::anyhow!(
"File format not recognized from extension (supported: .toml, .json, .yaml / .yml)"
));
}
};
Ok(())
}
pub fn prof_names(&self) -> Vec<String> {
self.figment
.profiles()
.map(|p| String::from(p.as_str().as_str()))
.collect()
}
pub fn select_defined(self: Configs, name: &str) -> anyhow::Result<Config> {
let defined_profiles = self.prof_names();
let requested_profile_defined: bool = defined_profiles
.iter()
.any(|p| p.to_lowercase() == name.to_lowercase());
let figext: figment::error::Result<Config> = self.figment.select(name).extract();
match figext {
Err(figerr) => {
bail!(
"Failed to parse supplied configuration:\n{}",
figerr
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
Ok(config) => {
if !requested_profile_defined {
bail!(
"the requested configuration profile \"{}\" hasn't been defined (defined profiles: {})",
name,
defined_profiles.join(", ")
);
}
Ok(config)
}
}
}
#[cfg(test)]
pub fn new_with_test_data(conf_dir: PathBuf) -> Configs {
let mut cfs = Configs::new(conf_dir);
cfs.figment = cfs.figment.merge(
Toml::string(
r#"
[default]
bogusInvalidItem = "oops"
"#,
)
.nested(),
);
cfs
}
}
#[cfg(test)]
mod test {
use super::Configs;
#[test]
fn default_profile() {
let configs = Configs::new(std::env::current_dir().unwrap());
let _config = configs.select_defined("default").unwrap();
}
#[test]
fn non_existant_profile_is_error() {
let configs = Configs::new(std::env::current_dir().unwrap());
let _noprofile = configs.select_defined("nonexxistantprofle").unwrap_err();
}
#[test]
fn unknown_config_items_in_config_fail() {
let configs = Configs::new_with_test_data(std::env::current_dir().unwrap());
let _superfluous: anyhow::Error = configs.select_defined("default").unwrap_err();
}
#[test]
fn file_name_patterns() {
Configs::new(std::env::current_dir().unwrap())
.merge("nonexistent-file.yml".into())
.unwrap();
Configs::new(std::env::current_dir().unwrap())
.merge("nonexistent-file.unknown".into())
.unwrap_err();
}
}