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
+2
View File
@@ -11,6 +11,8 @@ wip-client = "0.1.0"
Applications provide the HTTP adapter and presentation layer, send each prepared request exactly as owned by the runtime, then return the corresponding response for completion.
Interface acquisition and call APIs use `wip_protocol::InterfaceReference { scope, name }` directly. Interface caches and Object membership compare the complete structured value, so equal names in different scopes remain distinct. Interface observations retain the optional `scope_ref`, and calls copy it into `InterfaceTarget` unchanged. Scope-identity recovery is not implemented by this mechanical migration.
See the [API documentation](https://docs.rs/wip-client) for session, acquisition, cache, and call APIs.
Licensed under either of Apache-2.0 or MIT at your option.
+23 -15
View File
@@ -21,8 +21,8 @@ use wip_http::{
};
use wip_protocol::{
CallOperationRequest, CallOperationResponse, FetchInterfaceRequest, InterfaceDescriptor,
InterfaceTarget, Object, ObjectObservation as ProtocolObjectObservation, ObserveRequest,
OperationDeclaration, ProtocolError, ProtocolErrorCode, Target, Value,
InterfaceReference, InterfaceTarget, Object, ObjectObservation as ProtocolObjectObservation,
ObserveRequest, OperationDeclaration, ProtocolError, ProtocolErrorCode, Target, Value,
};
/// Monotonic identity assigned to one logical request.
@@ -226,12 +226,14 @@ pub struct TreeObservation {
/// Cached interface descriptor observation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceObservation {
/// Opaque interface reference.
pub reference: String,
/// Structured interface reference.
pub reference: InterfaceReference,
/// Last accepted descriptor, if one has been received.
pub descriptor: Option<InterfaceDescriptor>,
/// Optional interface validator. Absence makes the reference immutable.
pub validator: Option<Vec<u8>>,
/// Optional opaque identity of the interface scope Object.
pub scope_ref: Option<String>,
/// Current observation lifecycle.
pub state: ObservationState,
}
@@ -404,7 +406,7 @@ struct Session {
objects: BTreeMap<u64, ObjectRecord>,
refs: BTreeMap<String, u64>,
trees: BTreeMap<String, TreeObservation>,
interfaces: BTreeMap<String, InterfaceRecord>,
interfaces: BTreeMap<InterfaceReference, InterfaceRecord>,
calls: VecDeque<CallRecord>,
diagnostics: VecDeque<Diagnostic>,
next_object_key: u64,
@@ -596,7 +598,11 @@ impl Client {
/// Returns the current interface observation.
#[must_use]
pub fn interface(&self, session: &SessionId, reference: &str) -> Option<&InterfaceObservation> {
pub fn interface(
&self,
session: &SessionId,
reference: &InterfaceReference,
) -> Option<&InterfaceObservation> {
self.sessions
.get(session)?
.interfaces
@@ -762,9 +768,8 @@ impl Client {
pub fn ensure_interface(
&mut self,
session: &SessionId,
reference: impl Into<String>,
reference: InterfaceReference,
) -> Result<Option<PreparedRequest>, ClientError> {
let reference = reference.into();
let state = self.session(session)?;
if state.interfaces.contains_key(&reference)
|| state.interfaces.len() >= self.limits.interfaces_per_session
@@ -782,11 +787,11 @@ impl Client {
pub fn prepare_interface(
&mut self,
session: &SessionId,
reference: impl Into<String>,
reference: InterfaceReference,
) -> Result<PreparedRequest, ClientError> {
self.ensure_capacity()?;
let request = FetchInterfaceRequest {
interface: reference.into(),
interface: reference,
};
let endpoint = session_endpoint(session)?;
let encoded = encode_fetch_interface_request(&endpoint, &request, self.wire_limits)
@@ -804,6 +809,7 @@ impl Client {
.or_insert(InterfaceRecord {
observation: InterfaceObservation {
reference: request.interface.clone(),
scope_ref: None,
descriptor: None,
validator: None,
state: ObservationState::Loading(id),
@@ -833,7 +839,7 @@ impl Client {
&mut self,
session: &SessionId,
path: &str,
interface: &str,
interface: &InterfaceReference,
operation: impl Into<String>,
arguments: BTreeMap<String, Value>,
) -> Result<PreparedRequest, ClientError> {
@@ -900,7 +906,8 @@ impl Client {
validator: object.validator.clone(),
},
interface: InterfaceTarget {
reference: interface.to_owned(),
reference: interface.clone(),
scope_ref: interface_observation.scope_ref.clone(),
validator: interface_observation.validator.clone(),
},
operation,
@@ -1182,6 +1189,7 @@ impl Client {
let touched = state.tick();
let observation = InterfaceObservation {
reference: value.interface,
scope_ref: value.scope_ref,
descriptor: Some(value.descriptor),
validator: value.validator,
state: ObservationState::Fresh,
@@ -1466,7 +1474,7 @@ impl Client {
fn interface_request_is_current(
&self,
session: &SessionId,
reference: &str,
reference: &InterfaceReference,
id: RequestId,
) -> bool {
matches!(
@@ -1511,7 +1519,7 @@ impl Client {
#[derive(Debug)]
enum RetrievalSubject {
Observe(ObserveRequest),
Interface(String),
Interface(InterfaceReference),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1617,7 +1625,7 @@ fn set_observe_subject_state(
fn ensure_interface_slot(
state: &mut Session,
reference: &str,
reference: &InterfaceReference,
maximum: usize,
) -> Result<(), ClientError> {
if state.interfaces.contains_key(reference) || state.interfaces.len() < maximum {
+29 -13
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::{
@@ -39,7 +46,7 @@ fn object() -> Object {
Object {
name: "item".into(),
description: None,
interfaces: vec!["math".into()],
interfaces: vec![root_reference("math")],
r#ref: None,
validator: Some(b"object-v1".to_vec()),
}
@@ -72,7 +79,7 @@ fn malformed_json_unknown_fields_duplicates_and_invalid_success_are_invalid_resp
let cases: &[&[u8]] = &[
b"{not json",
br#"{"name":"item","interfaces":[],"unknown":true}"#,
br#"{"name":"item","interfaces":["math","math"]}"#,
br#"{"name":"item","interfaces":[{"scope":"/","name":"math"},{"scope":"/","name":"math"}]}"#,
br#"{"name":"wrong","interfaces":[]}"#,
];
for body in cases {
@@ -181,12 +188,15 @@ fn non_success_without_valid_envelope_remains_transport_failure() {
#[test]
fn unsupported_descriptor_format_is_distinct_from_invalid_response() {
let (mut client, session) = setup();
let request = client.prepare_interface(&session, "math").unwrap();
let request = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
let logical = FetchInterfaceRequest {
interface: "math".into(),
interface: root_reference("math"),
};
let value = FetchInterfaceResponse {
interface: "math".into(),
scope_ref: None,
interface: root_reference("math"),
descriptor: descriptor("future-format/2"),
validator: Some(b"v2".to_vec()),
};
@@ -216,16 +226,19 @@ fn malformed_call_success_after_dispatch_is_outcome_unknown_and_stales_object()
.unwrap(),
)
.unwrap();
let interface = client.prepare_interface(&session, "math").unwrap();
let interface = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
client
.complete(
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(INTERFACE_FORMAT_V1),
validator: None,
},
@@ -238,7 +251,7 @@ fn malformed_call_success_after_dispatch_is_outcome_unknown_and_stales_object()
.prepare_call(
&session,
"/item",
"math",
&root_reference("math"),
"calculate",
BTreeMap::from([("value".into(), Value::Integer(2))]),
)
@@ -293,16 +306,19 @@ fn plain_413_after_call_dispatch_is_unknown_transport_outcome() {
.unwrap(),
)
.unwrap();
let interface = client.prepare_interface(&session, "math").unwrap();
let interface = client
.prepare_interface(&session, root_reference("math"))
.unwrap();
client
.complete(
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(INTERFACE_FORMAT_V1),
validator: None,
},
@@ -315,7 +331,7 @@ fn plain_413_after_call_dispatch_is_unknown_transport_outcome() {
.prepare_call(
&session,
"/item",
"math",
&root_reference("math"),
"calculate",
BTreeMap::from([("value".into(), Value::Integer(2))]),
)
+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);
}