use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::iter::FusedIterator;
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use super::error::ConfigurationError;
use super::parameters::{ParameterSpace, TaskParameters, TaskParametersIter};
use super::paths::ProjectPaths;
const CONFIGURATION_DIRECTORY: &str = "config";
const FIXED_FILE: &str = "fixed.json";
const SWEEP_FILE: &str = "sweep.json";
const PATHS_FILE: &str = "paths.json";
#[derive(Clone)]
pub struct ProjectConfig {
project_root: PathBuf,
parameters: ParameterSpace,
paths: ProjectPaths,
}
impl ProjectConfig {
pub fn load(project_root: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
let project_root = project_root.into();
let configuration_directory = project_root.join(CONFIGURATION_DIRECTORY);
let parameters = ParameterSpace::load(&configuration_directory)?;
let paths = ProjectPaths::load(&project_root)?;
Ok(Self {
project_root,
parameters,
paths,
})
}
pub fn project_root(&self) -> &Path {
&self.project_root
}
pub fn configuration_directory(&self) -> &Path {
self.parameters.configuration_directory()
}
pub fn parameters(&self) -> &ParameterSpace {
&self.parameters
}
pub fn paths(&self) -> &ProjectPaths {
&self.paths
}
pub fn task_count(&self) -> u64 {
self.parameters.task_count()
}
pub fn task_config(&self, ordinal: u64) -> Result<TaskConfig, ConfigurationError> {
Ok(TaskConfig {
parameters: self.parameters.task(ordinal)?,
paths: self.paths.clone(),
})
}
pub fn task_configs(&self) -> TaskConfigIter {
TaskConfigIter {
parameters: self.parameters.tasks(),
paths: self.paths.clone(),
}
}
pub fn task_configs_matching<V>(
&self,
key: impl Into<String>,
value: V,
) -> Result<MatchingTaskConfigIter, ConfigurationError>
where
V: Serialize,
{
let key = key.into();
if !self
.parameters
.sweep_keys()
.any(|candidate| candidate == key)
{
return Err(ConfigurationError::UnknownSweepParameter { key });
}
let value = serde_json::to_value(value).map_err(|source| {
ConfigurationError::EncodeTaskSelection {
key: key.clone(),
source,
}
})?;
Ok(MatchingTaskConfigIter {
tasks: self.task_configs(),
key: key.into_boxed_str(),
value,
})
}
pub fn unique_task_config_matching<V>(
&self,
key: impl Into<String>,
value: V,
) -> Result<TaskConfig, ConfigurationError>
where
V: Serialize,
{
let key = key.into();
let mut matches = self.task_configs_matching(key.clone(), value)?;
let task = matches
.next()
.ok_or_else(|| ConfigurationError::NoMatchingTaskConfiguration { key: key.clone() })?;
if matches.next().is_some() {
return Err(ConfigurationError::AmbiguousTaskConfiguration { key });
}
Ok(task)
}
pub fn into_parts(self) -> (ParameterSpace, ProjectPaths) {
(self.parameters, self.paths)
}
pub fn write_source_config(
&self,
destination_project_root: impl AsRef<Path>,
) -> Result<(), ConfigurationError> {
let destination_project_root = destination_project_root.as_ref();
create_destination_root(destination_project_root)?;
let destination = destination_project_root.join(CONFIGURATION_DIRECTORY);
create_configuration_directory(&destination)?;
write_source_file(
&destination.join(FIXED_FILE),
self.parameters.fixed_source_json(),
)?;
write_source_file(
&destination.join(SWEEP_FILE),
self.parameters.sweep_source_json(),
)?;
write_source_file(&destination.join(PATHS_FILE), self.paths.source_json())?;
sync_directory(&destination)?;
sync_directory(destination_project_root)
}
}
#[derive(Clone)]
pub struct TaskConfig {
parameters: TaskParameters,
paths: ProjectPaths,
}
impl TaskConfig {
pub fn task_ordinal(&self) -> u64 {
self.parameters.task_ordinal()
}
pub fn parameters(&self) -> &TaskParameters {
&self.parameters
}
pub fn paths(&self) -> &ProjectPaths {
&self.paths
}
pub fn value(&self, key: &str) -> Option<&Value> {
self.parameters.value(key)
}
pub fn require_value(&self, key: &str) -> Result<&Value, ConfigurationError> {
self.parameters.require_value(key)
}
pub fn decode_value<T>(&self, key: &str) -> Result<T, ConfigurationError>
where
T: DeserializeOwned,
{
self.parameters.decode_value(key)
}
pub fn resolve_path(&self, key: &str) -> Result<PathBuf, ConfigurationError> {
self.paths.resolve_path(key)
}
}
impl fmt::Debug for TaskConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TaskConfig")
.field("task_ordinal", &self.task_ordinal())
.field("parameters", &self.parameters.len())
.field("paths", &self.paths.len())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct TaskConfigIter {
parameters: TaskParametersIter,
paths: ProjectPaths,
}
impl Iterator for TaskConfigIter {
type Item = TaskConfig;
fn next(&mut self) -> Option<Self::Item> {
self.parameters.next().map(|parameters| TaskConfig {
parameters,
paths: self.paths.clone(),
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.parameters.size_hint()
}
}
impl FusedIterator for TaskConfigIter {}
impl fmt::Debug for TaskConfigIter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TaskConfigIter")
.field("parameters", &self.parameters)
.field("paths", &self.paths.len())
.finish_non_exhaustive()
}
}
pub struct MatchingTaskConfigIter {
tasks: TaskConfigIter,
key: Box<str>,
value: Value,
}
impl Iterator for MatchingTaskConfigIter {
type Item = TaskConfig;
fn next(&mut self) -> Option<Self::Item> {
self.tasks
.find(|task| task.value(&self.key) == Some(&self.value))
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.tasks.size_hint().1)
}
}
impl FusedIterator for MatchingTaskConfigIter {}
impl fmt::Debug for MatchingTaskConfigIter {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("MatchingTaskConfigIter")
.field("key", &self.key)
.field("tasks", &self.tasks)
.finish_non_exhaustive()
}
}
impl fmt::Debug for ProjectConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProjectConfig")
.field("project_root", &self.project_root())
.field("parameters", &self.parameters.parameter_count())
.field("tasks", &self.parameters.task_count())
.field("paths", &self.paths.len())
.finish_non_exhaustive()
}
}
fn create_destination_root(path: &Path) -> Result<(), ConfigurationError> {
match fs::create_dir_all(path) {
Ok(()) => Ok(()),
Err(source) => Err(write_error(path.to_path_buf(), source)),
}
}
fn create_configuration_directory(path: &Path) -> Result<(), ConfigurationError> {
fs::create_dir(path).map_err(|source| write_error(path.to_path_buf(), source))
}
fn write_source_file(path: &Path, source_bytes: &[u8]) -> Result<(), ConfigurationError> {
let mut output = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|source| write_error(path.to_path_buf(), source))?;
output
.write_all(source_bytes)
.map_err(|source| write_error(path.to_path_buf(), source))?;
output
.sync_all()
.map_err(|source| write_error(path.to_path_buf(), source))
}
fn sync_directory(path: &Path) -> Result<(), ConfigurationError> {
File::open(path)
.and_then(|directory| directory.sync_all())
.map_err(|source| write_error(path.to_path_buf(), source))
}
fn write_error(path: PathBuf, source: io::Error) -> ConfigurationError {
ConfigurationError::WriteConfigurationFile { path, source }
}