feat: migrate protocol and HTTP to structured interface references

T-20: identify interfaces by exact scope/name pairs; add optional opaque scope_ref metadata and separate shape validation from path-dependent scope compatibility. Preserve call error precedence, strict JSON, validators and transport boundaries. Mechanically migrate consumers and document API/wire compatibility and deferred lifecycle semantics.

Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test -p wip-protocol -p wip-http; cargo test --workspace (221 passing tests including doctests); cargo test --workspace --all-targets; nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps. All pass. cargo/rustc 1.95.0; development-shell Clippy/rustc 1.97.1.
This commit is contained in:
2026-10-07 18:29:51 +09:00
parent bf5fa54f44
commit 8ecc307fc9
34 changed files with 2124 additions and 263 deletions
+133 -14
View File
@@ -35,7 +35,10 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
.unwrap();
assert_eq!(
projected.protocol_object().interfaces,
vec![write.reference().as_str(), read.reference().as_str()]
vec![
write.reference().protocol_reference(),
read.reference().protocol_reference()
]
);
assert!(
publication()
@@ -58,14 +61,18 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
.interfaces
.is_empty()
);
let full = registry.fetch_interface(read.reference().as_str()).unwrap();
let full = registry
.fetch_interface(&read.reference().protocol_reference())
.unwrap();
let filtered = full
.clone()
.project_operations(|operation| operation == "label");
assert_eq!(filtered.descriptor.operations.len(), 1);
assert_eq!(full.descriptor.operations.len(), 3);
assert_eq!(
registry.fetch_interface(read.reference().as_str()).unwrap(),
registry
.fetch_interface(&read.reference().protocol_reference())
.unwrap(),
full
);
}
@@ -478,7 +485,8 @@ fn request(
validator: object_validator.map(<[u8]>::to_vec),
},
interface: InterfaceTarget {
reference: reference.as_str().to_owned(),
reference: reference.protocol_reference(),
scope_ref: None,
validator: interface_validator.map(<[u8]>::to_vec),
},
operation: operation.into(),
@@ -497,17 +505,22 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
let reference = provider.reader.reference();
assert_ne!(reference.as_str(), "Reader");
let observation = registry.fetch_interface(reference.as_str()).unwrap();
let observation = registry
.fetch_interface(&reference.protocol_reference())
.unwrap();
assert_eq!(observation.reference, *reference);
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
let response = observation.protocol_response().unwrap();
assert_eq!(response.interface, reference.as_str());
assert_eq!(response.interface, reference.protocol_reference());
assert_eq!(response.scope_ref, None);
assert_eq!(response.descriptor, ReaderDefinition::descriptor());
assert_eq!(response.validator.as_deref(), Some(b"schema-v1".as_slice()));
response.descriptor.validate().unwrap();
let mut invalid_observation = registry.fetch_interface(reference.as_str()).unwrap();
let mut invalid_observation = registry
.fetch_interface(&reference.protocol_reference())
.unwrap();
let duplicate_operation = invalid_observation.descriptor.operations[0].clone();
invalid_observation
.descriptor
@@ -516,7 +529,12 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
let invalid_response = invalid_observation.protocol_response().unwrap_err();
assert_eq!(invalid_response.code, ProtocolErrorCode::Internal);
let error = registry.fetch_interface("unknown").unwrap_err();
let error = registry
.fetch_interface(&wip_protocol::InterfaceReference {
scope: "/".into(),
name: "unknown".into(),
})
.unwrap_err();
assert_eq!(error.code, ProtocolErrorCode::InterfaceNotFound);
}
@@ -532,8 +550,8 @@ fn canonical_object_projection_supports_optional_metadata_and_ordered_sets() {
assert_eq!(
object.interfaces,
vec![
provider.writer.reference().as_str().to_owned(),
provider.reader.reference().as_str().to_owned(),
provider.writer.reference().protocol_reference(),
provider.reader.reference().protocol_reference(),
]
);
assert!(!format!("{object:?}").contains("secret-handle"));
@@ -814,7 +832,8 @@ fn descriptor_guided_decoder_runs_inside_canonical_resolution_boundary() {
validator: Some(b"stale".to_vec()),
},
InterfaceTarget {
reference: hidden.reference().as_str().into(),
reference: hidden.reference().protocol_reference(),
scope_ref: None,
validator: None,
},
"missing".into(),
@@ -832,7 +851,8 @@ fn descriptor_guided_decoder_runs_inside_canonical_resolution_boundary() {
validator: Some(b"stale".to_vec()),
},
InterfaceTarget {
reference: hidden.reference().as_str().into(),
reference: hidden.reference().protocol_reference(),
scope_ref: None,
validator: None,
},
"hidden".into(),
@@ -850,7 +870,8 @@ fn descriptor_guided_decoder_runs_inside_canonical_resolution_boundary() {
validator: Some(b"object-v1".to_vec()),
},
InterfaceTarget {
reference: hidden.reference().as_str().into(),
reference: hidden.reference().protocol_reference(),
scope_ref: None,
validator: None,
},
"hidden".into(),
@@ -868,7 +889,8 @@ fn descriptor_guided_decoder_runs_inside_canonical_resolution_boundary() {
validator: Some(b"object-v1".to_vec()),
},
InterfaceTarget {
reference: provider.reader.reference().as_str().into(),
reference: provider.reader.reference().protocol_reference(),
scope_ref: None,
validator: Some(b"schema-v1".to_vec()),
},
"add".into(),
@@ -1122,3 +1144,100 @@ fn execution_context_is_propagated_and_preflight_prevents_invocation() {
);
assert_eq!(provider.calls.get(), calls);
}
#[test]
fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
let (registry, provider, _) = registry_and_provider();
let legacy = provider.writer.reference();
let reference = legacy.protocol_reference();
assert_eq!(reference.scope, "/");
assert_eq!(reference.name, legacy.as_str());
// The registry's colon-delimited name remains opaque, not parsed.
assert!(reference.name.starts_with("wip-interface:"));
assert_eq!(registry.resolve(&reference).as_ref(), Some(legacy));
let nested = wip_protocol::InterfaceReference {
scope: "/open".into(),
name: reference.name.clone(),
};
assert!(registry.resolve(&nested).is_none());
assert_eq!(
registry.fetch_interface(&nested).unwrap_err().code,
ProtocolErrorCode::InterfaceNotFound
);
let malformed = wip_protocol::InterfaceReference {
scope: "relative".into(),
name: reference.name,
};
assert_eq!(
registry.fetch_interface(&malformed).unwrap_err().code,
ProtocolErrorCode::InvalidRequest
);
}
#[test]
fn call_reference_shape_precedes_resolution_and_scope_membership_is_exact() {
let (_, provider, _) = registry_and_provider();
for (scope, name) in [("relative", "valid"), ("/", ""), ("/bad/", "valid")] {
let mut call = request(
"/open",
None,
provider.writer.reference(),
None,
"label",
BTreeMap::new(),
);
call.interface.reference = wip_protocol::InterfaceReference {
scope: scope.into(),
name: name.into(),
};
assert_eq!(
provider.call_operation(call.clone()).unwrap_err().kind(),
CallErrorKind::InvalidRequest
);
assert_eq!(
provider
.call_operation_with_context_and_decoder(
&ExecutionContext::new(),
call.target,
call.interface,
call.operation,
|_| Ok::<_, ()>(BTreeMap::new()),
)
.unwrap_err()
.kind(),
CallErrorKind::InvalidRequest
);
}
assert_eq!(provider.resolutions.get(), 0);
// /other is outside the target scope; /open is self but has no legacy artifact.
for scope in ["/other", "/open"] {
let mut call = request(
"/open",
None,
provider.writer.reference(),
None,
"label",
BTreeMap::new(),
);
call.interface.reference.scope = scope.into();
assert_eq!(
provider.call_operation(call.clone()).unwrap_err().kind(),
CallErrorKind::InterfaceMismatch
);
assert_eq!(
provider
.call_operation_with_context_and_decoder(
&ExecutionContext::new(),
call.target,
call.interface,
call.operation,
|_| -> Result<BTreeMap<String, Value>, ()> { panic!("must not decode") },
)
.unwrap_err()
.kind(),
CallErrorKind::InterfaceMismatch
);
}
assert_eq!(provider.calls.get(), 0);
assert_eq!(provider.authorization_checks.get(), 0);
}