Plan M4a: gatewayd in 15 tasks, with skeletons and given tests

Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 19:05:44 -07:00
co-authored by Claude Opus 5.5
parent f38dc8d474
commit 0339dc13b2
69 changed files with 7790 additions and 3 deletions
@@ -0,0 +1,95 @@
//! Mattermost's REST calls, one connection per request, with the token in the `Authorization`
//! header. A 429 waits as asked; a 5xx is tried twice more; a 401 or 403 is `Auth` at once.
use std::time::{Duration, SystemTime};
use serde::Deserialize;
use crate::config::valid_id;
use crate::http::{self, Response, rate_limit_wait};
use crate::mm::{Me, MmError, Post, Since, json, since_list};
use crate::net::Connector;
use crate::secrets::Secret;
use crate::ws::conn::host_header;
/// The pause before trying a call again after a 5xx.
pub const RETRY_5XX: Duration = Duration::from_millis(500);
/// How many times a call is tried again after a 5xx, and waits after a 429.
pub const RETRIES: u32 = 2;
/// How much of an error body is kept for the message.
const BODY_KEPT: usize = 200;
pub struct Client {
connector: Connector,
token: Secret,
timeout: Duration,
}
#[derive(Deserialize)]
struct Channel {
id: String,
}
impl Client {
/// `timeout` bounds connecting and each read: no bytes for that long is an error.
pub fn new(connector: Connector, token: Secret, timeout: Duration) -> Client {
// Store the three.
todo!()
}
pub fn connector(&self) -> &Connector {
// The Connector.
todo!()
}
pub fn token(&self) -> &Secret {
// The Secret.
todo!()
}
fn once(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result<Response, MmError> {
// 1. `self.connector.connect(self.timeout)`, then `set_read_timeout(Some(self.timeout))`.
// 2. Headers: Authorization "Bearer <token>" (the only use of `expose`), Accept
// "application/json", and Content-Type "application/json" when there is a body. Host is
// `host_header(self.connector.server())`.
// 3. `http::request`. Every error on the way is Net("<method> <path>: <error>").
todo!()
}
/// One call, tried again as the module comment says; the body of a 2xx answer.
fn call(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result<Vec<u8>, MmError> {
// Loop over `once`: 2xx -> the body. 401 or 403 -> Auth(status) at once. 429 -> after
// RETRIES waits already, RateLimited(wait); else sleep `rate_limit_wait(&head,
// SystemTime::now())` and try again. 5xx -> up to RETRIES more tries, sleeping RETRY_5XX
// before each. Anything else -> Status(status, the first BODY_KEPT characters of the body,
// lossy UTF-8).
todo!()
}
/// `GET /api/v4/users/me`: who this token is.
pub fn me(&self) -> Result<Me, MmError> {
// GET /api/v4/users/me into Me; an id that is not `valid_id` or an empty username is Json.
todo!()
}
/// `POST /api/v4/posts`: a post in `channel`, in the thread of `root` (empty: top level).
pub fn create_post(&self, channel: &str, root: &str, message: &str) -> Result<Post, MmError> {
// POST /api/v4/posts with {"channel_id", "root_id", "message"}; the answer is a Post,
// `check`ed.
todo!()
}
/// `GET /api/v4/channels/{channel}/posts?since=<ms>`: the posts created after `since`.
pub fn posts_since(&self, channel: &str, since: i64) -> Result<Since, MmError> {
// A channel that is not `valid_id` is Json, and nothing is sent. Otherwise GET
// /api/v4/channels/<channel>/posts?since=<since>, through `since_list`.
todo!()
}
/// `POST /api/v4/channels/direct`: the id of the direct channel between two users.
pub fn direct_channel(&self, a: &str, b: &str) -> Result<String, MmError> {
// POST /api/v4/channels/direct with the JSON array [a, b]; the answer has an "id", which
// must be `valid_id` (else Json).
todo!()
}
}