Build: Workspace + Parameter Types
Maps to: Task 1 in the task plan. Kind: Build (you write the code).
Objective
Stand up the Cargo workspace and create panoptes-core, the crate that will hold every type crossing a boundary. Define the scenario parameter enums (TimePressure, Reversibility) and the Params struct, and prove with tests that they round-trip through strings and JSON.
Scaffold
Create (this is a new project directory, separate from this book's repo):
Cargo.toml— the workspace manifest. Declare every shared dependency for the whole course under[workspace.dependencies]now (full annotated list in the Workspace Scaffold appendix); later crates then just writedep = { workspace = true }.crates/panoptes-core/Cargo.toml— with[dependencies]:serde,serde_with,strum,chrono(all{ workspace = true }).crates/panoptes-core/src/lib.rs— module declarations + re-exports.crates/panoptes-core/src/params.rs— the types and their tests.
Dependencies this chapter actually exercises: serde (derives) and strum (Display, EnumString). Add serde_json under [dev-dependencies] for the JSON round-trip test.
Expected result: cargo test -p panoptes-core params → 2 tests pass (time_pressure_string_roundtrip, params_json_roundtrip).
The spec (design decisions — givens, not puzzles)
TimePressurehas variantsHours,Days;ReversibilityhasReversible,Irreversible.- Both enums carry
#[strum(serialize_all = "UPPERCASE")]— the string form is"HOURS","IRREVERSIBLE". This convention is a design decision (it becomes the manifest CSV's wire format), not something to infer. - Derives on both enums:
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString. Paramshas exactly four fields:attribution_confidence: u8,time_pressure: TimePressure,reversibility: Reversibility,info_request: bool. Derives:Debug, Clone, Copy, PartialEq, Serialize, Deserialize.
Full code: Answer Key, Task 1.
Concepts exercised
- Cargo workspaces and
[workspace.dependencies]for shared version pinning. #[derive(...)]stacking:Serialize, Deserialize, Debug, Clone, Copy, PartialEq.strum'sDisplay+EnumStringfor enum↔string conversion.
What "round-trip" means, concretely
Encode a value, decode the result, and assert you end up with exactly the value you started with — while also pinning what the encoded form looks like in between. A round-trip test proves the codec is lossless and freezes the wire format, so a refactor that silently changes "HOURS" to "Hours" fails a test instead of corrupting every downstream join. These strings become the manifest CSV and the JSONL log — the file contract.
The two tests exercise two independent codecs, and they encode the same enum differently:
| Direction | Expression | Expected |
|---|---|---|
enum → string (strum Display) | TimePressure::Hours.to_string() | "HOURS" |
string → enum (strum EnumString) | TimePressure::from_str("DAYS") | Ok(TimePressure::Days) |
| rejection | TimePressure::from_str("MINUTES") | Err(_) |
| struct → JSON (serde) | serde_json::to_string(&p) | {"attribution_confidence":60,"time_pressure":"Hours",…} |
| JSON → struct (serde) | serde_json::from_str::<Params>(&json) | a Params for which assert_eq!(p, back) holds |
#[strum(serialize_all = "UPPERCASE")] affects only Display/FromStr — so the string codec says "HOURS". serde independently serializes the variant name — so JSON says "Hours". That is fine: the manifest uses the strum codec in both directions, the JSONL log uses serde in both directions. But it is why each test must round-trip through its own codec, and why the enum→string direction is Display's job — not into()/try_into(), which have no implementation here and will not compile.
The build loop (you drive)
- Write the failing test for
TimePressurestring round-tripping (Hours↔"HOURS") and forParamsJSON round-tripping. Derive them from the behavior described, not from the answer key. - Predict the failure — will it fail to compile or fail at runtime? (Hint: the type does not exist yet. What does the compiler say about that?)
- Run, check the prediction.
- Implement the enums and struct with the minimal derives to pass.
- Run green, then commit.
Done when
cargo test -p panoptes-core params shows two passing tests, and you can explain why Copy is safe to derive on these types (all fields are themselves Copy).
Check yourself
Compare against Task 1 in the appendix task plan. If your derives differ, work out whether the difference matters — some are load-bearing (Deserialize), some are ergonomic (Copy).