use crate::utils::IdMap;
use anyhow::{bail, Context, Error};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::read_to_string,
path::{Path, PathBuf},
};
pub(crate) type WidgetConfigCollection = IdMap<Result<WidgetConfig, String>>;
#[derive(Clone, Serialize)]
#[cfg_attr(test, derive(PartialEq, Debug))]
#[serde(rename_all = "camelCase")]
pub(crate) struct WidgetConfig {
pub(crate) deskulpt_conf: DeskulptConf,
pub(crate) external_deps: HashMap<String, String>,
pub(crate) directory: PathBuf,
}
#[derive(Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub(crate) struct DeskulptConf {
pub(crate) name: String,
pub(crate) entry: String,
pub(crate) ignore: bool,
}
#[derive(Deserialize)]
struct PackageJson {
dependencies: Option<HashMap<String, String>>,
}
pub(crate) fn read_widget_config(path: &Path) -> Result<Option<WidgetConfig>, Error> {
if !path.is_absolute() || !path.is_dir() {
bail!(
"Absolute path to an existing directory is expected; got: {}",
path.display()
);
}
let deskulpt_conf_path = path.join("deskulpt.conf.json");
let deskulpt_conf_str = match read_to_string(deskulpt_conf_path) {
Ok(deskulpt_conf_str) => deskulpt_conf_str,
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => return Ok(None),
_ => return Err(e).context("Failed to read deskulpt.conf.json"),
}
},
};
let deskulpt_conf: DeskulptConf = serde_json::from_str(&deskulpt_conf_str)
.context("Failed to interpret deskulpt.conf.json")?;
if deskulpt_conf.ignore {
return Ok(None);
}
let package_json_path = path.join("package.json");
let external_deps = if package_json_path.exists() {
let package_json_str =
read_to_string(package_json_path).context("Failed to read package.json")?;
let package_json: PackageJson = serde_json::from_str(&package_json_str)
.context("Failed to interpret package.json")?;
package_json.dependencies.unwrap_or_default()
} else {
Default::default()
};
Ok(Some(WidgetConfig {
directory: path.to_path_buf(),
deskulpt_conf,
external_deps,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{assert_err_eq, ChainReason};
use path_clean::PathClean;
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::env::current_dir;
fn fixture_dir() -> PathBuf {
current_dir().unwrap().join("tests/fixtures/config").clean()
}
fn get_standard_deskulpt_conf() -> DeskulptConf {
DeskulptConf {
name: "sample".to_string(),
entry: "index.jsx".to_string(),
ignore: false,
}
}
#[rstest]
#[case::standard(
fixture_dir().join("standard"),
Some(WidgetConfig {
directory: fixture_dir().join("standard"),
deskulpt_conf: get_standard_deskulpt_conf(),
external_deps: [("express".to_string(), "^4.17.1".to_string())].into(),
}),
)]
#[case::no_package_json(
fixture_dir().join("no_package_json"),
Some(WidgetConfig {
directory: fixture_dir().join("no_package_json"),
deskulpt_conf: get_standard_deskulpt_conf(),
external_deps: HashMap::new(),
}),
)]
#[case::package_json_no_dependencies(
fixture_dir().join("package_json_no_dependencies"),
Some(WidgetConfig {
directory: fixture_dir().join("package_json_no_dependencies"),
deskulpt_conf: get_standard_deskulpt_conf(),
external_deps: HashMap::new(),
}),
)]
#[case::no_conf(fixture_dir().join("no_conf"), None)]
#[case::ignore_true(fixture_dir().join("ignore_true"), None)]
fn test_read_ok(
#[case] path: PathBuf,
#[case] expected_config: Option<WidgetConfig>,
) {
let result = read_widget_config(&path)
.expect("Expected successful read of widget configuration");
assert_eq!(result, expected_config);
}
#[rstest]
#[case::not_absolute(
"tests/fixtures/config/not_absolute",
vec![ChainReason::Exact(
"Absolute path to an existing directory is expected; got: \
tests/fixtures/config/not_absolute".to_string()
)],
)]
#[case::not_dir(
fixture_dir().join("not_a_directory"),
vec![ChainReason::Exact(format!(
"Absolute path to an existing directory is expected; got: {}",
fixture_dir().join("not_a_directory").display(),
))],
)]
#[case::non_existent(
fixture_dir().join("non_existent"),
vec![ChainReason::Exact(format!(
"Absolute path to an existing directory is expected; got: {}",
fixture_dir().join("non_existent").display(),
))],
)]
#[case::conf_not_readable(
fixture_dir().join("conf_not_readable"),
vec![
ChainReason::Exact("Failed to read deskulpt.conf.json".to_string()),
ChainReason::IOError,
],
)]
#[case::conf_missing_field(
fixture_dir().join("conf_missing_field"),
vec![
ChainReason::Exact("Failed to interpret deskulpt.conf.json".to_string()),
ChainReason::SerdeError,
],
)]
#[case::package_json_not_readable(
fixture_dir().join("package_json_not_readable"),
vec![
ChainReason::Exact("Failed to read package.json".to_string()),
ChainReason::IOError,
],
)]
fn test_read_error(
#[case] path: PathBuf,
#[case] expected_error: Vec<ChainReason>,
) {
let error = read_widget_config(&path)
.expect_err("Expected an error reading widget configuration");
assert_err_eq(error, expected_error);
}
}