mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
Describe canonical local and CI verification, cache reuse, output assertions, workflow boundaries, and observed regression results without treating test counts as coverage targets.
5.2 KiB
5.2 KiB
Conventions
Design Philosophy
Schema-Driven Development
Define data types FIRST, then build logic around them. Types are the spec.
// ✅ Good: rich type with documented fields, integer precision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KseiHolding {
/// Ownership percentage in basis points: 41.10% → 4110.
pub percentage_bps: i64,
/// Total shares held (absolute count).
pub total_shares: i64,
}
// ❌ Bad: stringly-typed, float precision
pub struct Holding {
pub percentage: f64, // what unit? what precision?
pub shares: String, // why string?
}
Functional Approach
Parse/transform functions are pure: take input, return Result<T, IdxError>, no side effects.
// ✅ Good: pure function, testable in isolation
pub fn parse_id_number(s: &str) -> Result<i64, IdxError> { ... }
pub fn normalize_name(raw: &str) -> String { ... }
pub fn parse_quote_from_str(symbol: &str, raw: &str) -> Result<Quote, IdxError> { ... }
// ❌ Bad: function with side effects, hard to test
pub fn fetch_and_save_quote(symbol: &str) -> Result<(), IdxError> { ... }
Types Over Primitives
Use newtypes, enums, and rich structs. Avoid String where a domain type exists.
// ✅ Good
pub struct InvestorTypeCode(pub String);
pub enum Locality { Local, Foreign }
pub enum FlowSignal { Holder, Buyer, Seller, NewPosition, Exited }
// ❌ Bad
pub type InvestorType = String;
pub type Locality = String;
Naming
Files
types.rs— domain data types for a modulemod.rs— module declarations and re-exportsclient.rs— HTTP client codemap.rs/parse.rs— response mapping / parsing functionsraw_types.rs— raw API response shapes (before normalization)
Functions
parse_*— deserialize raw data into domain typesnormalize_*— clean/transform data (names, numbers, dates)resolve_*— lookup/match entitiesquery_*— read from databasefetch_*— HTTP requests to external APIsrender_*— output formatting (tables, JSON)handle— CLI command dispatch entry point
Types
*Raw/*RawRow— pre-normalization data (strings from API/PDF)*Holding— ownership fact row*Metrics— computed analytics*Row— display-ready composite type*Args— clap command arguments
Patterns
Provider Trait Pattern
pub trait QuoteProvider {
fn quote(&self, symbol: &str) -> Result<Quote, IdxError>;
}
// Factory function, not direct construction
pub fn default_provider(kind: ProviderKind) -> Box<dyn MarketDataProvider> { ... }
Parse Pipeline Pattern
// Raw API response → domain type, always via parse function
let raw: &str = &response_body;
let quote = yahoo::parse_quote_from_str("BBCA.JK", raw)?;
DB Function Pattern
// Take &Connection, caller manages lifetime. Use transactions for bulk.
pub fn insert_ksei_holdings(conn: &Connection, holdings: &[KseiHolding]) -> Result<usize, IdxError> { ... }
pub fn query_ticker_holdings(conn: &Connection, code: &str) -> Result<TickerOwnership, IdxError> { ... }
Error Propagation
// Map external errors to IdxError variants
let conn = Connection::open(path)
.map_err(|e| IdxError::DatabaseError(e.to_string()))?;
Integer Precision
- Percentages: basis points (i64).
41.10%→4110 - Shares: absolute count (i64). No floats.
- Prices: whole IDR (i64). Rounded from float at parse boundary.
- Money (USD): whole dollars (i64) for Bing data.
Testing
- Canonical verification:
nix flake check -Llocally and before pushing; CI uses the same gate. Do not repeat full Cargo build/lint/test runs after a successful flake check. - Focused debugging: use
nix develop --command cargo test <filter>when iterating on one failure. - Unit tests: retain precise parser, normalization, calculation, and boundary regressions; avoid pinning incidental wording, enum counts, or internal wiring.
- Integration tests: prefer CLI workflows, real temporary/in-memory SQLite, and deterministic local provider fixtures for user-visible contracts and state transitions.
- Output contracts: parse JSON from stdout on success and stderr on failure; errors must leave stdout empty. Assert meaningful values, types, and stable error codes rather than substrings or diagnostic wording.
- Stateful workflows: compare cached data across offline/failure transitions, and verify failed snapshot updates preserve both database bytes and query results. Keep packaged-binary smoke representative; put exhaustive boundaries in integration tests.
- Fixtures:
tests/fixtures/*.json— real API responses, sanitized - No live API calls in CI —
IDX_USE_MOCK_PROVIDER=1 - Test naming:
test_<function>_<scenario>(e.g.,test_parse_id_number_with_dots) - Coverage over counts: replacing redundant unit assertions with stronger integration scenarios must retain meaningful error, boundary, and data-integrity coverage.
Git / VCS
- jj (Jujutsu) as local workflow, colocated with git
- Push via
nix develop --command git push(for prek hooks) - Branch naming:
feat/<name>,fix/<name> - Commit messages: conventional commits (
feat:,fix:,refactor:,docs:)