use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::{ChartSpec, validate_number, validate_text};
use crate::error::{ChartError, ChartWarning};
const MAX_COMPONENTS: usize = 40;
const MAX_DEPENDENCIES: usize = 80;
pub(crate) const VALUEMAP_STAGES: [(f64, ValuemapStage); 4] = [
(0.0, ValuemapStage::Genesis),
(0.17, ValuemapStage::Custom),
(0.4, ValuemapStage::Product),
(0.7, ValuemapStage::Commodity),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValuemapStage {
Genesis,
Custom,
Product,
Commodity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValuemapSpec {
pub components: Vec<ValuemapComponentSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<ValuemapDependencySpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValuemapComponentSpec {
pub id: String,
pub label: String,
pub visibility: f64,
pub evolution: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub moves_to: Option<ValuemapMoveSpec>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub inertia: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValuemapMoveSpec {
pub evolution: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValuemapDependencySpec {
pub from: String,
pub to: String,
}
impl ValuemapStage {
pub(crate) fn of(evolution: f64) -> Self {
VALUEMAP_STAGES
.iter()
.rev()
.find(|(start, _)| evolution >= *start)
.map_or(Self::Genesis, |(_, stage)| *stage)
}
}
impl ValuemapSpec {
pub(crate) fn index_of(&self, id: &str) -> Option<usize> {
self.components
.iter()
.position(|component| component.id == id)
}
}
impl ChartSpec {
#[allow(clippy::too_many_lines)]
pub(super) fn validate_valuemap(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.reject_chart_fields("a value chain map is drawn from its components")?;
let Some(map) = &self.valuemap else {
return Err(ChartError::new(
"missing_valuemap",
"/valuemap",
"a value chain map requires a valuemap block",
));
};
if map.components.is_empty() {
return Err(ChartError::new(
"empty_data",
"/valuemap/components",
"provide at least one component",
));
}
if map.components.len() > MAX_COMPONENTS {
return Err(ChartError::new(
"too_many_nodes",
"/valuemap/components",
format!(
"at most {MAX_COMPONENTS} components are supported; map one part of the chain"
),
));
}
if map.dependencies.len() > MAX_DEPENDENCIES {
return Err(ChartError::new(
"too_many_edges",
"/valuemap/dependencies",
format!("at most {MAX_DEPENDENCIES} dependencies are supported"),
));
}
let mut ids = BTreeSet::new();
let mut labels = BTreeSet::new();
for (index, component) in map.components.iter().enumerate() {
let path = format!("/valuemap/components/{index}");
validate_text(&component.id, &format!("{path}/id"), 60)?;
if !ids.insert(component.id.as_str()) {
return Err(ChartError::new(
"duplicate_id",
format!("{path}/id"),
"component ids must be unique",
));
}
validate_text(&component.label, &format!("{path}/label"), 100)?;
if !labels.insert(component.label.as_str()) {
return Err(ChartError::new(
"duplicate_label",
format!("{path}/label"),
"component labels must be unique",
));
}
let mut positions = vec![
("visibility", component.visibility),
("evolution", component.evolution),
];
if let Some(target) = &component.moves_to {
positions.push(("movesTo/evolution", target.evolution));
if let Some(visibility) = target.visibility {
positions.push(("movesTo/visibility", visibility));
}
}
for (name, value) in positions {
validate_number(value, &format!("{path}/{name}"))?;
if !(0.0..=1.0).contains(&value) {
return Err(ChartError::new(
"invalid_position",
format!("{path}/{name}"),
"positions are between 0 and 1",
));
}
}
if let Some(target) = &component.moves_to
&& target.evolution <= component.evolution
{
return Err(ChartError::new(
"invalid_position",
format!("{path}/movesTo"),
"a movement goes to a place with more evolution than the component's own",
));
}
}
let mut pairs = BTreeSet::new();
for (index, dependency) in map.dependencies.iter().enumerate() {
let path = format!("/valuemap/dependencies/{index}");
for (name, id) in [("from", &dependency.from), ("to", &dependency.to)] {
if !ids.contains(id.as_str()) {
return Err(ChartError::new(
"unknown_node",
format!("{path}/{name}"),
"a dependency joins components by their ids",
));
}
}
if dependency.from == dependency.to {
return Err(ChartError::new(
"self_loop",
&path,
"a component does not depend on itself",
));
}
if !pairs.insert((dependency.from.as_str(), dependency.to.as_str())) {
return Err(ChartError::new(
"duplicate_edge",
&path,
"a dependency is given once",
));
}
}
Ok(Vec::new())
}
}