# M1 task 02: `proto` identifiers and values **Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add validated identifiers, Hash32, Timestamp and DataClass to proto` ## Goal Add the small validated value types that every later type is built from. They are defined by `crates/proto/tests/ids.rs`, which you copy in and must not edit. ## Context From the spec: "Identifiers. Constructors and Deserialize validate; invalid input is an error, never a panic." A `SessionId` becomes a directory name, so it must never contain `/` or `.`. Every value has exactly one spelling in JSON, because log files are compared and hashed as bytes. ## Files - Copy: `crates/proto/tests/ids.rs` - Create: `crates/proto/src/ids.rs`, `crates/proto/src/class.rs` - Modify: `Cargo.toml`, `crates/proto/Cargo.toml`, `crates/proto/src/lib.rs`, `docs/dependencies.md`, `docs/implementer-log.md`, `Cargo.lock` (generated) ## Interfaces Produces, all re-exported from the crate root (`proto::SessionId` and so on): ```rust // crates/proto/src/ids.rs pub enum ValueError { SessionId, Hash32, Timestamp } // Debug, Clone, Copy, PartialEq, Eq; Display; std::error::Error pub struct SessionId(String); // 1 to 64 bytes, each one of a-z, 0-9 or '-' impl SessionId { pub fn new(s: &str) -> Result; // Err(ValueError::SessionId) pub fn as_str(&self) -> &str; } pub struct Epoch(pub u32); // JSON: a plain number pub struct CallId(pub u64); // JSON: a plain number pub struct Hash32([u8; 32]); // JSON: exactly 64 lowercase hex characters impl Hash32 { pub const ZERO: Hash32; // 32 zero bytes pub fn from_bytes(bytes: [u8; 32]) -> Self; pub fn as_bytes(&self) -> &[u8; 32]; pub fn to_hex(&self) -> String; pub fn from_hex(s: &str) -> Result; // Err(ValueError::Hash32); uppercase is an error } pub struct Timestamp(u64); // Unix milliseconds; JSON: "2026-09-17T08:05:00.000Z" impl Timestamp { pub fn from_unix_millis(ms: u64) -> Self; pub fn unix_millis(&self) -> u64; pub fn now() -> Self; pub fn to_rfc3339(&self) -> String; // always three decimals and a final Z pub fn parse(s: &str) -> Result; // Err(ValueError::Timestamp) } // crates/proto/src/class.rs pub enum DataClass { Public, Private, Secret } // JSON: "public", "private", "secret" ``` Derives: `SessionId` gets `Debug, Clone, PartialEq, Eq, Hash`. `Epoch`, `CallId` and `Timestamp` get `Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash`. `Hash32` gets `Debug, Clone, Copy, PartialEq, Eq, Hash`. `DataClass` gets `Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash`, with the variants in the order shown so that `Public < Private < Secret`. All of them get `Serialize, Deserialize`. Rules the tests check: - `Timestamp::parse` accepts only the spelling `to_rfc3339` produces. Parse the text, format the result again, and reject the input if the two strings differ. - Deserializing must validate. A JSON string that `new`, `from_hex` or `parse` would reject must fail to deserialize. ## API notes (verified on docs.rs, 2026-09-17) - serde container attributes: `#[serde(try_from = "String", into = "String")]` makes a type serialize through `From for String` and deserialize through `TryFrom for T`; the error type must implement `Display`. `into` requires `Clone`. `#[serde(transparent)]` makes a one-field struct serialize as that field. `#[serde(rename_all = "lowercase")]` on an enum. - `humantime::format_rfc3339_millis(t: std::time::SystemTime) -> humantime::Rfc3339Timestamp`, which implements `Display`. - `humantime::parse_rfc3339(s: &str) -> Result`. - `std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms)` gives a `SystemTime`; `t.duration_since(UNIX_EPOCH)` gives a `Result`; `Duration::as_millis()` is `u128`. ## Steps - [ ] **1. Copy the test and add the dependencies.** ```sh git switch m1 mkdir -p crates/proto/tests cp docs/plans/M1/files/crates/proto/tests/ids.rs crates/proto/tests/ ``` Add to `[workspace.dependencies]` in the root `Cargo.toml`: ```toml serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" humantime = "2.4.0" ``` Add to `[dependencies]` in `crates/proto/Cargo.toml`: ```toml serde.workspace = true serde_json.workspace = true humantime.workspace = true ``` Add these rows to the table in `docs/dependencies.md`: ```markdown | `serde` | 1.0.229 | `proto` | Derives serialization for every shared type. MIT OR Apache-2.0. | | `serde_json` | 1.0.151 | `proto` | JSON for frames and log files. MIT OR Apache-2.0. | | `humantime` | 2.4.0 | `proto` | RFC 3339 timestamps. No dependencies of its own. MIT OR Apache-2.0. | ``` - [ ] **2. See the test fail.** `cargo test -p proto --test ids`. Expected: it does not compile, because `proto::SessionId` and the other names do not exist. - [ ] **3. Write `class.rs` and `ids.rs`,** and in `lib.rs` add below the doc comment: ```rust pub mod class; pub mod ids; pub use class::DataClass; pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError}; ``` Write the hex encoding and decoding yourself; do not add a hex crate. - [ ] **4. See the test pass.** `cargo test -p proto --test ids`. Expected: `11 passed; 0 failed`. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** ```sh git add Cargo.toml Cargo.lock crates/proto docs/dependencies.md docs/implementer-log.md git commit ``` ## Done when - `cargo test -p proto --test ids` reports 11 passed. - `make gate` prints `gate: ok`. - `cmp crates/proto/tests/ids.rs docs/plans/M1/files/crates/proto/tests/ids.rs` prints nothing. ## Stop and report if - A test in `ids.rs` seems wrong to you. Do not edit it. - `humantime` does not have the two functions named above.