9.7 KiB
wip-rs
Rust building blocks for the Web Interface Protocol (WIP).
The repository currently provides a transport-independent protocol model, a Rust interface-authoring facade, and a request-scoped host runtime. HTTP and other transport bindings are intentionally outside the supported slice.
Crate graph and responsibilities
wip-public-api-example (workspace-only executable, integration tests, doctest)
├── wip-protocol logical declarations, values, calls, and validation
├── wip-interface application DTO codecs and interface authoring facade
│ ├── wip-protocol
│ └── wip-interface-macros proc-macro implementation detail
└── wip-host descriptor registry, publication, and qualified dispatch
├── wip-interface
└── wip-protocol
| Crate | Responsibility | Does not own |
|---|---|---|
wip-protocol |
Transport-independent descriptors, object/call/result types, logical Values, validation, and stable protocol error categories. |
Application DTOs, implementations, persistence, or wire encoding. |
wip-interface |
The public WipType/interface authoring API, Rust-to-protocol codecs, generated descriptors, and typed implementation dispatch. |
Object publication, backend lifetime, or transport policy. |
wip-interface-macros |
Procedural-macro expansion used by wip-interface. It is not an application dependency. |
A supported direct user API. |
wip-host |
Opaque interface registration, explicit object membership, validator checks, and interface-qualified dispatch against one host-owned instance. | Backend atomicity, global object lookup, or network serving. |
wip-public-api-example |
An independently compiled consumer of only the three public crates. Its DTOs and traits are application-owned; its implementation and backend handle remain host-private. | Reusable library API or a production server. |
The boundary is intentional: protocol types describe what can cross a
transport; application DTOs opt into that model with WipType; a host-private
implementation owns credentials, handles, and backend state; and a future
transport maps its own routes/envelopes to CallOperationRequest and
CallOperationResponse without placing transport-specific types in descriptors
or application traits.
End-to-end example
src/lib.rs is the complete public-API example and
src/main.rs runs it. It demonstrates all of the following:
- public application DTOs derived as
WipTyperecords and unions; - two public traits declared with
#[wip::interface]; - a private host struct implementing both traits;
- explicit publication of both interfaces on one object;
- registry observation and validation of generated descriptors;
- calls made with interface references, operation names, and
Valuearguments; - validated result values decoded back to the typed application result;
- independent dispatch of the same
statusoperation name on both interfaces; - a domain rejection returned as data and a backend failure returned as a host error.
Run it inside the development shell:
nix develop
cargo run --package wip-public-api-example
The normal integration test is in tests/end_to_end.rs.
Unit tests beside the example additionally cover every host dispatch failure,
invalid arguments and results, interface membership, validators, and
same-named operations.
Interface authoring
Applications depend on wip-interface, normally imported as wip:
use wip_interface as wip;
#[derive(wip::WipType)]
struct Message {
/// Message body.
text: String,
}
#[wip::interface]
pub trait Messenger {
/// Sends a message.
///
/// # Returns
/// Whether the application accepted the message.
fn send(
&self,
/// Message to send.
message: Message,
) -> wip::OperationResult<bool>;
}
The interface attribute and WipType derive are re-exported by the facade;
users do not import wip-interface-macros or generated private support symbols.
The generated MessengerDefinition constructs an InterfaceDescriptor,
decodes named arguments, encodes results, validates both boundaries, and
provides typed dispatch for any T: Messenger.
Supported Rust syntax
A supported interface is a public, non-generic trait containing methods with:
- exactly an immutable
&selfreceiver; - simple named, owned parameters;
- an explicit
wip::OperationResult<T>return; ()only as the application return when no value is produced; and- direct
Option<T>only for an omittable parameter.
WipType derives support named-field structs, all-unit enums, and unions whose
cases may be unit variants or variants containing exactly one unnamed payload.
The macros reject private,
generic, inherited, unsafe, auto, or associated-item traits; const, async,
unsafe, extern, variadic, generic, default-bodied, or non-&self methods;
borrowed/custom-lifetime values; tuple/unit/generic structs; Rust unions;
named-field/multi-field enum variants; explicit discriminants; nested Option; and
Option in returns, collections, entries, or union payloads. These diagnostics
are compile-fail tested under crates/wip-interface/tests/ui.
Rust type mapping
| Rust | Protocol |
|---|---|
() |
unit / Value::Unit |
bool |
boolean |
| integer primitives | JavaScript-lossless integer |
f32, f64 |
finite number |
String |
string |
wip::Bytes |
bytes |
wip::JsonValue |
JSON interpreted over the schema-neutral Value tree |
wip::Entry |
canonical absolute Worldspace path encoded as a string |
Vec<T> |
list |
named-field #[derive(wip::WipType)] struct |
named record |
all-unit #[derive(wip::WipType)] enum |
named enum encoded as a string |
| mixed unit/newtype or all-newtype enum | named union encoded as a discriminator record |
Option<T> means that a direct DTO field or operation parameter may be
omitted; it is not protocol null. JSON null remains available inside
wip::JsonValue.
Migration from the pre-canonical value API
The old generic wip::Entry<K, V> represented an application key/value pair
and is no longer available. Use wip::Entry::new("/canonical/path") for a
Worldspace entry path and place any associated application data in a record.
Entry encodes as Value::String and validates the canonical absolute path.
The semantic Value::Json, Value::Entry, Value::Enum, and Value::Union
variants have also been removed. JSON recursively uses Unit, primitive,
List, and Record values and rejects Bytes; enums use String; unions use
a Record containing $case and, for payload-bearing cases, value. Unit
operation results are represented explicitly as Value::Unit.
Rust doc comments become descriptor metadata. For an operation, a # Returns
section documents the return declaration; text before that section documents
the operation itself.
Host publication and dispatch
Applications use wip-host to register generated definitions and explicitly
choose the ordered interface set for each object publication:
let mut interfaces = wip_host::InterfaceRegistry::new();
let messenger = interfaces.register(MessengerDefinition, None)?;
let published = wip_host::PublishedObject::new(request_scoped_object)
.expose(&messenger)?;
let protocol_object = published.protocol_object();
expose compiles only when the object implements the generated definition's
interface trait. Implementing a trait does not expose it automatically, empty
interface sets are valid, insertion order is preserved, and duplicate
references are rejected. Registry references are opaque and remain outside the
descriptor; generated descriptors contain neither those references nor Rust
trait identity.
Dispatch is qualified by interface reference and operation name and targets a
canonical Worldspace path. An ObjectProvider validates that path before
resolution, maps malformed paths to InvalidRequest, then resolves a valid path
exactly once inside with_object; the host checks the supplied object validator,
current interface membership, interface validator, descriptor operation,
operation-specific object-validator policy, arguments, and authorization in
canonical precedence. Path and children remain provider resolution context and
are never projected by WipObject; host-private fields and backend handles are
also excluded.
Application domain failures belong in a derived result union and are returned
through Ok. wip::HostError is reserved for runtime/backend failures. Invalid
arguments are rejected before invocation; malformed encoded results are
reported as InvalidResult and never returned as application data. Validator,
membership, authorization, and result failures retain their distinct canonical
protocol error codes.
Providers remain responsible for backend atomicity. Enter a transaction, lock,
stable-handle scope, or conditional-update primitive before invoking the
with_object callback, and retain it until the callback returns. Mutable
operations must make validator comparison and backend commit atomic and return a
validator for the committed state. The runtime never validates one object and
then resolves a second snapshot, but it cannot make an arbitrary external
backend atomic by itself.
Validation
Enter the dev shell and run the acceptance commands exactly:
nix develop
cargo fmt --all -- --check
cargo check --workspace --all-targets
cargo test --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --all-targets includes the public example integration
test, runtime failure tests, and trybuild compile-fail suites. Run
cargo test --workspace --doc when validating the checked rustdoc examples
separately.