open_agent_profile/
composition.rs1use serde_json::Value;
2use thiserror::Error;
3
4use crate::{AgentProfile, Document, object, spec_digest};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ProfileReference {
9 pub name: String,
11 pub uri: String,
13 pub revision: u64,
15 pub digest: String,
17}
18
19#[derive(Debug, Error)]
21pub enum CompositionError {
22 #[error("inheritance cycle")]
24 Cycle,
25 #[error("inheritance depth exceeds 8")]
27 Depth,
28 #[error("{0}")]
30 Load(String),
31 #[error("{0} revision does not match pin")]
33 Revision(String),
34 #[error("{0} digest does not match pin")]
36 Digest(String),
37 #[error("composed profile has no name")]
39 MissingName,
40}
41
42pub fn merge_profile_values(base: &Document, child: &Document) -> Document {
44 let mut result = base.clone();
45 for (key, value) in child {
46 if value.is_null() {
47 result.remove(key);
48 } else if let (Some(left), Some(right)) = (
49 result.get(key).and_then(Value::as_object),
50 value.as_object(),
51 ) {
52 result.insert(
53 key.clone(),
54 Value::Object(merge_profile_values(left, right)),
55 );
56 } else {
57 result.insert(key.clone(), value.clone());
58 }
59 }
60 result
61}
62
63pub fn resolve_composition<F>(
65 profile: &AgentProfile,
66 mut load: F,
67) -> Result<AgentProfile, CompositionError>
68where
69 F: FnMut(&ProfileReference) -> Result<AgentProfile, CompositionError>,
70{
71 fn resolve<F>(
72 profile: &AgentProfile,
73 load: &mut F,
74 active: &mut Vec<String>,
75 ) -> Result<AgentProfile, CompositionError>
76 where
77 F: FnMut(&ProfileReference) -> Result<AgentProfile, CompositionError>,
78 {
79 let name = object(profile.get("metadata"))
80 .get("name")
81 .and_then(Value::as_str)
82 .unwrap_or_default()
83 .to_owned();
84 if active.contains(&name) {
85 return Err(CompositionError::Cycle);
86 }
87 if active.len() >= 8 {
88 return Err(CompositionError::Depth);
89 }
90 active.push(name.clone());
91 let mut merged = Document::new();
92 for raw in profile
93 .get("extends")
94 .and_then(Value::as_array)
95 .into_iter()
96 .flatten()
97 {
98 let value = object(Some(raw));
99 let reference = ProfileReference {
100 name: value
101 .get("name")
102 .and_then(Value::as_str)
103 .unwrap_or_default()
104 .into(),
105 uri: value
106 .get("uri")
107 .and_then(Value::as_str)
108 .unwrap_or_default()
109 .into(),
110 revision: value.get("revision").and_then(Value::as_u64).unwrap_or(0),
111 digest: value
112 .get("digest")
113 .and_then(Value::as_str)
114 .unwrap_or_default()
115 .into(),
116 };
117 let base = load(&reference)?;
118 if reference.revision != 0
119 && object(base.get("metadata"))
120 .get("revision")
121 .and_then(Value::as_u64)
122 != Some(reference.revision)
123 {
124 return Err(CompositionError::Revision(reference.name));
125 }
126 if !reference.digest.is_empty()
127 && spec_digest(&base).map_err(|error| CompositionError::Load(error.to_string()))?
128 != reference.digest
129 {
130 return Err(CompositionError::Digest(reference.name));
131 }
132 let mut resolved = resolve(&base, load, active)?;
133 for key in ["extends", "state", "history"] {
134 resolved.remove(key);
135 }
136 if let Some(metadata) = resolved.get_mut("metadata").and_then(Value::as_object_mut) {
137 for key in ["name", "id", "revision"] {
138 metadata.remove(key);
139 }
140 }
141 merged = merge_profile_values(&merged, &resolved);
142 }
143 active.pop();
144 merged = merge_profile_values(&merged, profile);
145 merged.insert(
146 "metadata".into(),
147 profile.get("metadata").cloned().unwrap_or_default(),
148 );
149 for key in ["state", "history"] {
150 if let Some(value) = profile.get(key) {
151 merged.insert(key.into(), value.clone());
152 } else {
153 merged.remove(key);
154 }
155 }
156 merged.remove("extends");
157 if object(merged.get("metadata"))
158 .get("name")
159 .and_then(Value::as_str)
160 .unwrap_or_default()
161 .is_empty()
162 {
163 return Err(CompositionError::MissingName);
164 }
165 Ok(merged)
166 }
167 resolve(profile, &mut load, &mut vec![])
168}