use std::collections::BTreeMap;
use std::fmt;
use super::PipelineContract;
fn path_for_id(id: &str, named: ObjectPath, indexed: String) -> ObjectPath {
if id.trim().is_empty() {
ObjectPath::new(indexed)
} else {
named
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjectId(String);
impl ObjectId {
pub fn new(raw: impl Into<String>) -> Self {
Self(raw.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_present(&self) -> bool {
!self.0.trim().is_empty()
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for ObjectId {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for ObjectId {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PipelineIdentity {
pub id: ObjectId,
pub version: ObjectId,
pub dpcs_version: ObjectId,
pub name: Option<String>,
}
impl PipelineIdentity {
pub fn is_complete(&self) -> bool {
self.id.is_present() && self.version.is_present() && self.dpcs_version.is_present()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ObjectKind {
Pipeline,
InterfaceInput,
InterfaceOutput,
Step,
ContractReference,
QualityGate,
FailureSemantics,
}
impl fmt::Display for ObjectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pipeline => write!(f, "pipeline"),
Self::InterfaceInput => write!(f, "interfaceInput"),
Self::InterfaceOutput => write!(f, "interfaceOutput"),
Self::Step => write!(f, "step"),
Self::ContractReference => write!(f, "contractReference"),
Self::QualityGate => write!(f, "qualityGate"),
Self::FailureSemantics => write!(f, "failureSemantics"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjectPath(String);
impl ObjectPath {
pub fn new(raw: impl Into<String>) -> Self {
Self(raw.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn pipeline() -> Self {
Self("pipeline".to_owned())
}
pub fn interface_input(id: &str) -> Self {
Self(format!("interface.inputs.{id}"))
}
pub fn interface_output(id: &str) -> Self {
Self(format!("interface.outputs.{id}"))
}
pub fn step(id: &str) -> Self {
Self(format!("steps.{id}"))
}
pub fn contract_reference(id: &str) -> Self {
Self(format!("contractReferences.{id}"))
}
pub fn quality_gate(id: &str) -> Self {
Self(format!("qualityGates.{id}"))
}
pub fn failure_semantics(id: &str) -> Self {
Self(format!("failureSemantics.{id}"))
}
}
impl fmt::Display for ObjectPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentityEntry {
pub kind: ObjectKind,
pub id: ObjectId,
pub path: ObjectPath,
}
#[derive(Debug, Clone, Default)]
pub struct IdentityCatalog {
entries: Vec<IdentityEntry>,
by_path: BTreeMap<String, usize>,
by_kind_and_id: BTreeMap<(ObjectKind, String), usize>,
}
impl IdentityCatalog {
pub fn from_contract(contract: &PipelineContract) -> Self {
let mut catalog = Self::default();
catalog.insert(
ObjectKind::Pipeline,
ObjectId::new(contract.id.clone()),
ObjectPath::pipeline(),
);
for (index, port) in contract.interface.inputs.iter().enumerate() {
catalog.insert(
ObjectKind::InterfaceInput,
ObjectId::new(port.id.clone()),
path_for_id(
port.id.as_str(),
ObjectPath::interface_input(&port.id),
format!("interface.inputs[{index}]"),
),
);
}
for (index, port) in contract.interface.outputs.iter().enumerate() {
catalog.insert(
ObjectKind::InterfaceOutput,
ObjectId::new(port.id.clone()),
path_for_id(
port.id.as_str(),
ObjectPath::interface_output(&port.id),
format!("interface.outputs[{index}]"),
),
);
}
for (index, step) in contract.steps.iter().enumerate() {
catalog.insert(
ObjectKind::Step,
ObjectId::new(step.id.clone()),
path_for_id(
step.id.as_str(),
ObjectPath::step(&step.id),
format!("steps[{index}]"),
),
);
}
for (index, reference) in contract.contract_references.iter().enumerate() {
catalog.insert(
ObjectKind::ContractReference,
ObjectId::new(reference.id.clone()),
path_for_id(
reference.id.as_str(),
ObjectPath::contract_reference(&reference.id),
format!("contractReferences[{index}]"),
),
);
}
for (index, gate) in contract.quality_gates.iter().enumerate() {
catalog.insert(
ObjectKind::QualityGate,
ObjectId::new(gate.id.clone()),
path_for_id(
gate.id.as_str(),
ObjectPath::quality_gate(&gate.id),
format!("qualityGates[{index}]"),
),
);
}
for (index, failure) in contract.failure_semantics.iter().enumerate() {
catalog.insert(
ObjectKind::FailureSemantics,
ObjectId::new(failure.id.clone()),
path_for_id(
failure.id.as_str(),
ObjectPath::failure_semantics(&failure.id),
format!("failureSemantics[{index}]"),
),
);
}
catalog
}
pub fn paths_for_kind_and_id(&self, kind: ObjectKind, id: &str) -> Vec<&ObjectPath> {
let needle = id.trim();
self.entries
.iter()
.filter(|entry| entry.kind == kind && entry.id.as_str().trim() == needle)
.map(|entry| &entry.path)
.collect()
}
fn insert(&mut self, kind: ObjectKind, id: ObjectId, path: ObjectPath) {
let index = self.entries.len();
self.by_path.insert(path.as_str().to_owned(), index);
self.by_kind_and_id
.insert((kind, id.as_str().to_owned()), index);
self.entries.push(IdentityEntry { kind, id, path });
}
pub fn entries(&self) -> &[IdentityEntry] {
&self.entries
}
pub fn get_by_path(&self, path: &str) -> Option<&IdentityEntry> {
self.by_path.get(path).map(|index| &self.entries[*index])
}
pub fn get_by_kind_and_id(&self, kind: ObjectKind, id: &str) -> Option<&IdentityEntry> {
self.by_kind_and_id
.get(&(kind, id.to_owned()))
.map(|index| &self.entries[*index])
}
pub fn duplicate_ids_by_kind(&self) -> BTreeMap<ObjectKind, Vec<ObjectId>> {
let mut counts: BTreeMap<(ObjectKind, String), usize> = BTreeMap::new();
for entry in &self.entries {
if entry.kind == ObjectKind::Pipeline {
continue;
}
*counts
.entry((entry.kind, entry.id.as_str().trim().to_owned()))
.or_default() += 1;
}
let mut duplicates = BTreeMap::new();
for ((kind, id), count) in counts {
if count > 1 {
duplicates
.entry(kind)
.or_insert_with(Vec::new)
.push(ObjectId::new(id));
}
}
duplicates
}
pub fn entries_with_missing_ids(&self) -> Vec<&IdentityEntry> {
self.entries
.iter()
.filter(|entry| entry.kind != ObjectKind::Pipeline && !entry.id.is_present())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_yaml;
#[test]
fn builds_catalog_from_contract() {
let contract = parse_yaml(
r#"
dpcsVersion: "1.0.0"
id: "test.pipeline"
version: "0.1.0"
interface:
inputs:
- id: "in"
name: "In"
contractRef: "ref"
purpose: "input"
outputs: []
steps:
- id: "step_a"
type: "dtcs:transform"
graph:
edges: []
"#,
)
.unwrap();
let catalog = IdentityCatalog::from_contract(&contract);
assert!(catalog.get_by_path("pipeline").is_some());
assert!(catalog
.get_by_kind_and_id(ObjectKind::Step, "step_a")
.is_some());
assert!(catalog.get_by_path("interface.inputs.in").is_some());
}
}