Build: The Codebook Types
Maps to: Task 2. Kind: Build.
Objective
Create codes.rs in panoptes-core: the ActionType and StrategicLogic enums and the CodedRow struct. Write the test that is the whole reason we chose Rust — an invalid codebook value must fail to deserialize.
Scaffold
Create: crates/panoptes-core/src/codes.rs. Modify: crates/panoptes-core/src/lib.rs (declare and re-export the module).
Dependencies: no manifest changes — strum (for SCREAMING_SNAKE_CASE serialization) and serde_with (for NoneAsEmptyString on coder_notes) are already in core's Cargo.toml from the previous chapter.
Expected result: cargo test -p panoptes-core codes → 3 tests pass (valid_logic_parses, invalid_logic_is_rejected, coded_row_json_roundtrip).
The spec (givens)
ActionTypevariants:SensorRetask, Maneuver, Monitor, EscalateToCommand, RequestData, NoAction.StrategicLogicvariants:Control, Maritime, Political, Procedural, None, Mixed.- Both carry two attributes:
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]and#[serde(rename_all = "SCREAMING_SNAKE_CASE")]— wire form"SENSOR_RETASK","CONTROL"in both codecs — plus the same derive stack as the params enums. Unlikeparams.rs(where the strum and serde codecs never cross paths), the coded files are hand-filled with codebook strings and parsed by serde — so here the two codecs must agree, which means saying it to each codec separately. Omit the serde attribute andserde_json::from_str::<StrategicLogic>("\"CONTROL\"")fails withunknown variantCONTROL, expected one ofControl, …. CodedRowfields, in order:response_id: String,c1_action: ActionType,c2_logic: StrategicLogic,c2_anchor_quote: String,c3_escalation: u8,codebook_version: String,coder_notes: Option<String>— the last annotated#[serde_as(as = "NoneAsEmptyString")]under a#[serde_as]struct attribute.
Full code: Answer Key, Task 2.
Concepts exercised
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]to match codebook string conventions.serde_with::NoneAsEmptyStringfor optional free-text fields.- The negative test: asserting that a parse fails.
The build loop (you drive)
- Write three failing tests: a valid value parses; an invalid value (
"AGGRESSIVE") is rejected; a fullCodedRowround-trips through JSON. - Predict: the
invalid_logic_is_rejectedtest assertsresult.is_err(). Before implementing, what makes it currently fail — the assertion, or the fact that the type does not compile yet? - Run, check.
- Implement the enums and struct.
- Run green, commit.
invalid_logic_is_rejected is the load-bearing test of the entire harness. Everything about the instrument's integrity traces back to this one assertion passing. Write it deliberately.
Done when
cargo test -p panoptes-core codes is green, and the invalid-value test proves the codebook constraint lives in the type rather than in a separate check.