use std::collections::BTreeMap;
use std::num::NonZeroU64;
use serde_json::{Map, Value, json};
use uuid::Uuid;
use super::export::{ComponentScope, Export, ExternalId, Node};
use crate::attributes::VerificationFate;
use crate::identity::{Key, Label};
use crate::ids::{ComponentId, ConsequenceId, DecisionId, DocumentId, DriverId, NodeId, OptionId};
use crate::names::TagName;
pub const FORMAT: &str = "loftspec-v1-sidecar/1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct V1Sidecar {
pub document: DocumentId,
pub highest: BTreeMap<String, NonZeroU64>,
pub nodes: BTreeMap<NodeId, NodeFacts>,
pub scopes: BTreeMap<ComponentId, ScopeKind>,
pub tags: BTreeMap<NodeId, Vec<TagApplication>>,
pub decisions: BTreeMap<DecisionId, DecisionFacts>,
pub former_labels: BTreeMap<Label, Uuid>,
pub tag_names: BTreeMap<TagName, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NodeFacts {
pub ordinal: i64,
pub fate_confirmed: Option<bool>,
pub proposed_fate: Option<VerificationFate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeKind {
Document,
External,
Product,
}
impl ScopeKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Document => "document",
Self::External => "external",
Self::Product => "product",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagApplication {
pub tag: TagName,
pub node_id: Uuid,
pub external_id: ExternalId,
pub ordinal: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DecisionFacts {
pub ordinal: i64,
pub options: BTreeMap<OptionId, i64>,
pub drivers: BTreeMap<DriverId, i64>,
pub consequences: BTreeMap<ConsequenceId, i64>,
}
impl NodeFacts {
pub(super) const fn of(node: &Node) -> Self {
Self {
ordinal: node.ordinal,
fate_confirmed: node.fate_confirmed,
proposed_fate: node.proposed_fate,
}
}
}
impl From<ComponentScope> for ScopeKind {
fn from(scope: ComponentScope) -> Self {
match scope {
ComponentScope::Document => Self::Document,
ComponentScope::External => Self::External,
ComponentScope::Product { .. } => Self::Product,
}
}
}
impl V1Sidecar {
pub(super) const fn new(document: DocumentId) -> Self {
Self {
document,
highest: BTreeMap::new(),
nodes: BTreeMap::new(),
scopes: BTreeMap::new(),
tags: BTreeMap::new(),
decisions: BTreeMap::new(),
former_labels: BTreeMap::new(),
tag_names: BTreeMap::new(),
}
}
pub(super) fn record_highest(&mut self, export: &Export) {
let nodes = export.nodes.iter().map(|node| &node.external_id);
let decisions = export
.architecture_decisions
.iter()
.map(|decision| &decision.identity.external_id);
let seen = nodes
.chain(decisions)
.map(|external_id| (external_id.prefix(), external_id.number()));
let former = self
.former_labels
.keys()
.map(|label| (label.prefix().as_str(), label.number()));
for (prefix, number) in seen.chain(former) {
let value = self.highest.entry(prefix.to_owned()).or_insert(number);
*value = (*value).max(number);
}
}
#[must_use]
pub fn to_json(&self) -> String {
let mut value = json!({
"formerLabels": object(self.former_labels.iter().map(|(label, node_id)| {
(label.to_string(), json!(node_id))
})),
"decisions": object(self.decisions.iter().map(|(id, facts)| {
(id.key().to_string(), decision_json(facts))
})),
"document": self.document.key().to_string(),
"format": FORMAT,
"highest": object(self.highest.iter().map(|(prefix, value)| (prefix.clone(), json!(value.get())))),
"nodes": object(self.nodes.iter().map(|(id, facts)| {
(id.key().to_string(), json!({
"fateConfirmed": facts.fate_confirmed,
"ordinal": facts.ordinal,
"proposedFate": facts.proposed_fate,
}))
})),
"scopes": object(self.scopes.iter().map(|(id, scope)| (id.key().to_string(), json!(scope.as_str())))),
"tags": object(self.tags.iter().map(|(id, applications)| {
(id.key().to_string(), applications.iter().map(|application| json!({
"externalId": application.external_id.to_string(),
"nodeId": application.node_id,
"ordinal": application.ordinal,
"tag": application.tag.as_str(),
})).collect())
})),
});
if !self.tag_names.is_empty() {
value["tagNames"] = object(
self.tag_names
.iter()
.map(|(tag, v1_name)| (tag.to_string(), json!(v1_name))),
);
}
let mut text = serde_json::to_string_pretty(&value).unwrap_or_default();
text.push('\n');
text
}
}
fn decision_json(facts: &DecisionFacts) -> Value {
let ordinals = |entries: Vec<(Key, i64)>| {
object(
entries
.into_iter()
.map(|(key, ordinal)| (key.to_string(), json!(ordinal))),
)
};
json!({
"consequences": ordinals(facts.consequences.iter().map(|(id, ordinal)| (id.key(), *ordinal)).collect()),
"drivers": ordinals(facts.drivers.iter().map(|(id, ordinal)| (id.key(), *ordinal)).collect()),
"options": ordinals(facts.options.iter().map(|(id, ordinal)| (id.key(), *ordinal)).collect()),
"ordinal": facts.ordinal,
})
}
fn object(members: impl Iterator<Item = (String, Value)>) -> Value {
let sorted: BTreeMap<String, Value> = members.collect();
Value::Object(sorted.into_iter().collect::<Map<String, Value>>())
}