pub mod merge_utils;
mod ociom_yaml;
pub(crate) mod section_merger;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_colorspaces;
#[cfg(test)]
mod tests_merges;
#[cfg(test)]
mod tests_named_transforms;
#[cfg(test)]
mod tests_sections;
use crate::config::utils::{split, trim};
use crate::config::{ColorSpace, Config};
use crate::error::{Error, Result};
use crate::path_utils;
use section_merger::{
ColorspacesMerger, DisplayViewMerger, FileRulesMerger, GeneralMerger, LooksMerger,
MergeHandlerOptions, NamedTransformsMerger, RolesMerger, SectionMerger, ViewTransformsMerger,
};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MergeStrategies {
PreferInput,
PreferBase,
InputOnly,
BaseOnly,
Remove,
Unspecified,
}
impl MergeStrategies {
pub fn as_str(&self) -> &'static str {
match self {
MergeStrategies::PreferInput => "PreferInput",
MergeStrategies::PreferBase => "PreferBase",
MergeStrategies::InputOnly => "InputOnly",
MergeStrategies::BaseOnly => "BaseOnly",
MergeStrategies::Remove => "Remove",
MergeStrategies::Unspecified => "Unspecified",
}
}
pub fn from_str_lossy(s: &str) -> MergeStrategies {
match s {
"PreferInput" => MergeStrategies::PreferInput,
"PreferBase" => MergeStrategies::PreferBase,
"InputOnly" => MergeStrategies::InputOnly,
"BaseOnly" => MergeStrategies::BaseOnly,
"Remove" => MergeStrategies::Remove,
_ => MergeStrategies::Unspecified,
}
}
}
impl fmt::Display for MergeStrategies {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ConfigMergingParameters {
base_config: String,
input_config: String,
output_name: String,
name: String,
description: String,
override_cfg: Config,
input_family_prefix: String,
base_family_prefix: String,
input_first: bool,
error_on_conflict: bool,
avoid_duplicates: bool,
adjust_input_reference_space: bool,
default_strategy: MergeStrategies,
roles: MergeStrategies,
file_rules: MergeStrategies,
display_views: MergeStrategies,
view_transforms: MergeStrategies,
looks: MergeStrategies,
colorspaces: MergeStrategies,
named_transforms: MergeStrategies,
}
impl Default for ConfigMergingParameters {
fn default() -> Self {
Self::new()
}
}
macro_rules! strategy_accessors {
($($(#[$doc:meta])* $field:ident, $setter:ident;)*) => {
$(
$(#[$doc])*
///
/// The default strategy is returned if the strategy is unspecified.
pub fn $field(&self) -> MergeStrategies {
if self.$field == MergeStrategies::Unspecified {
return self.default_strategy;
}
self.$field
}
$(#[$doc])*
pub fn $setter(&mut self, strategy: MergeStrategies) {
self.$field = strategy;
}
)*
};
}
impl ConfigMergingParameters {
pub fn new() -> Self {
let mut override_cfg = Config::new();
override_cfg.clear_environment_vars();
Self {
base_config: String::new(),
input_config: String::new(),
output_name: "merged".to_string(),
name: String::new(),
description: String::new(),
override_cfg,
input_family_prefix: String::new(),
base_family_prefix: String::new(),
input_first: true,
error_on_conflict: false,
avoid_duplicates: true,
adjust_input_reference_space: true,
default_strategy: MergeStrategies::PreferInput,
roles: MergeStrategies::Unspecified,
file_rules: MergeStrategies::Unspecified,
display_views: MergeStrategies::Unspecified,
view_transforms: MergeStrategies::Unspecified,
looks: MergeStrategies::Unspecified,
colorspaces: MergeStrategies::Unspecified,
named_transforms: MergeStrategies::Unspecified,
}
}
pub fn create_editable_copy(&self) -> Self {
self.clone()
}
pub fn set_base_config_name(&mut self, base_config: &str) {
self.base_config = base_config.to_string();
}
pub fn base_config_name(&self) -> &str {
&self.base_config
}
pub fn set_input_config_name(&mut self, input_config: &str) {
self.input_config = input_config.to_string();
}
pub fn input_config_name(&self) -> &str {
&self.input_config
}
pub fn set_output_name(&mut self, output_name: &str) {
self.output_name = output_name.to_string();
}
pub fn output_name(&self) -> &str {
&self.output_name
}
pub fn set_default_strategy(&mut self, strategy: MergeStrategies) {
self.default_strategy = strategy;
}
pub fn default_strategy(&self) -> MergeStrategies {
self.default_strategy
}
pub fn set_input_family_prefix(&mut self, prefix: &str) {
self.input_family_prefix = prefix.to_string();
}
pub fn input_family_prefix(&self) -> &str {
&self.input_family_prefix
}
pub fn set_base_family_prefix(&mut self, prefix: &str) {
self.base_family_prefix = prefix.to_string();
}
pub fn base_family_prefix(&self) -> &str {
&self.base_family_prefix
}
pub fn set_input_first(&mut self, enabled: bool) {
self.input_first = enabled;
}
pub fn is_input_first(&self) -> bool {
self.input_first
}
pub fn set_error_on_conflict(&mut self, enabled: bool) {
self.error_on_conflict = enabled;
}
pub fn is_error_on_conflict(&self) -> bool {
self.error_on_conflict
}
pub fn set_avoid_duplicates(&mut self, enabled: bool) {
self.avoid_duplicates = enabled;
}
pub fn is_avoid_duplicates(&self) -> bool {
self.avoid_duplicates
}
pub fn set_adjust_input_reference_space(&mut self, enabled: bool) {
self.adjust_input_reference_space = enabled;
}
pub fn is_adjust_input_reference_space(&self) -> bool {
self.adjust_input_reference_space
}
pub fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_description(&mut self, desc: &str) {
self.description = desc.to_string();
}
pub fn description(&self) -> &str {
&self.description
}
pub fn add_environment_var(&mut self, name: &str, default_value: &str) {
self.override_cfg
.add_environment_var(name, Some(default_value));
}
pub fn num_environment_vars(&self) -> usize {
self.override_cfg.num_environment_vars()
}
pub fn environment_var(&self, index: usize) -> &str {
self.override_cfg.environment_var_name_by_index(index)
}
pub fn environment_var_value(&self, index: usize) -> &str {
let name = self.override_cfg.environment_var_name_by_index(index);
self.override_cfg.environment_var_default(name)
}
pub fn set_search_path(&mut self, path: &str) {
self.override_cfg.set_search_path(path);
}
pub fn add_search_path(&mut self, path: &str) {
self.override_cfg.add_search_path(path);
}
pub fn search_path(&self) -> String {
self.override_cfg.search_path()
}
pub fn set_active_displays(&mut self, displays: &str) -> Result<()> {
self.override_cfg.set_active_displays(displays)
}
pub fn active_displays(&self) -> String {
self.override_cfg.active_displays()
}
pub fn set_active_views(&mut self, views: &str) -> Result<()> {
self.override_cfg.set_active_views(views)
}
pub fn active_views(&self) -> String {
self.override_cfg.active_views()
}
pub fn set_inactive_color_spaces(&mut self, colorspaces: &str) {
self.override_cfg.set_inactive_color_spaces(colorspaces);
}
pub fn inactive_color_spaces(&self) -> &str {
self.override_cfg.inactive_color_spaces()
}
strategy_accessors! {
roles, set_roles;
file_rules, set_file_rules;
display_views, set_display_views;
view_transforms, set_view_transforms;
looks, set_looks;
colorspaces, set_colorspaces;
named_transforms, set_named_transforms;
}
}
impl fmt::Display for ConfigMergingParameters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts: Vec<String> = Vec::new();
let mut s = |label: &str, value: &str| {
if !value.is_empty() {
parts.push(format!("{label}: {value}"));
}
};
s("base", self.base_config_name());
s("input", self.input_config_name());
s("output_name", self.output_name());
s("input_family_prefix", self.input_family_prefix());
s("base_family_prefix", self.base_family_prefix());
let b = |v: bool| if v { "true" } else { "false" };
parts.push(format!("input_first: {}", b(self.is_input_first())));
parts.push(format!(
"error_on_conflict: {}",
b(self.is_error_on_conflict())
));
parts.push(format!("default_strategy: {}", self.default_strategy()));
parts.push(format!(
"avoid_duplicates: {}",
b(self.is_avoid_duplicates())
));
parts.push(format!(
"adjust_input_reference_space: {}",
b(self.is_adjust_input_reference_space())
));
let mut s = |label: &str, value: &str| {
if !value.is_empty() {
parts.push(format!("{label}: {value}"));
}
};
s("name", self.name());
s("description", self.description());
s("search_path", &self.search_path());
s("active_displays", &self.active_displays());
s("active_views", &self.active_views());
s("inactive_colorspaces", self.inactive_color_spaces());
parts.push(format!("roles: {}", self.roles()));
parts.push(format!("file_rules: {}", self.file_rules()));
parts.push(format!("display-views: {}", self.display_views()));
parts.push(format!("view_transforms: {}", self.view_transforms()));
parts.push(format!("looks: {}", self.looks()));
parts.push(format!("colorspaces: {}", self.colorspaces()));
parts.push(format!("named_transforms: {}", self.named_transforms()));
let n = self.num_environment_vars();
if n > 0 {
let vars: Vec<String> = (0..n)
.map(|i| {
let v = self.environment_var_value(i);
if v.is_empty() {
self.environment_var(i).to_string()
} else {
format!("{}={}", self.environment_var(i), v)
}
})
.collect();
parts.push(format!("environment: [{}]", vars.join(", ")));
}
write!(f, "<{}>", parts.join(", "))
}
}
#[derive(Debug, Clone)]
pub struct ConfigMerger {
search_paths: Vec<String>,
working_dir: String,
major_version: u32,
minor_version: u32,
merge_params: Vec<ConfigMergingParameters>,
merged_configs: Vec<Config>,
}
impl Default for ConfigMerger {
fn default() -> Self {
Self::new()
}
}
impl ConfigMerger {
pub fn new() -> Self {
Self {
search_paths: Vec::new(),
working_dir: String::new(),
major_version: 1,
minor_version: 0,
merge_params: Vec::new(),
merged_configs: Vec::new(),
}
}
pub fn create_from_file(filepath: &str) -> Result<ConfigMerger> {
if filepath.is_empty() {
return Err(Error::missing_file(
"The merge options filepath is missing.",
));
}
let data = std::fs::read(filepath)
.map_err(|_| Error::msg(format!("Error could not read '{filepath}' merge options.")))?;
let text = String::from_utf8_lossy(&data);
ociom_yaml::read(&text, filepath)
}
pub fn create_editable_copy(&self) -> Self {
self.clone()
}
pub fn set_search_path(&mut self, path: &str) {
self.search_paths = split(path, ':');
}
pub fn add_search_path(&mut self, path: &str) {
if !path.is_empty() {
self.search_paths.push(path.to_string());
}
}
pub fn num_search_paths(&self) -> usize {
self.search_paths.len()
}
pub fn search_path(&self, index: usize) -> &str {
self.search_paths
.get(index)
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn set_working_dir(&mut self, dirname: &str) {
self.working_dir = dirname.to_string();
}
pub fn working_dir(&self) -> &str {
&self.working_dir
}
pub fn params(&self, index: usize) -> Option<&ConfigMergingParameters> {
self.merge_params.get(index)
}
pub fn params_mut(&mut self, index: usize) -> Option<&mut ConfigMergingParameters> {
self.merge_params.get_mut(index)
}
pub fn num_config_merging_parameters(&self) -> usize {
self.merge_params.len()
}
pub fn add_params(&mut self, params: ConfigMergingParameters) {
self.merge_params.push(params);
}
fn load_config(&self, value: &str) -> Option<Config> {
let mut searchpaths: Vec<String> = Vec::new();
if self.search_paths.is_empty() {
searchpaths.push(self.working_dir.clone());
}
for path in &self.search_paths {
let mut dirname = trim(path).trim_end_matches('/').to_string();
if !path_utils::is_absolute(&dirname) {
dirname = path_utils::join(&self.working_dir, &dirname);
}
searchpaths.push(path_utils::normpath(&dirname));
}
for sp in &searchpaths {
let full = path_utils::normpath(&path_utils::join(sp, value));
if let Ok(c) = Config::create_from_file(&full) {
return Some(c);
}
}
if let Ok(c) = Config::create_from_builtin_config(value) {
return Some(c);
}
for (i, p) in self.merge_params.iter().enumerate() {
if p.output_name().eq_ignore_ascii_case(value) {
return self.merged_configs.get(i).cloned();
}
}
None
}
pub fn merge_configs(&self) -> Result<ConfigMerger> {
let mut merger = self.create_editable_copy();
for params in &self.merge_params {
let base = merger.load_config(params.base_config_name());
let input = merger.load_config(params.input_config_name());
match (base, input) {
(Some(base), Some(input)) => {
let merged = merge_configs(params, &base, &input)?;
merger.merged_configs.push(merged);
}
_ => return Err(Error::msg("Could not load the base or the input config")),
}
}
Ok(merger)
}
pub fn merged_config(&self) -> Option<&Config> {
self.merged_configs.last()
}
pub fn merged_config_at(&self, index: usize) -> Option<&Config> {
self.merged_configs.get(index)
}
pub fn num_merged_configs(&self) -> usize {
self.merged_configs.len()
}
pub fn serialize(&self) -> Result<String> {
ociom_yaml::write(self)
.map_err(|e| Error::msg(format!("Error building YAML: {}", e.message())))
}
pub fn set_version(&mut self, major: u32, minor: u32) {
self.major_version = major;
self.minor_version = minor;
}
pub fn major_version(&self) -> u32 {
self.major_version
}
pub fn minor_version(&self) -> u32 {
self.minor_version
}
}
impl fmt::Display for ConfigMerger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.serialize() {
Ok(s) => f.write_str(&s),
Err(_) => Err(fmt::Error),
}
}
}
fn run_all_mergers(options: &mut MergeHandlerOptions) -> Result<()> {
GeneralMerger::new(options).merge()?;
RolesMerger::new(options).merge()?;
FileRulesMerger::new(options).merge()?;
DisplayViewMerger::new(options).merge()?;
ViewTransformsMerger::new(options)?.merge()?;
LooksMerger::new(options).merge()?;
ColorspacesMerger::new(options)?.merge()?;
NamedTransformsMerger::new(options).merge()?;
Ok(())
}
pub fn merge_configs(
params: &ConfigMergingParameters,
base_config: &Config,
input_config: &Config,
) -> Result<Config> {
let mut merged = base_config.create_editable_copy();
{
let mut options = MergeHandlerOptions {
base_config,
input_config,
params,
merged_config: &mut merged,
};
run_all_mergers(&mut options)?;
}
Ok(merged)
}
pub fn merge_color_space(
params: &ConfigMergingParameters,
base_config: &Config,
colorspace: &ColorSpace,
) -> Result<Config> {
let mut input_config = Config::new();
input_config.add_color_space(colorspace)?;
let mut merged = base_config.create_editable_copy();
let mut e_params = params.create_editable_copy();
e_params.set_adjust_input_reference_space(false);
{
let mut options = MergeHandlerOptions {
base_config,
input_config: &input_config,
params: &e_params,
merged_config: &mut merged,
};
ColorspacesMerger::new(&mut options)?.merge()?;
}
Ok(merged)
}