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
+255 -55
View File
@@ -1,3 +1,10 @@
fn root_reference(name: &str) -> wip_protocol::InterfaceReference {
wip_protocol::InterfaceReference {
scope: "/".into(),
name: name.into(),
}
}
use std::collections::BTreeMap;
use wip_client::{
@@ -65,7 +72,7 @@ fn object(name: &str, reference: Option<&str>, validator: &[u8]) -> Object {
Object {
name: name.into(),
description: Some(format!("object {name}")),
interfaces: vec!["math".into()],
interfaces: vec![root_reference("math")],
r#ref: reference.map(str::to_owned),
validator: Some(validator.to_vec()),
}
@@ -296,12 +303,18 @@ fn ensure_interface_deduplicates_loading_fresh_and_retained_failure() {
.unwrap();
let request = client
.ensure_interface(&session, "math")
.ensure_interface(&session, root_reference("math"))
.unwrap()
.expect("missing descriptor starts a fetch");
assert!(client.ensure_interface(&session, "math").unwrap().is_none());
assert!(
client
.ensure_interface(&session, root_reference("math"))
.unwrap()
.is_none()
);
let value = FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"math-v1".to_vec()),
};
@@ -310,7 +323,7 @@ fn ensure_interface_deduplicates_loading_fresh_and_retained_failure() {
request.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
},
&value,
wire_limits(),
@@ -318,10 +331,15 @@ fn ensure_interface_deduplicates_loading_fresh_and_retained_failure() {
.unwrap(),
)
.unwrap();
assert!(client.ensure_interface(&session, "math").unwrap().is_none());
assert!(
client
.ensure_interface(&session, root_reference("math"))
.unwrap()
.is_none()
);
let failed = client
.ensure_interface(&session, "broken")
.ensure_interface(&session, root_reference("broken"))
.unwrap()
.expect("new descriptor starts a fetch");
assert_eq!(
@@ -333,7 +351,7 @@ fn ensure_interface_deduplicates_loading_fresh_and_retained_failure() {
);
assert!(
client
.ensure_interface(&session, "broken")
.ensure_interface(&session, root_reference("broken"))
.unwrap()
.is_none(),
"retained interface failures require an explicit refresh"
@@ -348,7 +366,10 @@ fn ensure_interface_waits_for_request_and_cache_capacity_without_evicting() {
.unwrap();
let pending_object = client.refresh_object(&session, "/busy").unwrap();
assert!(
client.ensure_interface(&session, "math").unwrap().is_none(),
client
.ensure_interface(&session, root_reference("math"))
.unwrap()
.is_none(),
"lookahead waits while the in-flight bound is full"
);
client
@@ -359,11 +380,12 @@ fn ensure_interface_waits_for_request_and_cache_capacity_without_evicting() {
.unwrap();
let request = client
.ensure_interface(&session, "math")
.ensure_interface(&session, root_reference("math"))
.unwrap()
.expect("lookahead resumes after capacity becomes available");
let value = FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
};
@@ -372,7 +394,7 @@ fn ensure_interface_waits_for_request_and_cache_capacity_without_evicting() {
request.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
},
&value,
wire_limits(),
@@ -383,18 +405,32 @@ fn ensure_interface_waits_for_request_and_cache_capacity_without_evicting() {
assert!(
client
.ensure_interface(&session, "other")
.ensure_interface(&session, root_reference("other"))
.unwrap()
.is_none(),
"lookahead does not churn a full descriptor cache"
);
assert!(client.interface(&session, "math").is_some());
assert!(client.interface(&session, "other").is_none());
assert!(
client
.interface(&session, &root_reference("math"))
.is_some()
);
assert!(
client
.interface(&session, &root_reference("other"))
.is_none()
);
let explicit = client.prepare_interface(&session, "other").unwrap();
assert!(client.interface(&session, "math").is_none());
let explicit = client
.prepare_interface(&session, root_reference("other"))
.unwrap();
assert!(
client
.interface(&session, &root_reference("math"))
.is_none()
);
assert!(matches!(
client.interface(&session, "other").unwrap().state,
client.interface(&session, &root_reference("other")).unwrap().state,
ObservationState::Loading(id) if id == explicit.id
));
}
@@ -408,7 +444,9 @@ fn independent_retrievals_run_concurrently_and_complete_out_of_order() {
let first = client.refresh_observed(&session, "/one", 0).unwrap();
let second = client.refresh_observed(&session, "/two", 0).unwrap();
let interface = client.prepare_interface(&session, "math").unwrap();
let interface = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
assert_eq!(first.request.uri().path(), "/wip/v1/observe");
assert_eq!(second.request.uri().path(), "/wip/v1/observe");
@@ -426,10 +464,11 @@ fn independent_retrievals_run_concurrently_and_complete_out_of_order() {
interface.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
},
&FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"math-v1".to_vec()),
},
@@ -454,7 +493,10 @@ fn independent_retrievals_run_concurrently_and_complete_out_of_order() {
ObservationState::Fresh
);
assert_eq!(
client.interface(&session, "math").unwrap().state,
client
.interface(&session, &root_reference("math"))
.unwrap()
.state,
ObservationState::Fresh
);
assert_eq!(client.in_flight_len(), 0);
@@ -941,14 +983,21 @@ fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"interface-v1".to_vec()),
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(7))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(7),
)
.unwrap();
assert_eq!(
client
@@ -965,7 +1014,8 @@ fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::String),
validator: Some(b"interface-v2".to_vec()),
},
@@ -981,7 +1031,8 @@ fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
validator: Some(b"object-v1".to_vec()),
},
interface: wip_protocol::InterfaceTarget {
reference: "math".into(),
scope_ref: None,
reference: root_reference("math"),
validator: Some(b"interface-v1".to_vec()),
},
operation: "calculate".into(),
@@ -1015,7 +1066,10 @@ fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
);
assert!(matches!(history_context.outcome, CallOutcome::Success(_)));
assert_eq!(
client.interface(&session, "math").unwrap().descriptor,
client
.interface(&session, &root_reference("math"))
.unwrap()
.descriptor,
Some(descriptor(TypeExpr::String))
);
let observation = client.object(&session, "/item").unwrap();
@@ -1039,14 +1093,21 @@ fn successful_call_without_validator_preserves_observed_validator() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(1),
)
.unwrap();
let request = client
.pending_call_context(call.id)
@@ -1080,7 +1141,13 @@ fn successful_call_without_validator_preserves_observed_validator() {
assert_eq!(observation.validator, Some(b"object-v1".to_vec()));
let next = client
.prepare_call(&session, "/item", "math", "calculate", args(2))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(2),
)
.unwrap();
assert_eq!(
client
@@ -1109,14 +1176,21 @@ fn newer_observation_prevents_older_success_validator_overwrite() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(1),
)
.unwrap();
let request = client
.pending_call_context(call.id)
@@ -1173,7 +1247,8 @@ fn older_retrieval_cannot_overwrite_success_validator() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"interface-v1".to_vec()),
},
@@ -1181,7 +1256,13 @@ fn older_retrieval_cannot_overwrite_success_validator() {
let older_alias = client.refresh_observed(&session, "/alias/item", 0).unwrap();
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(7))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(7),
)
.unwrap();
let request = client
.pending_call_context(call.id)
@@ -1227,7 +1308,13 @@ fn older_retrieval_cannot_overwrite_success_validator() {
assert_eq!(alias.validator, Some(b"object-v2".to_vec()));
assert_eq!(
client
.prepare_call(&session, "/alias/item", "math", "calculate", args(1))
.prepare_call(
&session,
"/alias/item",
&root_reference("math"),
"calculate",
args(1)
)
.unwrap_err()
.kind,
ClientErrorKind::MissingObservation
@@ -1243,7 +1330,13 @@ fn older_retrieval_cannot_overwrite_success_validator() {
.refresh_observed(&session, "/second/item", 0)
.unwrap();
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(8))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(8),
)
.unwrap();
client.mark_dispatched(call.id).unwrap();
let unknown = ProtocolError {
@@ -1292,13 +1385,20 @@ fn in_flight_calls_pin_target_identity_until_staleness_is_applied() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"interface-v1".to_vec()),
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(7))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(7),
)
.unwrap();
client.mark_dispatched(call.id).unwrap();
@@ -1350,13 +1450,20 @@ fn validator_mismatch_stales_only_its_observation_and_never_retries() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"i1".to_vec()),
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(1),
)
.unwrap();
client.mark_dispatched(call.id).unwrap();
let error = ProtocolError {
@@ -1369,7 +1476,10 @@ fn validator_mismatch_stales_only_its_observation_and_never_retries() {
client.complete(call.id, response).unwrap();
assert_eq!(
client.interface(&session, "math").unwrap().state,
client
.interface(&session, &root_reference("math"))
.unwrap()
.state,
ObservationState::Stale
);
assert_eq!(
@@ -1384,7 +1494,8 @@ fn validator_mismatch_stales_only_its_observation_and_never_retries() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"i2".to_vec()),
},
@@ -1404,14 +1515,21 @@ fn protocol_and_transport_unknown_outcomes_stale_without_retry() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(1),
)
.unwrap();
client.mark_dispatched(call.id).unwrap();
let unknown = ProtocolError {
@@ -1447,7 +1565,13 @@ fn protocol_and_transport_unknown_outcomes_stale_without_retry() {
observe_object(&mut client, &session, "/item", object("item", None, b"v2"));
let second = client
.prepare_call(&session, "/item", "math", "calculate", args(2))
.prepare_call(
&session,
"/item",
&root_reference("math"),
"calculate",
args(2),
)
.unwrap();
client.mark_dispatched(second.id).unwrap();
let completion = client
@@ -1695,8 +1819,12 @@ fn superseded_transport_failure_does_not_poison_newer_refresh() {
)
.unwrap();
let old = client.prepare_interface(&session, "math").unwrap();
let newer = client.prepare_interface(&session, "math").unwrap();
let old = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
let newer = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
assert_eq!(
client
.fail_transport(old.id, "old connection failed")
@@ -1704,11 +1832,12 @@ fn superseded_transport_failure_does_not_poison_newer_refresh() {
Completion::StaleResponseRejected(old.id)
);
assert!(matches!(
client.interface(&session, "math").unwrap().state,
client.interface(&session, &root_reference("math")).unwrap().state,
ObservationState::Loading(id) if id == newer.id
));
let value = FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"current".to_vec()),
};
@@ -1717,7 +1846,7 @@ fn superseded_transport_failure_does_not_poison_newer_refresh() {
newer.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
},
&value,
wire_limits(),
@@ -1737,15 +1866,19 @@ fn validator_free_interface_observation_remains_immutable() {
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let refresh = client.prepare_interface(&session, "math").unwrap();
let refresh = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
let contradictory = FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"became-mutable".to_vec()),
};
@@ -1754,7 +1887,7 @@ fn validator_free_interface_observation_remains_immutable() {
refresh.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
},
&contradictory,
wire_limits(),
@@ -1763,7 +1896,13 @@ fn validator_free_interface_observation_remains_immutable() {
)
.unwrap_err();
assert_eq!(error.kind, ClientErrorKind::InvalidResponse);
assert_eq!(client.interface(&session, "math").unwrap().validator, None);
assert_eq!(
client
.interface(&session, &root_reference("math"))
.unwrap()
.validator,
None
);
}
#[test]
@@ -1876,3 +2015,64 @@ fn in_flight_and_history_are_bounded() {
.unwrap_err();
assert_eq!(client.diagnostics(&session).unwrap().len(), 1);
}
#[test]
fn structured_interface_keys_membership_and_scope_ref_are_preserved() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
let root = root_reference("same#::操作");
let local = wip_protocol::InterfaceReference {
scope: "/item".into(),
name: root.name.clone(),
};
let mut value = object("item", None, b"object-v1");
value.interfaces = vec![local.clone()];
observe_object(&mut client, &session, "/item", value);
for (reference, scope_ref) in [
(root.clone(), None),
(local.clone(), Some("scope-identity".into())),
] {
observe_interface(
&mut client,
&session,
FetchInterfaceResponse {
interface: reference,
descriptor: descriptor(TypeExpr::Integer),
scope_ref,
validator: None,
},
);
}
assert!(client.interface(&session, &root).is_some());
assert_eq!(
client
.interface(&session, &local)
.unwrap()
.scope_ref
.as_deref(),
Some("scope-identity")
);
assert_eq!(
client
.prepare_call(&session, "/item", &root, "calculate", args(1))
.unwrap_err()
.kind,
ClientErrorKind::InvalidRequest
);
let call = client
.prepare_call(&session, "/item", &local, "calculate", args(1))
.unwrap();
let context = client.pending_call_context(call.id).unwrap();
assert_eq!(context.request().interface.reference, local);
assert_eq!(
context.request().interface.scope_ref.as_deref(),
Some("scope-identity")
);
assert_eq!(
context.interface().scope_ref.as_deref(),
Some("scope-identity")
);
assert_eq!(client.interface(&session, &root).unwrap().scope_ref, None);
}