idx-cli/docs/CONVENTIONS.md
hermes e78d9fcecd refactor(core): compile the mock provider only with a testing feature
Phase 3a of docs/DESIGN.md. MockProvider moves to api/mock.rs, and it,
the embedded MSN fixture bodies, and the IDX_MOCK_* hooks compile only
with idx-core's new `testing` feature. idx-cli enables it as a
dev-dependency, so `cargo test` keeps the fixture-backed provider while
release binaries no longer embed fixture data or honor
IDX_USE_MOCK_PROVIDER. Parser helpers used only by the mock are gated
the same way.

The packaged-cli flake check no longer relies on the mock; it checks
an offline cache miss (JSON CACHEMISS, exit 3) instead.

Goldens unchanged; clippy is clean with and without `testing`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:31:12 +00:00

5.3 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 module
  • mod.rs — module declarations and re-exports
  • client.rs — HTTP client code
  • map.rs / parse.rs — response mapping / parsing functions
  • raw_types.rs — raw API response shapes (before normalization)

Functions

  • parse_* — deserialize raw data into domain types
  • normalize_* — clean/transform data (names, numbers, dates)
  • resolve_* — lookup/match entities
  • query_* — read from database
  • fetch_* — HTTP requests to external APIs
  • render_* — 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 -L locally 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. Put behavioral coverage in tests/cli.rs, not shell smoke.
  • Fixtures: tests/fixtures/*.json — real API responses, sanitized
  • No live API calls in CI — IDX_USE_MOCK_PROVIDER=1 selects the fixture-backed mock provider, which exists only when idx-core's testing feature is on (idx-cli enables it as a dev-dependency, so cargo test has it and release builds do not)
  • 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

  • Push via nix develop --command git push (for prek hooks)
  • Branch naming: feat/<name>, fix/<name>, docs/<name>
  • Commit messages: conventional commits (feat:, fix:, refactor:, docs:)