Build: Family Spec + Validity Trait
Maps to: Task 4. Kind: Build.
Objective
Create the panoptes-gen crate. Define FamilySpec (the TOML shape, loaded with the toml crate), the ScenarioFamily trait, and a first concrete family CaGeo whose is_valid encodes a real exclusion rule. Write the example ca_geo.toml.
Why TOML for the spec: it is the dialect you already speak (
Cargo.toml), thetomlcrate is what Cargo itself builds on, and TOML has no implicit typing — a level named"NO"can never silently becomefalsethe way it can in YAML. For an instrument whose configs are the experimental conditions, unambiguous scalars beat terseness.
Scaffold
Create (a whole new crate — remember to list it in the workspace members):
crates/panoptes-gen/Cargo.toml—[dependencies]:panoptes-core = { path = "../panoptes-core" }, plusserde,toml,tera,sha2,itertools,clap,anyhow(all{ workspace = true });[dev-dependencies]:serde_json.crates/panoptes-gen/src/lib.rs,src/family.rs(theFamilySpec+from_toml),src/validity.rs(theScenarioFamilytrait +CaGeo).scenarios/families/ca_geo.toml— the first family spec (content + metadata only).
Dependencies this chapter exercises: toml (spec parsing). tera/sha2/itertools sit unused until the next two chapters — declaring them now just saves manifest edits later.
Expected result: cargo test -p panoptes-gen family and cargo test -p panoptes-gen validity → 5 tests pass between them.
The spec (givens)
FamilySpecfields:name: String,version: u32,title: String,doctrine_refs: Vec<String>,action_menu: Vec<String>,template: String. Derives:Debug, Clone, Deserialize. Constructor:from_toml(s: &str) -> anyhow::Result<Self>.- The
ScenarioFamilytrait has exactly two methods:fn name(&self) -> &strandfn is_valid(&self, p: &Params) -> bool. CaGeo's exclusion rule: a combination is invalid whentime_pressureisHoursandinfo_requestisfalse(no time to act and no way to gather data).- The example
ca_geo.tomlcontent (family name, doctrine refs, action menu, template text) is data, not an exercise — copy it from the Answer Key.
Full code: Answer Key, Task 4.
Concepts exercised
tomldeserialization into a struct (same derive, different format).- Defining and implementing a trait.
- Keeping content (TOML) and logic (Rust) on opposite sides of the boundary.
The build loop (you drive)
- Write failing tests:
FamilySpec::from_tomlparses the fields;CaGeo::is_validrejects the excluded combo and accepts valid ones. - Predict: the TOML parse test — what happens if a required field is missing from the TOML? Compile error or runtime
Err? Why? - Run, check.
- Implement.
- Run green, commit.
Done when
cargo test -p panoptes-gen family and cargo test -p panoptes-gen validity pass. You can articulate why the same #[derive(Deserialize)] works for TOML and JSON alike (serde is format-agnostic; the derive describes the shape, the format crate handles the encoding).