Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Began the ADR-0029 network-adapter repartition: `oath-adapter-net-api` is now the
transport-neutral, **std-only** kernel (composition machinery + `ErrorKind` +
the new runtime-neutral `Timer` clock); the `Service` request/reply contract moved
into the new per-transport crate `oath-adapter-net-http-api`.
- Restructured the workspace to the process-aligned, spine-inverted crate topology
of ADR-0009: deleted `oath-engine` and `oath-ingest-core`; split
`oath-messaging-core` into `oath-bus-api` + `oath-event-log-api`; renamed the
Expand Down
49 changes: 4 additions & 45 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ resolver = "3"
members = [
"crates/model",
"crates/adapter/net/api",
"crates/adapter/net/http/api",
"crates/bus/api",
"crates/event-log/api",
"crates/persistence/api",
Expand Down Expand Up @@ -40,6 +41,7 @@ categories = ["finance", "asynchronous"]
# does not treat { workspace = true } path deps as wildcard requirements.
oath-model = { path = "crates/model", version = "0.1.0" }
oath-adapter-net-api = { path = "crates/adapter/net/api", version = "0.1.0" }
oath-adapter-net-http-api = { path = "crates/adapter/net/http/api", version = "0.1.0" }
oath-bus-api = { path = "crates/bus/api", version = "0.1.0" }
oath-event-log-api = { path = "crates/event-log/api", version = "0.1.0" }
oath-persistence-api = { path = "crates/persistence/api", version = "0.1.0" }
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Every subsystem is defined behind a trait. Backends, adapters, transports, and s
| `oath-core-kernel` | The `Kernel<R, E, P>` single-writer loop |
| `oath-core` | The Core process binary |
| `oath-adapter-api` | Harness + `Broker` / `DataProvider` traits for venue adapters |
| `oath-adapter-net-api` | HTTP/WS composition primitives (`Service`, `Layer`) for adapters |
| `oath-adapter-net-api` | Transport-neutral composition primitives (`Layer`, `ServiceBuilder`, `Stack`) + `ErrorKind` / `Timer` |
| `oath-adapter-net-http-api` | HTTP transport contract (`Service`, …) over the `oath-adapter-net-api` kernel |
| `oath-strategy-api` | User-facing `Strategy` trait and Signal ergonomics (the canonical `Signal` payload lives in `oath-model`, per ADR-0028) |
| `oath-strategy-host` | Strategy Node binary: hosts user strategies, isolated from Core |
| `oath-cli` | The first Frontend (MVP) |
Expand All @@ -41,6 +42,7 @@ graph TD
adapterapi[oath-adapter-api] --> model
stratapi[oath-strategy-api] --> model
netapi[oath-adapter-net-api]
nethttpapi[oath-adapter-net-http-api] --> netapi

risk[oath-core-risk] --> coreapi
risk --> model
Expand Down
11 changes: 0 additions & 11 deletions crates/adapter/net/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,3 @@ license.workspace = true

[lints]
workspace = true

[dependencies]
bytes = { workspace = true }
http = { workspace = true }
http-body = { workspace = true }
futures-core = { workspace = true }
futures-sink = { workspace = true }

# cargo-machete: deps declared ahead of first use; prune from `ignored` as they are adopted.
[package.metadata.cargo-machete]
ignored = ["bytes", "futures-core", "futures-sink", "http", "http-body"]
Original file line number Diff line number Diff line change
@@ -1,66 +1,34 @@
//! Core composition primitives: `Service`, `Layer`, `ServiceBuilder`, `Identity`, `Stack`.
//! Composition machinery: `Layer`, `ServiceBuilder`, `Identity`, `Stack`.
//!
//! These are the building blocks for the entire network stack. Every middleware
//! concern is expressed as a [`Layer`] that wraps any [`Service`], and
//! [`ServiceBuilder`] composes them at compile time with no virtual dispatch.
//! These compose **anything** — `Layer<S>` carries no `Service` bound (ADR-0029
//! §3), so the same machinery composes an HTTP `Service` stack today and a WS
//! subscription stack tomorrow.
//!
//! # Ordering invariant
//!
//! The **first** `.layer()` call is permanently the outermost wrapper and
//! therefore the first to handle each request. Subsequent calls are nested
//! progressively further inward.
//! therefore the first to handle each request.
//!
//! ```no_run
//! # use oath_adapter_net_api::service::{Layer, Service, ServiceBuilder};
//! # use std::future::Future;
//! ```
//! # use oath_adapter_net_api::compose::{Layer, ServiceBuilder};
//! # struct TracingLayer;
//! # struct MetricsLayer;
//! # struct Transport;
//! # impl<S> Layer<S> for TracingLayer { type Service = S; fn layer(&self, s: S) -> S { s } }
//! # impl<S> Layer<S> for MetricsLayer { type Service = S; fn layer(&self, s: S) -> S { s } }
//! # impl Service<()> for Transport {
//! # type Response = ();
//! # type Error = ();
//! # fn call(&self, _: ()) -> impl Future<Output = Result<(), ()>> + Send {
//! # async { Ok(()) }
//! # }
//! # }
//! // TracingLayer is added first → it is outermost → handles every request first.
//! let svc = ServiceBuilder::new()
//! .layer(TracingLayer) // outermost: first to see each request
//! .layer(MetricsLayer) // innermost of the two wrappers
//! .service(Transport); // leaf: performs actual I/O
//! // TracingLayer is added first → outermost → wraps everything else.
//! let _svc = ServiceBuilder::new()
//! .layer(TracingLayer) // outermost
//! .layer(MetricsLayer) // inner
//! .service(()); // leaf: any value (a `Service` leaf lives in net-http-api)
//! ```

use std::future::Future;

/// A single async call: request in, `Result` out.
///
/// Implementations must not require `&mut self` — services are shared across
/// tasks and must therefore be `Send + Sync`. Backpressure is handled inside
/// `call` (e.g. by awaiting a semaphore permit) rather than through a separate
/// `poll_ready`.
///
/// Use RPITIT for the return type — no `async-trait`, no `dyn`, no per-call
/// allocation.
pub trait Service<Req> {
/// The value produced on success.
type Response;

/// The error produced on failure.
type Error;

/// Drive the request to completion.
fn call(&self, req: Req) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}

/// Transform one [`Service`] into another [`Service`].
/// Wrap a value of type `S`, producing a new value that adds behaviour.
///
/// Typically a struct that holds configuration and owns an inner service. The
/// outer layer's [`Layer::layer`] method wraps the inner service, producing a
/// new [`Service`] that adds the layer's behaviour.
/// Typically a struct that holds configuration and owns an inner value. The
/// outer layer's [`Layer::layer`] method wraps the inner value, producing a
/// new value that adds the layer's behaviour.
pub trait Layer<S> {
/// The wrapped service type produced by this layer.
/// The wrapped type produced by this layer.
type Service;

/// Wrap `inner` with this layer's behaviour.
Expand All @@ -73,7 +41,7 @@ pub trait Layer<S> {
/// the outermost wrapper and therefore the first to execute on each request.
///
/// ```
/// # use oath_adapter_net_api::service::{Identity, ServiceBuilder};
/// # use oath_adapter_net_api::compose::{Identity, ServiceBuilder};
/// let _builder = ServiceBuilder::new(); // starts with Identity (no-op)
/// ```
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -111,9 +79,9 @@ impl<L> ServiceBuilder<L> {
}
}

/// Finalize the stack by wrapping a concrete service.
/// Finalize the stack by wrapping a concrete value.
///
/// Consumes the builder and returns the fully composed `Service` value.
/// Consumes the builder and returns the fully composed value.
/// The concrete type is fully resolved at compile time — no boxing, no
/// `dyn`.
pub fn service<S>(self, service: S) -> L::Service
Expand All @@ -124,7 +92,7 @@ impl<L> ServiceBuilder<L> {
}
}

/// The no-op layer — passes the inner service through unchanged.
/// The no-op layer — passes the inner value through unchanged.
///
/// `Identity` is the initial state of a fresh [`ServiceBuilder`].
#[derive(Debug, Clone, Copy)]
Expand All @@ -141,7 +109,7 @@ impl<S> Layer<S> for Identity {
/// Compose two [`Layer`] impls into one.
///
/// When assembling the stack, `Inner.layer(leaf)` is applied first, then
/// `Outer.layer(result)`. `Outer` is therefore the outermost service and the
/// `Outer.layer(result)`. `Outer` is therefore the outermost wrapper and the
/// first to handle each request.
///
/// Because [`ServiceBuilder::layer`] produces `Stack<New, L>` with `New` in
Expand Down
20 changes: 12 additions & 8 deletions crates/adapter/net/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
//! `oath-adapter-net-api` — composition primitives and capability trait contracts.
//! `oath-adapter-net-api` — transport-neutral composition primitives + contracts.
//!
//! This crate is **zero I/O, zero runtime**. It defines the shared
//! abstractions that every layer in the network stack depends on:
//! This crate is **std-only** (zero deps — the signal the ADR-0029 cut is
//! clean). It defines the shared abstractions every transport's layers depend
//! on:
//!
//! - [`service`] — `Service`, `Layer`, `ServiceBuilder`, `Identity`, `Stack`
//! - [`compose`] — `Layer`, `ServiceBuilder`, `Identity`, `Stack`
//! - [`error_kind`] — `ErrorKind`, `HasErrorKind`
//! - [`timer`] — `Timer`
//!
//! No `tokio`, `hyper`, `reqwest`, `serde`, or `thiserror` may appear in this
//! crate's dependency graph.
//! `Service` is **not** here — it is a per-transport contract in
//! `oath-adapter-net-http-api` (ADR-0029 §2).
#![forbid(unsafe_code)]

pub mod compose;
pub mod error_kind;
pub mod service;
pub mod timer;

pub use compose::{Identity, Layer, ServiceBuilder, Stack};
pub use error_kind::{ErrorKind, HasErrorKind};
pub use service::{Identity, Layer, Service, ServiceBuilder, Stack};
pub use timer::Timer;
44 changes: 44 additions & 0 deletions crates/adapter/net/api/src/timer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! The `Timer` clock contract — a runtime-neutral clock for timing layers.

use std::future::Future;
use std::time::{Duration, Instant};

/// A clock abstraction for timing layers, decoupled from any async runtime.
///
/// Timing middleware (`Timeout`, `Retry` backoff, `RateLimit` refill,
/// `CircuitBreaker` cooldown) is generic over `Timer` so a mock clock can drive
/// it deterministically in tests while production passes a runtime-backed impl.
/// A trait — not a runtime — so the kernel stays std-only (ADR-0029 §4).
pub trait Timer: Clone + Send + Sync {
/// Complete after `dur` has elapsed.
fn sleep(&self, dur: Duration) -> impl Future<Output = ()> + Send;

/// The current instant — for elapsed-time reads (token-bucket refill,
/// circuit cooldown).
fn now(&self) -> Instant;
}

#[cfg(test)]
mod tests {
use super::Timer;
use std::future::Future;
use std::time::{Duration, Instant};

#[derive(Clone)]
struct FixedTimer(Instant);

impl Timer for FixedTimer {
fn sleep(&self, _dur: Duration) -> impl Future<Output = ()> + Send {
std::future::ready(())
}
fn now(&self) -> Instant {
self.0
}
}

#[test]
fn now_returns_the_configured_instant() {
let t0 = Instant::now();
assert_eq!(FixedTimer(t0).now(), t0);
}
}
9 changes: 9 additions & 0 deletions crates/adapter/net/http/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "oath-adapter-net-http-api"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[lints]
workspace = true
11 changes: 11 additions & 0 deletions crates/adapter/net/http/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! `oath-adapter-net-http-api` — the HTTP transport contract over the kernel.
//!
//! Builds on `oath-adapter-net-api` (composition machinery + `ErrorKind` +
//! `Timer`) and adds the request/reply [`Service`] connection shape. The HTTP
//! data plane (`HttpError`, `HttpClient`, `ResponseBody`, the layers) lands in
//! later slices. No async runtime, `hyper`, `reqwest`, or `serde` here.
#![forbid(unsafe_code)]

pub mod service;

pub use service::Service;
34 changes: 34 additions & 0 deletions crates/adapter/net/http/api/src/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! The request/reply connection-shape contract for the HTTP transport.
//!
//! `Service` models **request → one reply** — it fits REST and unary RPC but
//! not WebSocket subscriptions or multicast, so it is a per-transport contract,
//! not a kernel primitive (ADR-0029 §2). It is transport-*neutral* (names no
//! HTTP type); it lives here, the first request/reply transport, until a second
//! one justifies hoisting it into a shared `net-req-reply-api` crate.

use std::future::Future;

/// A single async call: request in, `Result` out.
///
/// `call` takes `&self` (not `&mut self`) — a service is shared, not owned, by
/// its callers. The returned future is **`Send`** (enforced here) so it runs on
/// a multithreaded runtime. The service *value* is expected to be
/// `Send + Sync + 'static` too, but that is enforced at the **composition
/// boundary** (`stack()`'s return bound, ADR-0030), not on this trait — so a
/// service may be non-`Sync` in a context that never shares it. Backpressure is
/// handled inside `call` (e.g. awaiting a permit), not via a separate
/// `poll_ready`.
///
/// Because the `call` future borrows `&self`, it is **not** `'static`-spawnable:
/// to `tokio::spawn` a call, clone the (cheap, `Arc`-backed) service and move the
/// clone in. RPITIT return — no `async-trait`, no `dyn`, no per-call allocation.
pub trait Service<Req> {
/// The value produced on success.
type Response;

/// The error produced on failure.
type Error;

/// Drive the request to completion.
fn call(&self, req: Req) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}
Loading