Core types and JSON schema
The first real session on MapXr after the initial project setup. The goal was to define the complete data model that everything else would build on. Get this right and the rest follows naturally; get it wrong and every subsequent layer pays the price.
The type hierarchy
The core model is intentionally flat and serialization-first. Everything is designed to round-trip
cleanly through serde_json:
- TapCode: a 5-bit finger mask (one bit per finger, thumb to pinky) plus a device identifier. This is the raw event the BLE layer emits.
- Trigger: a single, double, or cross-device combo specification. Wraps one or two TapCodes with timing semantics.
- Action: what happens when a trigger fires. An enum covering key output, text typing, mouse, layer switching, and hold_modifier.
- Mapping: a (Trigger, Action) pair.
- Layer: a named list of Mappings.
- Profile: the top-level document: metadata, default layer name, and a map of Layer names to Layer definitions.
Design decisions
Why a 5-bit mask rather than named fingers? The TAP Strap hardware reports finger state as a bitmask. Keeping that representation in the type system means zero conversion overhead in the hot path (BLE event → engine lookup) and makes test fixtures trivial to write.
Why serde's adjacently-tagged enum representation? The JSON {"type": "key", "key": "ctrl+c"} shape is readable by humans editing profiles by
hand, and unambiguous for the parser. Internal enum tags would leak Rust naming conventions into
the user-facing format.
Test fixtures
Sample profile JSON files live in crates/mapping-core/tests/fixtures/ and are
loaded via include_str!() in tests. This tests the full deserialization path with
real files rather than inline strings, catching any mismatch between the schema spec and the
actual Rust types.