use std::collections::{HashMap, HashSet};
use std::path::Path;
use serde_json::{json, Value};
use crate::error::ComposeError;
use crate::loader::{bundle_refs, bundle_refs_with_url_mapping, is_url, load_schema};
use crate::types::{Direction, Requires, VersionConstraint};
#[cfg(feature = "remote")]
use crate::loader::{bundle_refs_remote, load_schema_url};
#[derive(Debug, Clone, Default)]
pub struct SchemaBaseConfig<'a> {
pub local_base: Option<&'a Path>,
pub remote_base: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub struct Capability {
pub name: String,
pub version: String,
pub schema_url: String,
pub extends: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectedDirection {
Response,
Request,
}
impl From<DetectedDirection> for Direction {
fn from(d: DetectedDirection) -> Self {
match d {
DetectedDirection::Response => Direction::Response,
DetectedDirection::Request => Direction::Request,
}
}
}
pub fn detect_direction(payload: &Value) -> Option<DetectedDirection> {
if let Some(ucp) = payload.get("ucp") {
if ucp.get("capabilities").is_some() {
return Some(DetectedDirection::Response);
}
}
if payload.get("meta").and_then(|m| m.get("profile")).is_some() {
return Some(DetectedDirection::Request);
}
None
}
pub fn extract_capabilities(
payload: &Value,
schema_base: &SchemaBaseConfig,
) -> Result<Vec<Capability>, ComposeError> {
if let Some(ucp) = payload.get("ucp") {
if let Some(caps) = ucp.get("capabilities") {
return parse_capabilities_object(caps);
}
}
if let Some(profile_url) = payload
.get("meta")
.and_then(|m| m.get("profile"))
.and_then(|p| p.as_str())
{
return extract_capabilities_from_profile(profile_url, schema_base);
}
Err(ComposeError::NotSelfDescribing)
}
pub fn extract_capabilities_from_profile(
profile_url: &str,
schema_base: &SchemaBaseConfig,
) -> Result<Vec<Capability>, ComposeError> {
let profile = fetch_profile(profile_url, schema_base)?;
let caps = profile
.get("ucp")
.and_then(|u| u.get("capabilities"))
.ok_or_else(|| ComposeError::ProfileFetch {
url: profile_url.to_string(),
message: "profile missing ucp.capabilities".to_string(),
})?;
parse_capabilities_object(caps)
}
pub fn extract_jsonrpc_payload<'a>(
envelope: &'a Value,
capabilities: &[Capability],
) -> Result<(&'a Value, String), ComposeError> {
let root = capabilities
.iter()
.find(|c| c.extends.is_none())
.ok_or(ComposeError::NoRootCapability)?;
let short_name = capability_short_name(&root.name);
let payload = envelope
.get(&short_name)
.ok_or_else(|| ComposeError::InvalidEnvelope {
message: format!(
"JSONRPC envelope missing '{}' key (derived from capability '{}')",
short_name, root.name
),
})?;
Ok((payload, short_name))
}
pub fn capability_short_name(name: &str) -> String {
name.rsplit('.').next().unwrap_or(name).to_string()
}
fn parse_capabilities_object(caps: &Value) -> Result<Vec<Capability>, ComposeError> {
let obj = caps.as_object().ok_or(ComposeError::EmptyCapabilities)?;
if obj.is_empty() {
return Err(ComposeError::EmptyCapabilities);
}
let mut capabilities = Vec::new();
for (name, versions) in obj {
let entries = versions
.as_array()
.ok_or_else(|| ComposeError::InvalidCapability {
name: name.clone(),
message: "expected array of capability entries".to_string(),
})?;
let entry = entries
.first()
.ok_or_else(|| ComposeError::InvalidCapability {
name: name.clone(),
message: "empty capability array".to_string(),
})?;
let version = entry
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| ComposeError::InvalidCapability {
name: name.clone(),
message: "missing version field".to_string(),
})?
.to_string();
let schema_url = entry
.get("schema")
.and_then(|v| v.as_str())
.ok_or_else(|| ComposeError::InvalidCapability {
name: name.clone(),
message: "missing schema field".to_string(),
})?
.to_string();
let extends = match entry.get("extends") {
None => None,
Some(Value::String(s)) => Some(vec![s.clone()]),
Some(Value::Array(arr)) => {
let parents: Result<Vec<String>, _> = arr
.iter()
.map(|v| {
v.as_str().map(|s| s.to_string()).ok_or_else(|| {
ComposeError::InvalidCapability {
name: name.clone(),
message: "extends array must contain strings".to_string(),
}
})
})
.collect();
Some(parents?)
}
Some(_) => {
return Err(ComposeError::InvalidCapability {
name: name.clone(),
message: "extends must be string or array of strings".to_string(),
});
}
};
capabilities.push(Capability {
name: name.clone(),
version,
schema_url,
extends,
});
}
Ok(capabilities)
}
fn fetch_profile(url: &str, schema_base: &SchemaBaseConfig) -> Result<Value, ComposeError> {
resolve_schema_url(url, schema_base).map_err(|e| ComposeError::ProfileFetch {
url: url.to_string(),
message: e.to_string(),
})
}
#[derive(Debug, Clone)]
pub struct VersionViolation {
pub extension: String,
pub target: String,
pub constraint: VersionConstraint,
pub actual: String,
}
impl VersionViolation {
pub fn range_display(&self) -> String {
match &self.constraint.max {
Some(max) => format!("[{}, {}]", self.constraint.min, max),
None => format!(">= {}", self.constraint.min),
}
}
}
impl std::fmt::Display for VersionViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"extension '{}' requires {} {} but found {}",
self.extension,
self.target,
self.range_display(),
self.actual
)
}
}
pub fn check_version_constraints(
extension_name: &str,
extension_schema: &Value,
protocol_version: Option<&str>,
capabilities: &[Capability],
) -> Vec<VersionViolation> {
let Some(requires_val) = extension_schema.get("requires") else {
return vec![];
};
let requires = match Requires::parse(requires_val) {
Ok(r) => r,
Err(_) => return vec![], };
let mut violations = Vec::new();
if let (Some(ref constraint), Some(version)) = (&requires.protocol, protocol_version) {
if !constraint.satisfied_by(version) {
violations.push(VersionViolation {
extension: extension_name.to_string(),
target: "protocol".to_string(),
constraint: constraint.clone(),
actual: version.to_string(),
});
}
}
let cap_versions: HashMap<&str, &str> = capabilities
.iter()
.map(|c| (c.name.as_str(), c.version.as_str()))
.collect();
for (cap_name, constraint) in &requires.capabilities {
if let Some(&version) = cap_versions.get(cap_name.as_str()) {
if !constraint.satisfied_by(version) {
violations.push(VersionViolation {
extension: extension_name.to_string(),
target: cap_name.clone(),
constraint: constraint.clone(),
actual: version.to_string(),
});
}
}
}
violations
}
pub fn compose_schema(
capabilities: &[Capability],
schema_base: &SchemaBaseConfig,
) -> Result<Value, ComposeError> {
if capabilities.is_empty() {
return Err(ComposeError::EmptyCapabilities);
}
let cap_map: HashMap<&str, &Capability> =
capabilities.iter().map(|c| (c.name.as_str(), c)).collect();
let roots: Vec<&Capability> = capabilities
.iter()
.filter(|c| c.extends.is_none())
.collect();
let root = match roots.len() {
0 => return Err(ComposeError::NoRootCapability),
1 => roots[0],
_ => {
return Err(ComposeError::MultipleRootCapabilities {
names: roots.iter().map(|c| c.name.clone()).collect(),
})
}
};
for cap in capabilities {
if let Some(parents) = &cap.extends {
for parent in parents {
if !cap_map.contains_key(parent.as_str()) {
return Err(ComposeError::UnknownParent {
extension: cap.name.clone(),
parent: parent.clone(),
});
}
}
}
}
for cap in capabilities {
if cap.extends.is_some() && !reaches_root(cap, &cap_map, &root.name) {
return Err(ComposeError::OrphanExtension {
extension: cap.name.clone(),
root: root.name.clone(),
});
}
}
let extensions: Vec<&Capability> = capabilities
.iter()
.filter(|c| c.extends.is_some())
.collect();
if extensions.is_empty() {
return resolve_schema_url(&root.schema_url, schema_base).map_err(|e| {
ComposeError::SchemaFetch {
url: root.schema_url.clone(),
message: e.to_string(),
}
});
}
let root_schema = resolve_schema_url(&root.schema_url, schema_base).map_err(|e| {
ComposeError::SchemaFetch {
url: root.schema_url.clone(),
message: e.to_string(),
}
})?;
let container = is_container_schema(&root_schema);
let mut ext_defs = Vec::new();
for ext in &extensions {
let ext_schema = resolve_schema_url(&ext.schema_url, schema_base).map_err(|e| {
ComposeError::SchemaFetch {
url: ext.schema_url.clone(),
message: e.to_string(),
}
})?;
let violations =
check_version_constraints(&ext.name, &ext_schema, Some(&root.version), capabilities);
if let Some(v) = violations.first() {
return Err(ComposeError::VersionConstraintViolation {
extension: v.extension.clone(),
target: v.target.clone(),
range: v.range_display(),
actual: v.actual.clone(),
});
}
let defs = ext_schema
.get("$defs")
.ok_or_else(|| ComposeError::MissingDefEntry {
extension: ext.name.clone(),
expected_key: root.name.clone(),
})?;
let ext_def = defs
.get(&root.name)
.ok_or_else(|| ComposeError::MissingDefEntry {
extension: ext.name.clone(),
expected_key: root.name.clone(),
})?;
let mut inlined = ext_def.clone();
inline_internal_refs(&mut inlined, defs);
ext_defs.push(inlined);
}
if container {
compose_container(&root_schema, &extensions, &ext_defs, &root.name)
} else {
Ok(json!({ "allOf": ext_defs }))
}
}
pub fn is_container_schema(schema: &Value) -> bool {
match schema.as_object() {
Some(obj) => {
obj.contains_key("$defs")
&& !obj.contains_key("properties")
&& !obj.contains_key("allOf")
&& !obj.contains_key("$ref")
}
None => false,
}
}
fn compose_container(
root_schema: &Value,
extensions: &[&Capability],
ext_defs: &[Value],
capability: &str,
) -> Result<Value, ComposeError> {
let mut merged_defs: serde_json::Map<String, Value> = root_schema
.get("$defs")
.and_then(|d| d.as_object())
.cloned()
.unwrap_or_default();
let mut order: Vec<String> = Vec::new();
let mut per_op: HashMap<String, Vec<Value>> = HashMap::new();
for (ext, inlined) in extensions.iter().zip(ext_defs.iter()) {
let nested = inlined
.get("$defs")
.and_then(|d| d.as_object())
.ok_or_else(|| ComposeError::ContainerExtensionShape {
extension: ext.name.clone(),
capability: capability.to_string(),
})?;
for (op_key, shape) in nested {
if !per_op.contains_key(op_key) {
order.push(op_key.clone());
}
per_op
.entry(op_key.clone())
.or_default()
.push(shape.clone());
}
}
for op_key in order {
let contribs = per_op.remove(&op_key).unwrap();
let merged = if contribs.len() == 1 {
contribs.into_iter().next().unwrap()
} else {
json!({ "allOf": contribs })
};
merged_defs.insert(op_key, merged);
}
let mut result = root_schema.clone();
result
.as_object_mut()
.expect("container root is an object")
.insert("$defs".to_string(), Value::Object(merged_defs));
Ok(result)
}
fn inline_internal_refs(value: &mut Value, defs: &Value) {
inline_internal_refs_inner(value, defs, &mut HashSet::new());
}
fn inline_internal_refs_inner(value: &mut Value, defs: &Value, visited: &mut HashSet<String>) {
match value {
Value::Object(obj) => {
if let Some(ref_val) = obj.get("$ref").and_then(|v| v.as_str()) {
if let Some(def_name) = ref_val.strip_prefix("#/$defs/") {
if visited.contains(def_name) {
return;
}
if let Some(def) = defs.get(def_name) {
visited.insert(def_name.to_string());
let mut inlined = def.clone();
inline_internal_refs_inner(&mut inlined, defs, visited);
visited.remove(def_name);
obj.remove("$ref");
if let Value::Object(def_obj) = inlined {
for (k, v) in def_obj {
obj.entry(k).or_insert(v);
}
}
return;
}
}
}
for v in obj.values_mut() {
inline_internal_refs_inner(v, defs, visited);
}
}
Value::Array(arr) => {
for item in arr {
inline_internal_refs_inner(item, defs, visited);
}
}
_ => {}
}
}
fn reaches_root(cap: &Capability, cap_map: &HashMap<&str, &Capability>, root_name: &str) -> bool {
let mut visited = HashSet::new();
let mut queue = vec![cap];
while let Some(current) = queue.pop() {
if visited.contains(¤t.name.as_str()) {
continue;
}
visited.insert(current.name.as_str());
if let Some(parents) = ¤t.extends {
for parent_name in parents {
if parent_name == root_name {
return true;
}
if let Some(parent) = cap_map.get(parent_name.as_str()) {
queue.push(parent);
}
}
}
}
false
}
pub fn compose_from_payload(
payload: &Value,
schema_base: &SchemaBaseConfig,
) -> Result<Value, ComposeError> {
let capabilities = extract_capabilities(payload, schema_base)?;
compose_schema(&capabilities, schema_base)
}
fn resolve_schema_url(url: &str, schema_base: &SchemaBaseConfig) -> Result<Value, ComposeError> {
if let Some(base) = schema_base.local_base {
let path = if let Some(remote_base) = schema_base.remote_base {
if let Some(remainder) = url.strip_prefix(remote_base) {
remainder.to_string()
} else {
extract_url_path(url)?
}
} else {
extract_url_path(url)?
};
let local_path = base.join(path.trim_start_matches('/'));
let mut schema = load_schema(&local_path).map_err(|_| ComposeError::SchemaFetch {
url: url.to_string(),
message: format!("file not found: {}", local_path.display()),
})?;
let schema_dir = local_path.parent().unwrap_or(base);
if let Some(remote_base) = schema_base.remote_base {
bundle_refs_with_url_mapping(&mut schema, schema_dir, base, remote_base).map_err(
|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: format!("bundling refs: {}", e),
},
)?;
} else {
bundle_refs(&mut schema, schema_dir).map_err(|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: format!("bundling refs: {}", e),
})?;
}
Ok(schema)
} else if is_url(url) {
#[cfg(feature = "remote")]
{
let mut schema = load_schema_url(url).map_err(|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: e.to_string(),
})?;
bundle_refs_remote(&mut schema, url).map_err(|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: format!("bundling refs: {}", e),
})?;
Ok(schema)
}
#[cfg(not(feature = "remote"))]
{
Err(ComposeError::SchemaFetch {
url: url.to_string(),
message: "HTTP fetching requires 'remote' feature".to_string(),
})
}
} else {
let local_path = Path::new(url);
let mut schema = load_schema(local_path).map_err(|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: e.to_string(),
})?;
if let Some(schema_dir) = local_path.parent() {
bundle_refs(&mut schema, schema_dir).map_err(|e| ComposeError::SchemaFetch {
url: url.to_string(),
message: format!("bundling refs: {}", e),
})?;
}
Ok(schema)
}
}
fn extract_url_path(url: &str) -> Result<String, ComposeError> {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"));
match rest {
Some(after_scheme) => {
after_scheme
.find('/')
.map(|idx| after_scheme[idx..].to_string())
.ok_or_else(|| ComposeError::InvalidUrl {
url: url.to_string(),
message: "could not extract path from URL".to_string(),
})
}
None => {
Ok(url.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn detect_direction_response() {
let payload = json!({
"ucp": {
"capabilities": {
"dev.ucp.shopping.checkout": [{"version": "2026-01-11", "schema": "..."}]
}
}
});
assert_eq!(
detect_direction(&payload),
Some(DetectedDirection::Response)
);
}
#[test]
fn detect_direction_request() {
let payload = json!({
"meta": {
"profile": "https://example.com/.well-known/ucp"
},
"checkout": {
"line_items": []
}
});
assert_eq!(detect_direction(&payload), Some(DetectedDirection::Request));
}
#[test]
fn detect_direction_old_request_format_not_detected() {
let payload = json!({
"ucp": {
"meta": {
"profile": "https://example.com/.well-known/ucp"
}
}
});
assert_eq!(detect_direction(&payload), None);
}
#[test]
fn detect_direction_neither() {
let payload = json!({
"ucp": {
"version": "2026-01-11"
}
});
assert_eq!(detect_direction(&payload), None);
}
#[test]
fn detect_direction_no_ucp() {
let payload = json!({
"id": "123",
"status": "incomplete"
});
assert_eq!(detect_direction(&payload), None);
}
#[test]
fn parse_capabilities_single_root() {
let caps = json!({
"dev.ucp.shopping.checkout": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/checkout.json"
}]
});
let result = parse_capabilities_object(&caps).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "dev.ucp.shopping.checkout");
assert_eq!(result[0].version, "2026-01-11");
assert!(result[0].extends.is_none());
}
#[test]
fn parse_capabilities_with_extension() {
let caps = json!({
"dev.ucp.shopping.checkout": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/checkout.json"
}],
"dev.ucp.shopping.discount": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/discount.json",
"extends": "dev.ucp.shopping.checkout"
}]
});
let result = parse_capabilities_object(&caps).unwrap();
assert_eq!(result.len(), 2);
let discount = result
.iter()
.find(|c| c.name == "dev.ucp.shopping.discount")
.unwrap();
assert_eq!(
discount.extends,
Some(vec!["dev.ucp.shopping.checkout".to_string()])
);
}
#[test]
fn parse_capabilities_multi_parent() {
let caps = json!({
"dev.ucp.shopping.checkout": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/checkout.json"
}],
"dev.ucp.shopping.discount": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/discount.json",
"extends": "dev.ucp.shopping.checkout"
}],
"dev.ucp.shopping.fulfillment": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout"
}],
"dev.ucp.shopping.combo": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/combo.json",
"extends": ["dev.ucp.shopping.discount", "dev.ucp.shopping.fulfillment"]
}]
});
let result = parse_capabilities_object(&caps).unwrap();
assert_eq!(result.len(), 4);
let combo = result
.iter()
.find(|c| c.name == "dev.ucp.shopping.combo")
.unwrap();
assert_eq!(
combo.extends,
Some(vec![
"dev.ucp.shopping.discount".to_string(),
"dev.ucp.shopping.fulfillment".to_string()
])
);
}
#[test]
fn parse_capabilities_empty() {
let caps = json!({});
let result = parse_capabilities_object(&caps);
assert!(matches!(result, Err(ComposeError::EmptyCapabilities)));
}
#[test]
fn extract_url_path_https() {
let path = extract_url_path("https://ucp.dev/schemas/shopping/checkout.json").unwrap();
assert_eq!(path, "/schemas/shopping/checkout.json");
}
#[test]
fn extract_url_path_http() {
let path = extract_url_path("http://localhost:8080/schemas/test.json").unwrap();
assert_eq!(path, "/schemas/test.json");
}
#[test]
fn extract_url_path_local() {
let path = extract_url_path("./schemas/checkout.json").unwrap();
assert_eq!(path, "./schemas/checkout.json");
}
#[test]
fn compose_no_extensions() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let config = SchemaBaseConfig {
local_base: Some(Path::new("/nonexistent")),
remote_base: None,
};
let result = compose_schema(&[checkout], &config);
assert!(matches!(result, Err(ComposeError::SchemaFetch { .. })));
}
#[test]
fn compose_no_root_error() {
let discount = Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-01-11".to_string(),
schema_url: "discount.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
};
let config = SchemaBaseConfig::default();
let result = compose_schema(&[discount], &config);
assert!(matches!(result, Err(ComposeError::NoRootCapability)));
}
#[test]
fn compose_multiple_roots_error() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let fulfillment = Capability {
name: "dev.ucp.shopping.fulfillment".to_string(),
version: "2026-01-11".to_string(),
schema_url: "fulfillment.json".to_string(),
extends: None, };
let config = SchemaBaseConfig::default();
let result = compose_schema(&[checkout, fulfillment], &config);
assert!(matches!(
result,
Err(ComposeError::MultipleRootCapabilities { .. })
));
}
#[test]
fn compose_unknown_parent_error() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let discount = Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-01-11".to_string(),
schema_url: "discount.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.nonexistent".to_string()]),
};
let config = SchemaBaseConfig::default();
let result = compose_schema(&[checkout, discount], &config);
assert!(matches!(result, Err(ComposeError::UnknownParent { .. })));
}
#[test]
fn reaches_root_direct() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let discount = Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-01-11".to_string(),
schema_url: "discount.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
};
let cap_map: HashMap<&str, &Capability> = vec![
("dev.ucp.shopping.checkout", &checkout),
("dev.ucp.shopping.discount", &discount),
]
.into_iter()
.collect();
assert!(reaches_root(
&discount,
&cap_map,
"dev.ucp.shopping.checkout"
));
}
#[test]
fn reaches_root_transitive_diamond() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let discount = Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-01-11".to_string(),
schema_url: "discount.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
};
let fulfillment = Capability {
name: "dev.ucp.shopping.fulfillment".to_string(),
version: "2026-01-11".to_string(),
schema_url: "fulfillment.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
};
let combo = Capability {
name: "dev.ucp.shopping.combo".to_string(),
version: "2026-01-11".to_string(),
schema_url: "combo.json".to_string(),
extends: Some(vec![
"dev.ucp.shopping.discount".to_string(),
"dev.ucp.shopping.fulfillment".to_string(),
]),
};
let cap_map: HashMap<&str, &Capability> = vec![
("dev.ucp.shopping.checkout", &checkout),
("dev.ucp.shopping.discount", &discount),
("dev.ucp.shopping.fulfillment", &fulfillment),
("dev.ucp.shopping.combo", &combo),
]
.into_iter()
.collect();
assert!(reaches_root(&combo, &cap_map, "dev.ucp.shopping.checkout"));
assert!(reaches_root(
&discount,
&cap_map,
"dev.ucp.shopping.checkout"
));
assert!(reaches_root(
&fulfillment,
&cap_map,
"dev.ucp.shopping.checkout"
));
}
#[test]
fn reaches_root_orphan() {
let checkout = Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-11".to_string(),
schema_url: "checkout.json".to_string(),
extends: None,
};
let discount = Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-01-11".to_string(),
schema_url: "discount.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.nonexistent".to_string()]),
};
let cap_map: HashMap<&str, &Capability> = vec![
("dev.ucp.shopping.checkout", &checkout),
("dev.ucp.shopping.discount", &discount),
]
.into_iter()
.collect();
assert!(!reaches_root(
&discount,
&cap_map,
"dev.ucp.shopping.checkout"
));
}
#[test]
fn capability_short_name_extracts_last_segment() {
assert_eq!(
capability_short_name("dev.ucp.shopping.checkout"),
"checkout"
);
assert_eq!(
capability_short_name("dev.ucp.shopping.discount"),
"discount"
);
assert_eq!(capability_short_name("checkout"), "checkout");
}
#[test]
fn extract_jsonrpc_payload_finds_checkout_key() {
let envelope = json!({
"meta": {"profile": "https://example.com/profile"},
"checkout": {"line_items": [{"item": {"id": "sku"}, "quantity": 2}]}
});
let capabilities = vec![Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-26".to_string(),
schema_url: "https://example.com/checkout.json".to_string(),
extends: None,
}];
let (payload, key) = extract_jsonrpc_payload(&envelope, &capabilities).unwrap();
assert_eq!(key, "checkout");
assert_eq!(payload["line_items"][0]["quantity"], 2);
}
#[test]
fn extract_jsonrpc_payload_missing_key_errors() {
let envelope = json!({
"meta": {"profile": "https://example.com/profile"},
"wrong_key": {"line_items": []}
});
let capabilities = vec![Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-01-26".to_string(),
schema_url: "https://example.com/checkout.json".to_string(),
extends: None,
}];
let result = extract_jsonrpc_payload(&envelope, &capabilities);
assert!(matches!(result, Err(ComposeError::InvalidEnvelope { .. })));
}
#[test]
fn compose_rejects_violated_protocol_constraint() {
let dir = tempfile::tempdir().unwrap();
let checkout_path = dir.path().join("checkout.json");
std::fs::write(
&checkout_path,
r#"{"type": "object", "properties": {"id": {"type": "string"}}}"#,
)
.unwrap();
let ext_path = dir.path().join("loyalty.json");
std::fs::write(
&ext_path,
r#"{
"requires": { "protocol": { "min": "2026-09-01" } },
"$defs": {
"dev.ucp.shopping.checkout": {
"type": "object",
"properties": { "loyalty": { "type": "integer" } }
}
}
}"#,
)
.unwrap();
let capabilities = vec![
Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-06-01".to_string(),
schema_url: checkout_path.to_str().unwrap().to_string(),
extends: None,
},
Capability {
name: "com.acme.loyalty".to_string(),
version: "2026-01-01".to_string(),
schema_url: ext_path.to_str().unwrap().to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
},
];
let config = SchemaBaseConfig::default();
let result = compose_schema(&capabilities, &config);
assert!(
matches!(result, Err(ComposeError::VersionConstraintViolation { .. })),
"expected VersionConstraintViolation, got {:?}",
result
);
}
#[test]
fn compose_rejects_violated_capability_constraint() {
let dir = tempfile::tempdir().unwrap();
let checkout_path = dir.path().join("checkout.json");
std::fs::write(
&checkout_path,
r#"{"type": "object", "properties": {"id": {"type": "string"}}}"#,
)
.unwrap();
let ext_path = dir.path().join("loyalty.json");
std::fs::write(
&ext_path,
r#"{
"requires": {
"capabilities": {
"dev.ucp.shopping.checkout": { "min": "2026-09-01" }
}
},
"$defs": {
"dev.ucp.shopping.checkout": {
"type": "object",
"properties": { "loyalty": { "type": "integer" } }
}
}
}"#,
)
.unwrap();
let capabilities = vec![
Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-06-01".to_string(),
schema_url: checkout_path.to_str().unwrap().to_string(),
extends: None,
},
Capability {
name: "com.acme.loyalty".to_string(),
version: "2026-01-01".to_string(),
schema_url: ext_path.to_str().unwrap().to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
},
];
let config = SchemaBaseConfig::default();
let result = compose_schema(&capabilities, &config);
match &result {
Err(ComposeError::VersionConstraintViolation {
extension, target, ..
}) => {
assert_eq!(extension, "com.acme.loyalty");
assert_eq!(target, "dev.ucp.shopping.checkout");
}
other => panic!("expected VersionConstraintViolation, got {:?}", other),
}
}
#[test]
fn compose_succeeds_when_constraints_satisfied() {
let dir = tempfile::tempdir().unwrap();
let checkout_path = dir.path().join("checkout.json");
std::fs::write(
&checkout_path,
r#"{"type": "object", "properties": {"id": {"type": "string"}}}"#,
)
.unwrap();
let ext_path = dir.path().join("loyalty.json");
std::fs::write(
&ext_path,
r#"{
"requires": {
"protocol": { "min": "2026-01-23" },
"capabilities": {
"dev.ucp.shopping.checkout": { "min": "2026-01-23" }
}
},
"$defs": {
"dev.ucp.shopping.checkout": {
"type": "object",
"properties": { "loyalty": { "type": "integer" } }
}
}
}"#,
)
.unwrap();
let capabilities = vec![
Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-06-01".to_string(),
schema_url: checkout_path.to_str().unwrap().to_string(),
extends: None,
},
Capability {
name: "com.acme.loyalty".to_string(),
version: "2026-01-01".to_string(),
schema_url: ext_path.to_str().unwrap().to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
},
];
let config = SchemaBaseConfig::default();
let result = compose_schema(&capabilities, &config);
assert!(result.is_ok(), "expected Ok, got {:?}", result);
}
#[test]
fn compose_succeeds_without_requires() {
let dir = tempfile::tempdir().unwrap();
let checkout_path = dir.path().join("checkout.json");
std::fs::write(
&checkout_path,
r#"{"type": "object", "properties": {"id": {"type": "string"}}}"#,
)
.unwrap();
let ext_path = dir.path().join("discount.json");
std::fs::write(
&ext_path,
r#"{
"$defs": {
"dev.ucp.shopping.checkout": {
"type": "object",
"properties": { "discounts": { "type": "array" } }
}
}
}"#,
)
.unwrap();
let capabilities = vec![
Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-06-01".to_string(),
schema_url: checkout_path.to_str().unwrap().to_string(),
extends: None,
},
Capability {
name: "dev.ucp.shopping.discount".to_string(),
version: "2026-06-01".to_string(),
schema_url: ext_path.to_str().unwrap().to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
},
];
let config = SchemaBaseConfig::default();
let result = compose_schema(&capabilities, &config);
assert!(result.is_ok(), "expected Ok, got {:?}", result);
}
fn make_capabilities() -> Vec<Capability> {
vec![
Capability {
name: "dev.ucp.shopping.checkout".to_string(),
version: "2026-06-01".to_string(),
schema_url: "https://example.com/checkout.json".to_string(),
extends: None,
},
Capability {
name: "dev.ucp.shopping.fulfillment".to_string(),
version: "2026-03-01".to_string(),
schema_url: "https://example.com/fulfillment.json".to_string(),
extends: Some(vec!["dev.ucp.shopping.checkout".to_string()]),
},
]
}
#[test]
fn version_constraints_satisfied() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"protocol": { "min": "2026-01-23" },
"capabilities": {
"dev.ucp.shopping.checkout": { "min": "2026-01-23" }
}
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert!(violations.is_empty());
}
#[test]
fn version_constraints_protocol_violation() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"protocol": { "min": "2026-09-01" }
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].target, "protocol");
}
#[test]
fn version_constraints_capability_violation() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"capabilities": {
"dev.ucp.shopping.checkout": { "min": "2026-09-01" }
}
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].target, "dev.ucp.shopping.checkout");
assert_eq!(violations[0].actual, "2026-06-01");
}
#[test]
fn version_constraints_max_exceeded() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"capabilities": {
"dev.ucp.shopping.checkout": {
"min": "2026-01-23",
"max": "2026-03-01"
}
}
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert_eq!(violations.len(), 1);
assert!(violations[0]
.to_string()
.contains("[2026-01-23, 2026-03-01]"));
}
#[test]
fn version_constraints_no_requires() {
let caps = make_capabilities();
let schema = json!({ "type": "object" });
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert!(violations.is_empty());
}
#[test]
fn version_constraints_no_protocol_version() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"protocol": { "min": "2026-09-01" }
}
});
let violations = check_version_constraints("com.acme.loyalty", &schema, None, &caps);
assert!(violations.is_empty());
}
#[test]
fn version_constraints_multiple_violations() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"protocol": { "min": "2026-09-01" },
"capabilities": {
"dev.ucp.shopping.checkout": { "min": "2026-09-01" }
}
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert_eq!(violations.len(), 2);
let targets: Vec<&str> = violations.iter().map(|v| v.target.as_str()).collect();
assert!(targets.contains(&"protocol"));
assert!(targets.contains(&"dev.ucp.shopping.checkout"));
}
#[test]
fn version_constraints_unknown_capability() {
let caps = make_capabilities();
let schema = json!({
"requires": {
"capabilities": {
"dev.ucp.shopping.order": { "min": "2026-01-23" }
}
}
});
let violations =
check_version_constraints("com.acme.loyalty", &schema, Some("2026-06-01"), &caps);
assert!(violations.is_empty());
}
}