duklog Coding Standards
Style
- Iterators over loops, expressions over
return,matchoverif let - No
.unwrap()/.expect()in lib code (only tests andmain.rs) - Derive order: Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize
- Specific imports (no globs), grouped: std / external / crate-internal
- Each module has its own
thiserrorerror type; propagate with?
Domain Invariants
- Never construct domain structs with struct literal syntax from outside the module — always go through
::new()or a dedicated constructor that enforces validation. Struct literal construction in the storage layer silently bypasses invariants (e.g.,tx_count >= 1) that::new()enforces. - Loading from storage must enforce the same invariants as creation — if a constructor validates a field, the deserialization path must validate it too. Check for
ok_or_elseguards that catchNonebut silently accept invalidSome(0)or empty-string values.
Testing
- Every
pub fntested with success and failure paths - Assert on specific values, not
is_ok()/is_empty()— critical for mutation testing - Test boundary values (e.g., activation threshold at 9, 10, 11 QSOs)
- Use quickcheck aggressively — default to it for any new pure function, not just validators
- Every normalization/transform function needs an idempotency property:
fn foo_is_idempotent(s: String) -> bool - Every normalize → validate pipeline needs a roundtrip property (construct invalid-case-but-valid-structure input, normalize, assert validates)
- Add
if !s.is_ascii() { return true; }guard in quickcheck properties for ASCII-domain functions (callsigns, grid squares) to avoid Unicode expansion false failures - Use
tempfile::tempdir()for storage tests — never real paths - Deterministic and fast; no surviving mutants per module
- 90% minimum line coverage
- For storage deserialization: test corrupt-but-parseable inputs (e.g.,
tx_count: 0, empty section) — not just missing fields
Coverage Exclusions
- Allowed:
main.rssetup, event loop methods requiring a real terminal - Test
draw_*functions withTestBackendrender tests (not excluded from coverage) - Keep
#[mutants::skip]on draw functions (mutation testing visual layout isn't productive) - Never exclude:
src/model/,src/adif/,src/storage/,handle_key()methods
TUI: When to Extract a Dedicated Widget
Extract a dedicated widget (a new concrete struct implementing a trait) when either of these is true:
- Multiple instances of the same specialized input appear in one form — two RST fields on one screen is the clearest signal. Duplication of construction logic (
set_defaultcalled twice) is the code smell. - A generic type is carrying specialized behavior via flags or post-construction setters — if
FormFieldgrew aclear_on_first_inputflag that only applies to RST fields, that behavior belongs in a dedicatedRstFieldtype, even if there were only one RST field.
The general principle: extract a widget to encapsulate its own logic and behavior, not only when you have duplicates. A widget owns its invariants (defaults, reset semantics, mode-aware updates) and enforces them internally — callers should not need to know how it works.
A well-extracted widget:
- Is constructed with its domain constraints baked in (
RstField::new(label, default)) rather than applied post-construction - Has
reset()semantics appropriate to the input type (RST restores to default; text clears to empty) - Does not leak implementation details (no public flags, no caller-managed state)
TUI: draw_* Function Structure
- High-level
draw_*functions should read like prose — delegate formatting and row-building to small private helpers; the top-level function orchestrates, it does not inline per-cell logic - When a
matcharm inside adraw_*function contains more than ~3 lines of data transformation (e.g.,.iter().take(n).map(|qso| Row::new(vec![...]))with 5+ fields), extract a private helper:fn format_rst(qso: &Qso) -> Stringfor repeatedformat!("{}/{}", qso.rst_sent, qso.rst_rcvd)fn format_timestamp(qso: &Qso) -> Stringfor repeated.format("%H:%M").to_string()fn qso_to_row_general(qso: &Qso) -> Row/fn qso_to_row_pota(qso: &Qso) -> Rowper type — or a trait if the dispatch pattern warrants it
- Consider a
ToRecentRow/ similar trait with per-variant impls when the same "build a Row from a Qso" logic branches onQsoFormTypeacross more than one function
When Touching a File (Boy Scout Rule)
Before finishing any change, scan the files you modified for:
- Dead code — unused imports, unreachable branches, or stale comments in your diff
- Style violations — loops that should be iterators,
if letchains that should bematch, explicitreturnthat should be a tail expression, in the code you touched - Duplicated helpers — if the same logic appears twice in the same file, extract a shared function
- Oversized functions — if a function grew during this change, check whether it should be split
If you find something outside the direct scope of the task, fix it in the same PR as a clearly labeled "cleanup" commit. Do not defer cleanup to a future PR that may never come.
ADIF/POTA Correctness
- Field format:
<FIELDNAME:length>value(length = byte length) - Required:
STATION_CALLSIGN,CALL,QSO_DATE(YYYYMMDD),TIME_ON(HHMMSS),BAND,MODE - Park ref format:
[A-Z]{1,3}-\d{4,5} - Activation threshold: 10 QSOs, single park, one UTC day
Documentation
- Rustdoc (
///) on allpubitems - When adding
adif_str()/from_adif_str()methods, add a doc comment citing the spec source (e.g., the relevant file indocs/reference/) and explain the field name used in storage (e.g., "APP_DUKLOG_POWER") - Update
docs/when implementing or changing features:docs/user-guide.md— screen descriptions, keybindings, workflowsdocs/architecture.md— module layout, Action enum, design decisionsdocs/roadmap.md— move completed phases, update remaining workdocs/adif-format.md— if ADIF fields or format changes
- No feature is complete without documentation updates
Converted and distributed by TomeVault — claim your Tome and manage your conversions.