use core::fmt;
use std::fmt::Display;
use std::{
collections::{BTreeMap, BTreeSet},
fs::File,
io::Read,
path::Path,
};
use lazin_error::{Context, LazinResult};
use serde::{
Deserialize, Deserializer,
de::{DeserializeSeed, MapAccess, Visitor},
};
use crate::error::{LazinError, TomlError};
pub(crate) mod module;
pub(crate) mod workspace;
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Name(String);
impl From<Name> for String {
fn from(value: Name) -> Self {
value.0
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Display for Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl From<String> for Name {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Debug)]
pub struct Config {
pub modules: BTreeSet<module::Module>,
pub workspaces: BTreeSet<workspace::Workspace>,
}
#[derive(Debug, Default)]
pub struct DuplicateKeysError {
pub workspaces: Vec<String>,
pub modules: Vec<String>,
}
impl DuplicateKeysError {
fn is_empty(&self) -> bool {
self.workspaces.is_empty() && self.modules.is_empty()
}
}
impl Config {
pub fn parse(config_dir: &Path) -> LazinResult<Self> {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum RawEntry {
Workspace(workspace::Workspace),
Module(module::Module),
}
struct RawEntrySeed<'a> {
key: &'a str,
}
impl<'de, 'a> DeserializeSeed<'de> for RawEntrySeed<'a> {
type Value = RawEntry;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let mut value = toml::Value::deserialize(deserializer)?;
match &mut value {
toml::Value::Table(table) => {
table.insert(
"name".to_string(),
toml::Value::String(self.key.to_string()),
);
let module =
module::Module::deserialize(value).map_err(serde::de::Error::custom)?;
Ok(RawEntry::Module(module))
}
toml::Value::Array(_) => {
let modules: Vec<Name> =
Vec::deserialize(value).map_err(serde::de::Error::custom)?;
Ok(RawEntry::Workspace(workspace::Workspace {
name: Name(self.key.to_string()),
modules,
}))
}
other => Err(serde::de::Error::custom(format!(
"expected `{}` to be a module table or workspace array, found {}",
self.key,
other.type_str()
))),
}
}
}
fn merge_entries(config: &mut Config, other: RawEntries) -> Result<(), DuplicateKeysError> {
let mut duplicate_keys_errors = DuplicateKeysError::default();
for (key, module) in other.0 {
match module {
RawEntry::Workspace(workspace) => {
if config.workspaces.iter().any(|w| w.name == workspace.name) {
duplicate_keys_errors.workspaces.push(key);
} else {
config.workspaces.insert(workspace);
}
}
RawEntry::Module(module) => {
if config.modules.iter().any(|w| w.name == module.name) {
duplicate_keys_errors.modules.push(key);
} else {
config.modules.insert(module);
}
}
}
}
if !duplicate_keys_errors.is_empty() {
return Err(duplicate_keys_errors);
}
Ok(())
}
struct RawEntries(BTreeMap<String, RawEntry>);
impl<'de> Deserialize<'de> for RawEntries {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct RawEntriesVisitor;
impl<'de> Visitor<'de> for RawEntriesVisitor {
type Value = BTreeMap<String, RawEntry>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a table of workspaces or modules")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut out = BTreeMap::new();
while let Some(key) = map.next_key::<String>()? {
let entry = map.next_value_seed(RawEntrySeed { key: &key })?;
out.insert(key, entry);
}
Ok(out)
}
}
deserializer
.deserialize_map(RawEntriesVisitor)
.map(RawEntries)
}
}
let config_files = crate::common::files(config_dir)?;
let mut config = Config {
modules: BTreeSet::new(),
workspaces: BTreeSet::new(),
};
let mut file_data_buffer = String::new();
for file in config_files {
file_data_buffer.clear();
File::open(file.path)
.context("Failed to open config toml file")?
.read_to_string(&mut file_data_buffer)
.context("Failed to read toml file into string")?;
match toml::from_str::<RawEntries>(&file_data_buffer) {
Ok(file_entires) => merge_entries(&mut config, file_entires)
.map_err(LazinError::from)
.context("Failed to merge config and file entries")?,
Err(e) => {
return Err(LazinError::from(TomlError {
filename: file.filename,
source: file_data_buffer.clone(),
error: e,
}))
.context("Failed to parse config files")?;
}
}
}
Ok(config)
}
}