use serde_json::Value;
use thiserror::Error;
use crate::{AgentProfile, Document, object, spec_digest};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileReference {
pub name: String,
pub uri: String,
pub revision: u64,
pub digest: String,
}
#[derive(Debug, Error)]
pub enum CompositionError {
#[error("inheritance cycle")]
Cycle,
#[error("inheritance depth exceeds 8")]
Depth,
#[error("{0}")]
Load(String),
#[error("{0} revision does not match pin")]
Revision(String),
#[error("{0} digest does not match pin")]
Digest(String),
#[error("composed profile has no name")]
MissingName,
}
pub fn merge_profile_values(base: &Document, child: &Document) -> Document {
let mut result = base.clone();
for (key, value) in child {
if value.is_null() {
result.remove(key);
} else if let (Some(left), Some(right)) = (
result.get(key).and_then(Value::as_object),
value.as_object(),
) {
result.insert(
key.clone(),
Value::Object(merge_profile_values(left, right)),
);
} else {
result.insert(key.clone(), value.clone());
}
}
result
}
pub fn resolve_composition<F>(
profile: &AgentProfile,
mut load: F,
) -> Result<AgentProfile, CompositionError>
where
F: FnMut(&ProfileReference) -> Result<AgentProfile, CompositionError>,
{
fn resolve<F>(
profile: &AgentProfile,
load: &mut F,
active: &mut Vec<String>,
) -> Result<AgentProfile, CompositionError>
where
F: FnMut(&ProfileReference) -> Result<AgentProfile, CompositionError>,
{
let name = object(profile.get("metadata"))
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
if active.contains(&name) {
return Err(CompositionError::Cycle);
}
if active.len() >= 8 {
return Err(CompositionError::Depth);
}
active.push(name.clone());
let mut merged = Document::new();
for raw in profile
.get("extends")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let value = object(Some(raw));
let reference = ProfileReference {
name: value
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.into(),
uri: value
.get("uri")
.and_then(Value::as_str)
.unwrap_or_default()
.into(),
revision: value.get("revision").and_then(Value::as_u64).unwrap_or(0),
digest: value
.get("digest")
.and_then(Value::as_str)
.unwrap_or_default()
.into(),
};
let base = load(&reference)?;
if reference.revision != 0
&& object(base.get("metadata"))
.get("revision")
.and_then(Value::as_u64)
!= Some(reference.revision)
{
return Err(CompositionError::Revision(reference.name));
}
if !reference.digest.is_empty()
&& spec_digest(&base).map_err(|error| CompositionError::Load(error.to_string()))?
!= reference.digest
{
return Err(CompositionError::Digest(reference.name));
}
let mut resolved = resolve(&base, load, active)?;
for key in ["extends", "state", "history"] {
resolved.remove(key);
}
if let Some(metadata) = resolved.get_mut("metadata").and_then(Value::as_object_mut) {
for key in ["name", "id", "revision"] {
metadata.remove(key);
}
}
merged = merge_profile_values(&merged, &resolved);
}
active.pop();
merged = merge_profile_values(&merged, profile);
merged.insert(
"metadata".into(),
profile.get("metadata").cloned().unwrap_or_default(),
);
for key in ["state", "history"] {
if let Some(value) = profile.get(key) {
merged.insert(key.into(), value.clone());
} else {
merged.remove(key);
}
}
merged.remove("extends");
if object(merged.get("metadata"))
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.is_empty()
{
return Err(CompositionError::MissingName);
}
Ok(merged)
}
resolve(profile, &mut load, &mut vec![])
}