mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
The 43-case mock smoke duplicated tests/cli.rs, which already runs the real binary against fixture providers. Replace the flake's smoke-mock check with a small packaged-cli check (version JSON, bash completions, one mock quote), and cut scripts/live-smoke.sh from 672 to ~170 lines: it now only runs shipped commands against live MSN/Yahoo/IDX and validates output shape. Rewrite docs/SMOKE.md around the Nix gate and the drift check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.1 KiB
Markdown
131 lines
5.1 KiB
Markdown
# Conventions
|
|
|
|
## Design Philosophy
|
|
|
|
### Schema-Driven Development
|
|
Define data types FIRST, then build logic around them. Types are the spec.
|
|
|
|
```rust
|
|
// ✅ 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.
|
|
|
|
```rust
|
|
// ✅ 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.
|
|
|
|
```rust
|
|
// ✅ 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
|
|
```rust
|
|
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
|
|
```rust
|
|
// 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
|
|
```rust
|
|
// 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
|
|
```rust
|
|
// 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`
|
|
- **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:`)
|