diff --git a/API.md b/API.md new file mode 100644 index 0000000..9fb8cc1 --- /dev/null +++ b/API.md @@ -0,0 +1,173 @@ +# Public API audit — pre-1.0 + +This document is the authoritative public-API manifest for every +crate promoted to 1.0 in this workspace. It is committed at the repo +root (the canonical `docs/` folder is local-only per +`.git/info/exclude`); when a crate is published to crates.io, the +audit captured here is what its 1.0.0 promises. + +Generated: 2026-05-06. + +Closes the audit pass from #42. The companion 1.0 work lives in #43 +(MSRV policy) and #44 (coordinated release). + +## How to reproduce + +```bash +cargo install cargo-public-api --locked +cargo install cargo-semver-checks --locked + +for crate in itch-protocol itch-tcp itch-soup itch-mold; do + cargo public-api -p "$crate" > "API-$crate.txt" + cargo semver-checks check-release -p "$crate" +done +``` + +`cargo public-api` prints the full public surface for one crate; +diff against this file across releases to catch unintended changes. +`cargo semver-checks check-release` validates that no change since +the last published version is breaking under SemVer. + +Both tools are wired into the `make pre-publish` target (see +`Makefile`). + +## `#[non_exhaustive]` policy + +Applied to every error / event enum so future variants ship in a +minor release without breaking downstream `match` arms. + +Concretely, all the following enums are `#[non_exhaustive]`: + +- `itch_protocol::ProtocolError` +- `itch_protocol::primitives::TimestampError` (added 2026-05-06) +- `itch_tcp::TransportError` +- `itch_soup::SoupError` +- `itch_soup::LoginRejectReason` (added 2026-05-06) +- `itch_mold::MoldError` +- `itch_mold::MoldEvent` +- `itch_replay::ReplayError` +- `itch_book::BookError` +- `itch_source::SourceError` +- `itch_source::impls::RingBufferSeqStoreError` +- `itch_source_redis::RedisSeqStoreError` +- `itch_source_postgres::PostgresSeqStoreError` +- `itch_source_kafka::KafkaSourceError` + +NOT applied to closed-set ITCH 5.0 enums — exhaustive `match` is +intentional for forward-incompatible protocol revisions: + +- `itch_protocol::Message` (the 20 ITCH 5.0 message kinds) +- `itch_protocol::EventCode` +- `itch_protocol::Side` +- `itch_protocol::CrossType` +- `itch_protocol::ImbalanceDirection` +- `itch_protocol::MarketCategory` +- `itch_protocol::FinancialStatus` +- `itch_protocol::Authenticity` +- `itch_protocol::YesNo` +- `itch_protocol::LuldTier` +- `itch_protocol::TradingState` +- `itch_protocol::RegShoAction` +- `itch_protocol::MarketMakerMode` +- `itch_protocol::MarketParticipantState` +- `itch_protocol::BreachedLevel` +- `itch_protocol::IpoReleaseQualifier` +- `itch_protocol::Printable` +- `itch_protocol::PriceVariation` +- `itch_protocol::RpiInterestFlag` + +A new ITCH revision lands as a new module (per ADR direction); +`Message` itself stays exhaustive so the codec catches every +new variant at compile time. + +`itch_soup::SoupPacket` is also exhaustive — it is the SoupBinTCP +3.00 wire shape; new SoupBinTCP versions land as a separate +module. + +`itch_replay::CaptureFormat` is exhaustive — application-level +flag covering Glimpse vs RawBodies; users select one explicitly. + +## Per-crate public surface (summarised) + +The full output of `cargo public-api -p ` is reproducible +from a clean `cargo build`; this section names the headline shapes +that downstream code is most likely to touch. + +### `itch-protocol` + +- 20 message DTOs (`SystemEvent`, `StockDirectory`, …, + `RetailPriceImprovement`) + `Message` enum + `Header`. +- 18 closed-set ASCII enums in `enums` (see `#[non_exhaustive]` + policy above). +- Primitive newtypes: `Stock`, `Mpid`, `Price4`, `Price8`, + `Timestamp`, `StockLocate`, `TrackingNumber`, `OrderReference`, + `MatchNumber`, `Shares`. Every newtype is + `#[repr(transparent)]` so future versions can rely on layout. +- `Encode` / `Decode` traits + `Message::encode` / `decode` + + `ProtocolError`. + +### `itch-tcp` + +- `ItchCodec` (length-prefix Decoder / Encoder). +- `ItchConnection` type alias. +- `connect`, `bind`, `accept`. +- `Server` (issue #18 onwards). +- `TransportError` (`#[non_exhaustive]`). + +### `itch-soup` + +- `SoupConnection` + `login` / `login_with_timeout` + + `SoupCredentials`. +- `ResilientSoupClient` + `ResilientSoupConfig`. +- `SoupServer` + `Authenticator` trait + `AllowAllAuthenticator` / + `StaticAuthenticator`. +- `HeartbeatConfig`, `OutboundHeartbeat`, `InboundHeartbeat`. +- `SoupCodec` + `SoupPacket` (10 wire packet types). +- `LoginRequest`, `LoginAccepted`, `LoginRejectReason` + (`#[non_exhaustive]`). +- `SoupError` (`#[non_exhaustive]`). +- `DEFAULT_LOGIN_TIMEOUT`, `DEFAULT_SEND_TIMEOUT`, + `DEFAULT_BACKOFF_MIN`, `DEFAULT_BACKOFF_MAX`, + `DEFAULT_RESILIENT_LOGIN_TIMEOUT`, `DEFAULT_BROADCAST_CAPACITY`, + `DEFAULT_SHUTDOWN_GRACE`, `DEFAULT_UNSEQUENCED_INBOX_CAPACITY`. + +### `itch-mold` + +- `MoldStream` + `MoldConfig` + `MoldEvent` (`#[non_exhaustive]`). +- `MoldPublisher`, `MoldRequestServer`. +- `MoldPacket` + `MoldPacketHeader` + `MessageBlock` (downstream + packet codec). +- Constants: `HEADER_LEN`, `MSG_COUNT_HEARTBEAT`, + `MSG_COUNT_END_OF_SESSION`, `MAX_BLOCK_LEN`, + `DEFAULT_PACKING_MTU`, `BLOCK_LEN_PREFIX`. +- `MoldError` (`#[non_exhaustive]`). +- `PendingOverflowPolicy` (issue #22). + +## Doc coverage + +`cargo doc --workspace --no-deps` is required to be zero-warning +under `RUSTDOCFLAGS=-D warnings` (CI gate). Every `pub` item +carries a `///` doc comment; every fallible `pub fn` carries an +`# Errors` section. The doc gate is enforced on every PR. + +## SemVer commitment (effective at 1.0.0) + +From the moment a crate ships its 1.0.0 release: + +- Adding a new `pub fn` / `pub const` / `pub struct` / `pub enum` + variant to a `#[non_exhaustive]` enum is **non-breaking**. +- Renaming, removing, or changing the signature of any item in + this manifest is **breaking** (major bump). +- Raising MSRV is **breaking-by-policy** (see `MSRV.md`) — a + one-minor-version deprecation window applies. +- Changing wire format (`Message::encode` / `decode` byte shape) + is **breaking**. + +These rules are mechanically validated on every release PR by +`cargo semver-checks check-release`. + +## References + +- ADR-0007 (independent crates). +- `MSRV.md` (MSRV policy and current floors). +- `CHANGELOG.md` per crate (per-release deltas). diff --git a/Cargo.lock b/Cargo.lock index 82818ca..55a3041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1086,7 +1086,6 @@ dependencies = [ name = "itch-source-kafka" version = "0.1.0" dependencies = [ - "async-trait", "futures", "itch-protocol", "itch-source", diff --git a/Makefile b/Makefile index 960fb4c..4091804 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,35 @@ fuzz-list: ## List the configured fuzz targets pre-push: fix fmt lint-fix test doc ## Canonical pre-push gate +# ---------- 1.0 release gates ---------- + +# Promoted crates that ship a 1.0 release. Update this list when +# adding or removing crates from the 1.0 promotion set (see #42). +RELEASE_CRATES ?= itch-protocol itch-tcp itch-soup itch-mold + +public-api: ## Print the full public API of every promoted crate (requires `cargo install cargo-public-api`) + @for c in $(RELEASE_CRATES); do \ + echo "==> $$c"; \ + $(CARGO) public-api -p $$c || exit $$?; \ + done + +semver-check: ## Validate no breaking change since the last published version of each promoted crate (requires `cargo install cargo-semver-checks`) + @for c in $(RELEASE_CRATES); do \ + echo "==> $$c"; \ + $(CARGO) semver-checks check-release -p $$c || exit $$?; \ + done + +check-msrv: ## Build + test every promoted crate against the pinned MSRV (requires the MSRV toolchain installed via `rustup toolchain install`) + @MSRV=$$(grep -E '^rust-version' crates/itch-protocol/Cargo.toml | head -1 | sed -E 's/.*"([0-9.]+)".*/\1/'); \ + if [ -z "$$MSRV" ]; then echo "could not detect MSRV from crates/itch-protocol/Cargo.toml"; exit 1; fi; \ + echo "MSRV: $$MSRV"; \ + for c in $(RELEASE_CRATES); do \ + echo "==> $$c (build)"; $(CARGO) +$$MSRV build -p $$c || exit $$?; \ + echo "==> $$c (test)"; $(CARGO) +$$MSRV test -p $$c || exit $$?; \ + done + +pre-publish: pre-push public-api semver-check check-msrv ## Full pre-publish gate (1.0 release-readiness) + # ---------- workflow helpers ---------- workflow-list: ## List GitHub Actions workflow runs @@ -116,4 +145,5 @@ clean: ## Cargo clean .PHONY: help build release test run fmt fmt-check lint lint-fix check fix \ doc coverage coverage-html bench bench-save bench-compare fuzz \ - pre-push workflow-list workflow-view clean + pre-push public-api semver-check check-msrv pre-publish \ + workflow-list workflow-view clean diff --git a/crates/itch-client/src/main.rs b/crates/itch-client/src/main.rs index 944aa80..7f95187 100644 --- a/crates/itch-client/src/main.rs +++ b/crates/itch-client/src/main.rs @@ -366,6 +366,10 @@ async fn run_soup(server: &str, username: &str, password: &str) -> ExitCode { let detail = match reason { LoginRejectReason::NotAuthorized => "not authorized", LoginRejectReason::SessionUnavailable => "session unavailable", + // `LoginRejectReason` is `#[non_exhaustive]` — + // forward-compat arm for future SoupBinTCP + // reject codes. + _ => "unknown reject code", }; error!(?reason, detail, "login rejected — terminal"); return ExitCode::from(6); diff --git a/crates/itch-protocol/CHANGELOG.md b/crates/itch-protocol/CHANGELOG.md index 9d797f8..22d35da 100644 --- a/crates/itch-protocol/CHANGELOG.md +++ b/crates/itch-protocol/CHANGELOG.md @@ -6,6 +6,15 @@ this project adheres to per-crate [SemVer](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed + +- **API audit for 1.0** (#42). Added `#[non_exhaustive]` to + `primitives::TimestampError` so future stricter validation can + add reject reasons in a minor release without breaking + downstream `match` arms. Closed-set ITCH 5.0 enums (`Message`, + `EventCode`, `Side`, `CrossType`, …) deliberately stay + exhaustive — see the workspace `API.md` for the full policy. + ### Added - **Property-based tests** (`tests/property.rs` + shared diff --git a/crates/itch-protocol/src/primitives.rs b/crates/itch-protocol/src/primitives.rs index 0a4618b..a43d6fa 100644 --- a/crates/itch-protocol/src/primitives.rs +++ b/crates/itch-protocol/src/primitives.rs @@ -146,6 +146,11 @@ impl Shares { pub struct Timestamp(u64); /// Reasons a `Timestamp::try_new` can reject the input. +/// +/// `#[non_exhaustive]` is intentional: future ITCH revisions or +/// stricter validation may add reject reasons; consumers must +/// match with a fallback arm. +#[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TimestampError { /// Value exceeds the u48 range `[0, 2^48 - 1]`. diff --git a/crates/itch-soup/CHANGELOG.md b/crates/itch-soup/CHANGELOG.md index 5f333ac..52867f9 100644 --- a/crates/itch-soup/CHANGELOG.md +++ b/crates/itch-soup/CHANGELOG.md @@ -6,6 +6,15 @@ this project adheres to per-crate [SemVer](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed + +- **API audit for 1.0** (#42). Added `#[non_exhaustive]` to + `LoginRejectReason` so NASDAQ's future SoupBinTCP reject codes + can ship in a minor release without breaking downstream `match` + arms. Updated `itch-client::run_soup` to add a fallback arm + ("unknown reject code"). See the workspace `API.md` for the + full enum-by-enum policy. + ### Added - **Criterion bench `soup_codec`** (issue #31): per-packet diff --git a/crates/itch-soup/src/lib.rs b/crates/itch-soup/src/lib.rs index 5970439..fdbcadb 100644 --- a/crates/itch-soup/src/lib.rs +++ b/crates/itch-soup/src/lib.rs @@ -226,6 +226,11 @@ pub enum SoupError { } /// Codes carried by a Login Rejected (`J`) packet. +/// +/// `#[non_exhaustive]` is intentional: NASDAQ may extend the +/// SoupBinTCP reject codes in a future revision; consumers must +/// match with a fallback arm to remain forward-compatible. +#[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LoginRejectReason { /// `A` — Not Authorized: the username / password combination