261 lines
13 KiB
Markdown
261 lines
13 KiB
Markdown
# wip-rs
|
|
|
|
Rust building blocks for the Web Interface Protocol (WIP).
|
|
|
|
The repository is a resolver-3 virtual Cargo workspace whose packages all live
|
|
under `crates/*`. It provides a transport-independent protocol model, the
|
|
standard framework-independent WIP over HTTPS v1 binding, a Rust
|
|
interface-authoring facade, a request-scoped host runtime, and a private
|
|
public-API conformance consumer. The repository root is not itself a package.
|
|
|
|
## Crate graph and responsibilities
|
|
|
|
```text
|
|
wip-public-api-example (workspace-only executable, integration tests, doctest)
|
|
├── wip-protocol logical declarations, values, calls, and validation
|
|
├── wip-https standard HTTPS v1 routes, JSON codecs, envelopes, and batching
|
|
│ └── wip-protocol
|
|
├── 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 `Value`s, validation, and stable protocol error categories. | Application DTOs, implementations, persistence, or wire encoding. |
|
|
| `wip-https` | Standard HTTPS v1 endpoint/route derivation, UTF-8 JSON codecs, direct success bodies, error/status classification, and retrieval batches. | HTTP I/O, TLS termination, authentication schemes, server frameworks, filesystem projection, or client presentation/cache policy. |
|
|
| `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 the protocol, authoring, and host 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 `wip-https`
|
|
maps standard routes/envelopes to `CallOperationRequest` and
|
|
`CallOperationResponse` without placing transport-specific types in descriptors
|
|
or application traits.
|
|
|
|
## End-to-end example
|
|
|
|
[`crates/wip-public-api-example/src/lib.rs`](crates/wip-public-api-example/src/lib.rs)
|
|
is the complete public-API example and
|
|
[`crates/wip-public-api-example/src/main.rs`](crates/wip-public-api-example/src/main.rs)
|
|
runs it. It demonstrates all of the following:
|
|
|
|
1. public application DTOs derived as `WipType` records, enums, and unions,
|
|
including canonical entry paths and schema-opaque JSON;
|
|
2. two public traits declared with `#[wip::interface]`;
|
|
3. a private host struct implementing both traits;
|
|
4. explicit publication of both interfaces as an ordered set on one object;
|
|
5. separate observation of opaque interface references, descriptors, and
|
|
descriptor validators;
|
|
6. calls qualified by `target.path`, object validator, interface reference and
|
|
validator, operation, and schema-neutral `Value` arguments;
|
|
7. validated result values decoded back to the typed application result;
|
|
8. independent dispatch of the same `status` operation name on both interfaces;
|
|
9. distinct domain rejection, rollback-confirmed and outcome-unknown
|
|
host/backend failures, `PermissionDenied`, stale validator recovery, interface
|
|
membership, and lookup failure codes; and
|
|
10. canonical failure precedence when several request fields are invalid.
|
|
|
|
Run it inside the development shell:
|
|
|
|
```console
|
|
nix develop
|
|
cargo run --package wip-public-api-example
|
|
```
|
|
|
|
The public-only conformance suite is in
|
|
[`crates/wip-public-api-example/tests/end_to_end.rs`](crates/wip-public-api-example/tests/end_to_end.rs).
|
|
It covers canonical object/descriptor shape, ordered interfaces,
|
|
schema-neutral record/enum/union/entry/JSON values, fully qualified calls, and
|
|
the distinct domain, host, authorization, validator-recovery, and interface
|
|
failure classes. Exhaustive public type construction and matching make legacy
|
|
ObjectId, descriptor `name`, and semantic `Value` variants compile-time
|
|
regressions. Unit tests in the public crates additionally cover every host
|
|
dispatch failure, `InvalidRequest` versus descriptor-bound `InvalidArguments`,
|
|
invalid local responses, validators, same-named operations, and exhaustive
|
|
per-interaction protocol error-code sets.
|
|
|
|
## Interface authoring
|
|
|
|
Applications depend on `wip-interface`, normally imported as `wip`:
|
|
|
|
```rust
|
|
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 `&self` receiver;
|
|
- 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:
|
|
|
|
```rust,ignore
|
|
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. A
|
|
structurally malformed call is `InvalidRequest`; only argument values checked
|
|
after the current Interface Descriptor is fixed are `InvalidArguments`. A Host
|
|
must never return a wire `InvalidResult`: Clients keep invalid Object,
|
|
Descriptor, operation-result, and error responses as local `InvalidResponse`
|
|
failures, while unsupported descriptor formats remain local
|
|
`UnsupportedDescriptorFormat` failures.
|
|
|
|
For operation calls, `Internal` and `ResourceLimitExceeded` guarantee that the
|
|
operation did not run or that its effects were rolled back.
|
|
`OperationOutcomeUnknown` is the only protocol code that makes no such
|
|
guarantee, so Clients must not automatically retry it. `wip::HostError`
|
|
therefore requires both a `HostErrorKind` and an `EffectGuarantee`; implementation
|
|
panic, result encoding, and result validation default to an unknown outcome.
|
|
Host-private diagnostics are bounded locally but never copied into protocol
|
|
messages.
|
|
|
|
`ExecutionContext` carries a runtime-neutral cancellation token, monotonic
|
|
deadline, and optional Host-defined resource budget through resolution and
|
|
execution. Cancellation and deadlines are cooperative: the runtime checks them
|
|
immediately before typed invocation, while providers enforce them during backend
|
|
work. A pre-invocation limit failure is `ResourceLimitExceeded`; after invocation
|
|
it is safe to use that code only when non-commit or rollback is confirmed.
|
|
|
|
Providers remain responsible for backend atomicity. `with_object` returns a typed
|
|
`ProviderResult`, and `execute_operation` receives an `OperationAttempt` that
|
|
separates `NotStarted` from `Started`. A transactional provider holds its
|
|
transaction across the callback, commits successful attempts, and may call
|
|
`OperationAttempt::confirm_rollback` only after every operation effect has been
|
|
rolled back. Without that proof, post-invocation failures remain
|
|
`OperationOutcomeUnknown`. Mutable operations must still make validator
|
|
comparison and backend commit atomic and return a validator describing the state
|
|
that becomes committed.
|
|
|
|
## Validation
|
|
|
|
Enter the dev shell and run the acceptance commands exactly:
|
|
|
|
```console
|
|
nix develop
|
|
cargo fmt --all -- --check
|
|
cargo check --workspace --all-targets
|
|
cargo test --workspace --all-targets
|
|
cargo test --workspace --doc
|
|
cargo clippy --workspace --all-targets -- -D warnings
|
|
RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps
|
|
cargo run --package wip-public-api-example
|
|
git diff --check
|
|
```
|
|
|
|
`cargo test --workspace --all-targets` includes the public-only conformance
|
|
consumer, runtime failure tests, and `trybuild` compile-fail suites. The separate
|
|
doctest, rustdoc, executable, and whitespace checks are all required acceptance
|
|
steps.
|