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
+9
View File
@@ -9,6 +9,15 @@ This crate defines the canonical Object and Interface Descriptor models, the thr
wip-protocol = "0.1.0"
```
Interface identity is an exact `InterfaceReference { scope, name }` pair.
`scope` is a canonical absolute Worldspace path and `name` is nonempty Unicode.
Use `validate()` for shape, `validate_for_path()` for ancestor-or-self placement,
and Object/interaction validation at response boundaries. Fetching needs no
target Object path. Fetch responses and interface call targets carry optional
opaque string `scope_ref` metadata, not a third identity field. Descriptors do
not embed identity. See the repository's [migration guide](../../MIGRATION.md)
for the breaking Rust and HTTP wire changes and deferred consumer semantics.
See the [API documentation](https://docs.rs/wip-protocol) for the complete model and validation rules.
Licensed under either of Apache-2.0 or MIT at your option.
+16 -7
View File
@@ -143,8 +143,17 @@ pub enum ValidationErrorKind {
},
/// An object's ordered interface set contains a duplicate.
DuplicateInterface {
/// Duplicate opaque interface reference.
reference: String,
/// Duplicate structured interface reference.
reference: crate::InterfaceReference,
},
/// The local interface name is empty.
EmptyInterfaceName,
/// A valid scope is not an ancestor-or-self of the target path.
InterfaceScopeMismatch {
/// Selected scope path.
scope: String,
/// Target Object path.
path: String,
},
/// A string is not a canonical absolute Worldspace path.
InvalidPath {
@@ -175,10 +184,10 @@ pub enum ValidationErrorKind {
},
/// A response refers to a different interface than its request.
InterfaceResponseMismatch {
/// Requested opaque reference.
expected: String,
/// Returned opaque reference.
actual: String,
/// Requested structured reference (boxed to keep validation errors small).
expected: Box<crate::InterfaceReference>,
/// Returned structured reference (boxed to keep validation errors small).
actual: Box<crate::InterfaceReference>,
},
}
@@ -226,7 +235,7 @@ impl Error for ValidationError {}
pub enum ProtocolInteraction {
/// Observe one object and its indexable descendants to a requested depth.
Observe,
/// Fetch one interface descriptor by its opaque reference.
/// Fetch one interface descriptor by its structured reference.
FetchInterface,
/// Invoke one operation on a path-targeted object.
CallOperation,
+5 -4
View File
@@ -31,7 +31,7 @@
//! guarantee; Clients must not automatically retry it.
//!
//! Paths, object refs, interface refs, and validators remain separate concepts.
//! They are represented by ordinary protocol strings/bytes in the corresponding
//! Interface refs use [`InterfaceReference`]; other metadata use strings/bytes in their
//! fields. In particular, object refs are optional, operation targeting uses a
//! path, and this crate defines no interaction that fetches an object by ref.
//!
@@ -52,9 +52,10 @@ pub use error::{
};
pub use model::{
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
FetchInterfaceResponse, FieldDeclaration, InterfaceDescriptor, InterfaceTarget, Object,
ObjectObservation, ObserveRequest, ObserveResponse, OperationDeclaration, ParameterDeclaration,
ProtocolResult, ReturnDeclaration, Target, TypeDeclaration, TypeExpr, UnionCase, Value,
FetchInterfaceResponse, FieldDeclaration, InterfaceDescriptor, InterfaceReference,
InterfaceTarget, Object, ObjectObservation, ObserveRequest, ObserveResponse,
OperationDeclaration, ParameterDeclaration, ProtocolResult, ReturnDeclaration, Target,
TypeDeclaration, TypeExpr, UnionCase, Value,
};
/// Validates a canonical absolute Worldspace path.
+71 -11
View File
@@ -11,9 +11,49 @@ pub struct Documentation {
pub details: Option<String>,
}
/// An interface identified by an exact `(scope, name)` pair in one Worldspace.
///
/// Neither field is normalized, case-folded, URL-decoded, or split on display
/// delimiters. Descriptor identity is external to the descriptor itself.
///
/// ```
/// use wip_protocol::{FetchInterfaceRequest, InterfaceReference};
/// let reference = InterfaceReference {
/// scope: "/世::界/#".into(),
/// name: "read::書く#λ".into(),
/// };
/// reference.validate_for_path("/世::界/#/item")?;
/// // Fetching is independent of an Object target path.
/// FetchInterfaceRequest { interface: reference }.validate()?;
/// # Ok::<(), wip_protocol::ValidationError>(())
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InterfaceReference {
/// Canonical absolute Worldspace path of the interface's scope Object.
pub scope: String,
/// Nonempty Unicode local name, with no delimiter restrictions.
pub name: String,
}
impl InterfaceReference {
/// Validates reference shape without requiring a target Object path.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_interface_reference(self)
}
/// Validates shape and that scope is an ancestor-or-self of `path`.
///
/// This is independent of indexability. A Host maps a valid reference's
/// scope mismatch to `InterfaceMismatch` at the interface-selection stage,
/// not to the earlier shape-validation `InvalidRequest` stage.
pub fn validate_for_path(&self, path: &str) -> Result<(), ValidationError> {
crate::validation::validate_interface_scope(self, path)
}
}
/// The protocol projection currently published at one Worldspace path.
///
/// `name` is a path segment, `interfaces` is an ordered set of opaque interface
/// `name` is a path segment, `interfaces` is an ordered set of structured interface
/// references, `r#ref` is optional object identity, and `validator` is an
/// independent optional observation precondition. In particular, neither the
/// path nor `r#ref` is a mandatory object identifier.
@@ -23,8 +63,8 @@ pub struct Object {
pub name: String,
/// Short public description of the object.
pub description: Option<String>,
/// Opaque interface references in host publication order.
pub interfaces: Vec<String>,
/// Structured interface references in host publication order.
pub interfaces: Vec<InterfaceReference>,
/// Optional opaque identity within one client-facing Worldspace.
pub r#ref: Option<String>,
/// Optional opaque validator for the object's observed state.
@@ -239,18 +279,20 @@ pub struct ObjectObservation {
/// Successful `observe` response.
pub type ObserveResponse = ObjectObservation;
/// Request for the current descriptor of one opaque interface reference.
/// Request for the current descriptor of one structured interface reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchInterfaceRequest {
/// Opaque interface reference obtained from an object.
pub interface: String,
/// Structured interface reference obtained from an object.
pub interface: InterfaceReference,
}
/// Successful `fetch_interface` response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchInterfaceResponse {
/// Opaque interface reference corresponding to the request.
pub interface: String,
/// Structured interface reference corresponding to the request.
pub interface: InterfaceReference,
/// Optional opaque ref of the scope Object, not part of interface identity.
pub scope_ref: Option<String>,
/// Current validated descriptor.
pub descriptor: InterfaceDescriptor,
/// Optional opaque validator for this descriptor observation.
@@ -269,8 +311,10 @@ pub struct Target {
/// Interface portion of an operation target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceTarget {
/// Opaque interface reference selected from the object's interface set.
pub reference: String,
/// Structured reference selected from the object's interface set.
pub reference: InterfaceReference,
/// Optional scope Object ref copied from the same interface observation.
pub scope_ref: Option<String>,
/// Optional validator copied from the interface observation.
pub validator: Option<Vec<u8>>,
}
@@ -369,6 +413,13 @@ impl ObserveRequest {
}
}
impl FetchInterfaceRequest {
/// Validates reference shape; fetching an interface needs no target path.
pub fn validate(&self) -> Result<(), ValidationError> {
self.interface.validate()
}
}
impl FetchInterfaceResponse {
/// Validates request correspondence and descriptor semantics.
///
@@ -382,7 +433,16 @@ impl FetchInterfaceResponse {
}
impl CallOperationRequest {
/// Validates this call against the descriptor selected by `interface`.
/// Validates descriptor-independent request shape only.
///
/// Scope compatibility is deliberately deferred to interface selection so
/// that `NotFound` precedes `InterfaceMismatch` at the Host boundary.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_call_shape(self)
}
/// Validates this call's shape and descriptor-bound operation/arguments.
/// Scope compatibility and membership require Host publication context.
pub fn validate_with(&self, descriptor: &InterfaceDescriptor) -> Result<(), ValidationError> {
descriptor.validate_call(self)
}
+71 -9
View File
@@ -2,17 +2,22 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use crate::{
CallOperationRequest, FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor,
MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NameNamespace, Object, ObjectObservation, ObserveRequest,
PathSegment, TypeDeclaration, TypeExpr, UNION_CASE_FIELD, UNION_VALUE_FIELD, ValidationError,
ValidationErrorKind, Value, ValueKind,
InterfaceReference, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NameNamespace, Object,
ObjectObservation, ObserveRequest, PathSegment, TypeDeclaration, TypeExpr, UNION_CASE_FIELD,
UNION_VALUE_FIELD, ValidationError, ValidationErrorKind, Value, ValueKind,
};
pub(crate) fn validate_object(object: &Object) -> Result<(), ValidationError> {
validate_object_name(&object.name, true)?;
let mut seen = HashSet::new();
for interface in &object.interfaces {
if !seen.insert(interface.as_str()) {
for (index, interface) in object.interfaces.iter().enumerate() {
interface.validate().map_err(|error| {
error
.prepend(PathSegment::Index(index))
.prepend(PathSegment::Field("interfaces".into()))
})?;
if !seen.insert(interface) {
return Err(
ValidationError::new(ValidationErrorKind::DuplicateInterface {
reference: interface.clone(),
@@ -51,6 +56,14 @@ pub(crate) fn validate_object_at_path(object: &Object, path: &str) -> Result<(),
.next()
.expect("non-root path has a segment")
};
for (index, interface) in object.interfaces.iter().enumerate() {
interface.validate_for_path(path).map_err(|error| {
error
.prepend(PathSegment::Index(index))
.prepend(PathSegment::Field("interfaces".into()))
.prepend(PathSegment::Object(path.to_owned()))
})?;
}
if object.name == expected {
Ok(())
} else {
@@ -106,6 +119,54 @@ pub(crate) fn validate_path(path: &str) -> Result<(), ValidationError> {
}
}
pub(crate) fn validate_interface_reference(
reference: &InterfaceReference,
) -> Result<(), ValidationError> {
validate_path(&reference.scope)
.map_err(|error| error.prepend(PathSegment::Field("scope".into())))?;
if reference.name.is_empty() {
return Err(
ValidationError::new(ValidationErrorKind::EmptyInterfaceName)
.prepend(PathSegment::Field("name".into())),
);
}
Ok(())
}
pub(crate) fn validate_interface_scope(
reference: &InterfaceReference,
path: &str,
) -> Result<(), ValidationError> {
reference.validate()?;
validate_path(path)?;
let scope = reference.scope.as_str();
if scope == "/"
|| path == scope
|| path
.strip_prefix(scope)
.is_some_and(|rest| rest.starts_with('/'))
{
Ok(())
} else {
Err(ValidationError::new(
ValidationErrorKind::InterfaceScopeMismatch {
scope: reference.scope.clone(),
path: path.to_owned(),
},
))
}
}
pub(crate) fn validate_call_shape(request: &CallOperationRequest) -> Result<(), ValidationError> {
validate_path(&request.target.path)
.map_err(|error| error.prepend(PathSegment::Field("target".into())))?;
request.interface.reference.validate().map_err(|error| {
error
.prepend(PathSegment::Field("reference".into()))
.prepend(PathSegment::Field("interface".into()))
})
}
pub(crate) fn validate_descriptor(descriptor: &InterfaceDescriptor) -> Result<(), ValidationError> {
let declarations = declaration_map(descriptor)?;
@@ -582,8 +643,7 @@ pub(crate) fn validate_call(
descriptor: &InterfaceDescriptor,
request: &CallOperationRequest,
) -> Result<(), ValidationError> {
validate_path(&request.target.path)
.map_err(|error| error.prepend(PathSegment::Field("target".into())))?;
request.validate()?;
validate_arguments(descriptor, &request.operation, &request.arguments)
}
@@ -660,11 +720,13 @@ pub(crate) fn validate_fetch_interface_response(
response: &FetchInterfaceResponse,
request: &FetchInterfaceRequest,
) -> Result<(), ValidationError> {
request.validate()?;
response.interface.validate()?;
if response.interface != request.interface {
return Err(
ValidationError::new(ValidationErrorKind::InterfaceResponseMismatch {
expected: request.interface.clone(),
actual: response.interface.clone(),
expected: Box::new(request.interface.clone()),
actual: Box::new(response.interface.clone()),
})
.prepend(PathSegment::Field("interface".into())),
);
@@ -0,0 +1,219 @@
use std::collections::BTreeMap;
use wip_protocol::{
CallOperationRequest, FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor,
InterfaceReference, InterfaceTarget, Object, ObjectObservation, ObserveRequest,
OperationDeclaration, ReturnDeclaration, Target, TypeExpr, ValidationErrorKind,
};
fn reference(scope: &str, name: &str) -> InterfaceReference {
InterfaceReference {
scope: scope.into(),
name: name.into(),
}
}
fn object(path: &str, interfaces: Vec<InterfaceReference>) -> Object {
Object {
name: path.rsplit('/').next().unwrap().into(),
description: None,
interfaces,
r#ref: None,
validator: None,
}
}
fn descriptor() -> InterfaceDescriptor {
InterfaceDescriptor {
format: wip_protocol::INTERFACE_FORMAT_V1.into(),
documentation: None,
types: vec![],
operations: vec![OperationDeclaration {
name: "run".into(),
documentation: None,
parameters: vec![],
returns: ReturnDeclaration {
documentation: None,
r#type: TypeExpr::Unit,
},
}],
}
}
#[test]
fn scope_is_segment_based_ancestor_or_self_not_indexability() {
for (scope, path) in [
("/", "/"),
("/", "/a/b"),
("/a", "/a"),
("/a", "/a/b"),
("/a/b", "/a/b/c"),
] {
reference(scope, "read").validate_for_path(path).unwrap();
object(path, vec![reference(scope, "read")])
.validate_at_path(path)
.unwrap();
}
for (scope, path) in [
("/a", "/ab"),
("/a", "/ab/c"),
("/a/b", "/a/c"),
("/a/b", "/a"),
("/a", "/"),
] {
assert!(matches!(
reference(scope, "read")
.validate_for_path(path)
.unwrap_err()
.kind,
ValidationErrorKind::InterfaceScopeMismatch { .. }
));
// Without the placement context, the Object/reference shape is valid.
let object = object(path, vec![reference(scope, "read")]);
object.validate().unwrap();
assert!(matches!(
object.validate_at_path(path).unwrap_err().kind,
ValidationErrorKind::InterfaceScopeMismatch { .. }
));
}
}
#[test]
fn shape_validation_does_not_normalize_or_parse_fields() {
for scope in ["", "relative", "//a", "/a/", "/a//b", "/a/./b", "/a/../b"] {
let reference = reference(scope, "read");
assert!(matches!(
reference.validate().unwrap_err().kind,
ValidationErrorKind::InvalidPath { .. }
));
assert!(
FetchInterfaceRequest {
interface: reference.clone()
}
.validate()
.is_err()
);
assert!(object("/a", vec![reference]).validate().is_err());
}
assert_eq!(
reference("/", "").validate().unwrap_err().kind,
ValidationErrorKind::EmptyInterfaceName
);
for name in [".", "..", "a/b", "::", "#", "読み::書き#λ", "e\u{301}"] {
let reference = reference("/世::界/#/%2F", name);
reference.validate().unwrap();
assert_eq!(reference.name, name);
assert_eq!(reference.scope, "/世::界/#/%2F");
}
assert_ne!(reference("/", "é"), reference("/", "e\u{301}"));
assert_ne!(reference("/A", "read"), reference("/a", "read"));
assert_ne!(reference("/%61", "read"), reference("/a", "read"));
}
#[test]
fn duplicate_membership_and_correlation_use_complete_pair() {
let root = reference("/", "read");
let scoped = reference("/a", "read");
let mut object = object("/a", vec![root.clone(), scoped.clone()]);
object.validate_at_path("/a").unwrap();
assert!(object.interfaces.contains(&scoped));
assert!(!object.interfaces.contains(&reference("/b", "read")));
object.interfaces.push(root.clone());
assert_eq!(
object.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateInterface {
reference: root.clone()
}
);
let request = FetchInterfaceRequest { interface: root };
let mut response = FetchInterfaceResponse {
interface: scoped,
scope_ref: None,
descriptor: descriptor(),
validator: None,
};
assert!(matches!(
response.validate_for(&request).unwrap_err().kind,
ValidationErrorKind::InterfaceResponseMismatch { .. }
));
response.interface = request.interface.clone();
for scope_ref in [None, Some(String::new()), Some("opaque::世界#/%".into())] {
response.scope_ref = scope_ref;
response.validate_for(&request).unwrap();
}
response.interface.name.clear();
assert_eq!(
response.validate_for(&request).unwrap_err().kind,
ValidationErrorKind::EmptyInterfaceName
);
}
#[test]
fn observe_checks_scope_at_every_node_including_depth_boundary() {
let request = ObserveRequest {
path: "/a".into(),
depth: 1,
};
let mut response = ObjectObservation {
object: object("/a", vec![reference("/a", "read")]),
children: Some(vec![ObjectObservation {
object: object("/a/b", vec![reference("/a/b", "read")]),
children: None,
}]),
};
request.validate_response(&response).unwrap();
response.children.as_mut().unwrap()[0].object.interfaces[0].scope = "/a/c".into();
assert!(matches!(
request.validate_response(&response).unwrap_err().kind,
ValidationErrorKind::InterfaceScopeMismatch { .. }
));
}
#[test]
fn fetch_needs_no_target_path_and_call_separates_shape_from_scope_selection() {
let reference = reference("/some/unrelated/scope", "read");
let fetch = FetchInterfaceRequest {
interface: reference.clone(),
};
fetch.validate().unwrap();
FetchInterfaceResponse {
interface: reference.clone(),
scope_ref: None,
descriptor: descriptor(),
validator: None,
}
.validate_for(&fetch)
.unwrap();
let mut call = CallOperationRequest {
target: Target {
path: "/a".into(),
validator: None,
},
interface: InterfaceTarget {
reference,
scope_ref: Some("opaque".into()),
validator: None,
},
operation: "run".into(),
arguments: BTreeMap::new(),
};
call.validate().unwrap();
call.validate_with(&descriptor()).unwrap();
// This stage is Host InterfaceMismatch, after NotFound, not shape InvalidRequest.
assert!(matches!(
call.interface
.reference
.validate_for_path(&call.target.path)
.unwrap_err()
.kind,
ValidationErrorKind::InterfaceScopeMismatch { .. }
));
call.interface.reference.name.clear();
assert_eq!(
call.validate().unwrap_err().kind,
ValidationErrorKind::EmptyInterfaceName
);
assert_eq!(
call.validate_with(&descriptor()).unwrap_err().kind,
ValidationErrorKind::EmptyInterfaceName
);
}
+26 -13
View File
@@ -3,12 +3,19 @@ use std::collections::BTreeMap;
use wip_protocol::{
CallOperationRequest, CallOperationResponse, EnumCase, FetchInterfaceRequest,
FetchInterfaceResponse, FieldDeclaration, INTERFACE_FORMAT_V1, InterfaceDescriptor,
InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace, Object, ObjectObservation, ObserveRequest,
OperationDeclaration, ParameterDeclaration, PathSegment, ProtocolError, ProtocolErrorCode,
ProtocolInteraction, ProtocolResult, ReturnDeclaration, Target, TypeDeclaration, TypeExpr,
UnionCase, ValidationErrorKind, Value, ValueKind,
InterfaceReference, InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace, Object,
ObjectObservation, ObserveRequest, OperationDeclaration, ParameterDeclaration, PathSegment,
ProtocolError, ProtocolErrorCode, ProtocolInteraction, ProtocolResult, ReturnDeclaration,
Target, TypeDeclaration, TypeExpr, UnionCase, ValidationErrorKind, Value, ValueKind,
};
fn reference(scope: &str, name: &str) -> InterfaceReference {
InterfaceReference {
scope: scope.into(),
name: name.into(),
}
}
fn record(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
Value::Record(
entries
@@ -135,7 +142,7 @@ fn object(name: &str) -> Object {
Object {
name: name.into(),
description: Some(format!("Object {name}")),
interfaces: vec!["catalog-v1".into(), "search-v2".into()],
interfaces: vec![reference("/", "catalog-v1"), reference("/", "search-v2")],
r#ref: Some("opaque-object-ref".into()),
validator: Some(vec![1, 2, 3]),
}
@@ -146,16 +153,19 @@ fn object_fields_have_canonical_identity_and_ordered_set_semantics() {
let object = object("article");
object.validate().unwrap();
object.validate_at_path("/articles/article").unwrap();
assert_eq!(object.interfaces, ["catalog-v1", "search-v2"]);
assert_eq!(
object.interfaces,
[reference("/", "catalog-v1"), reference("/", "search-v2")]
);
let duplicate = Object {
interfaces: vec!["catalog-v1".into(), "catalog-v1".into()],
interfaces: vec![reference("/", "catalog-v1"), reference("/", "catalog-v1")],
..object.clone()
};
assert_eq!(
duplicate.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateInterface {
reference: "catalog-v1".into()
reference: reference("/", "catalog-v1")
}
);
@@ -574,7 +584,8 @@ fn calls_validate_canonical_paths_named_arguments_and_unit_results() {
validator: Some(vec![1]),
},
interface: InterfaceTarget {
reference: "catalog-v1".into(),
scope_ref: None,
reference: reference("/", "catalog-v1"),
validator: Some(vec![2]),
},
operation: "publish".into(),
@@ -659,17 +670,18 @@ fn observe_and_fetch_interface_dtos_validate_request_response_correspondence() {
}
let request = FetchInterfaceRequest {
interface: "catalog-v1".into(),
interface: reference("/", "catalog-v1"),
};
let response = FetchInterfaceResponse {
interface: "catalog-v1".into(),
scope_ref: None,
interface: reference("/", "catalog-v1"),
descriptor: descriptor(),
validator: Some(vec![9]),
};
response.validate_for(&request).unwrap();
assert!(matches!(
FetchInterfaceResponse {
interface: "other".into(),
interface: reference("/", "other"),
..response
}
.validate_for(&request)
@@ -847,7 +859,7 @@ fn invalid_success_payloads_remain_client_local_validation_failures() {
));
let interface_request = FetchInterfaceRequest {
interface: "catalog-v1".into(),
interface: reference("/", "catalog-v1"),
};
let mut invalid_descriptor = descriptor();
invalid_descriptor
@@ -856,6 +868,7 @@ fn invalid_success_payloads_remain_client_local_validation_failures() {
assert!(matches!(
FetchInterfaceResponse {
interface: interface_request.interface.clone(),
scope_ref: None,
descriptor: invalid_descriptor,
validator: None,
}