Files
wip-rs/README.md

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.