# src/ - Library Core

## Module Architecture

```text
lib.rs
  pub mod auth          # OAuth2 PKCE, token storage, provider (standalone module)
  mod client            # Client struct, HTTP plumbing (private, re-exported)
  mod config            # Config builder (private, re-exported)
  mod error             # Error/Result types (private, re-exported)
  mod market_data_api   # impl Client: 11 market data methods
  mod trader_api        # impl Client: 13 trader methods
  mod models            # response/request types (private, re-exported via models::*)
  mod options           # query param builders (private, re-exported individually)
  mod order_builder     # OrderBuilder (private, re-exported)
  mod query             # internal query string helpers
  mod test_support      # #[cfg(test)] helpers
```

Root re-exports: `Client`, `Config`, `Error`, `Result`, `models::*`, all option builder types, `OrderBuilder`.

## Adding API Methods

Market data and trader endpoints follow the same pattern:

1. Add an `async fn` on `Client` in the appropriate `*_api.rs` file
2. Use `&self` receiver with `#[instrument(skip_all)]`
3. Build the URL with `self.endpoint_url(ApiBase::MarketData or Trader, "path")`
4. Use `self.send_json(&url)` for GET returning JSON, or `self.send_empty(...)` for POST/PUT/DELETE
5. Return `Result<T>` where `T` is a model type from `models/`

Example pattern from existing methods:

```rust
#[instrument(skip_all)]
pub async fn get_quote(&self, symbol: &str) -> Result<QuoteResponseObject> {
    let url = self.endpoint_url(ApiBase::MarketData, &format!("{symbol}/quotes"));
    self.send_json(&url).await
}
```

For methods with query parameters, build a `Vec<(&str, String)>` using helpers from `query.rs`, then append with `Url::query_pairs_mut().extend_pairs()`.

For POST/PUT with a JSON body, use `self.send_empty()` or `self.send_empty_with_location()` which accept a `reqwest::RequestBuilder`.

6. Add a `# Errors` doc section listing specific `Error` variants returned by the method
7. Use `crate::Error::VariantName` in rustdoc links (private modules cannot resolve bare `Error`)

## Client Internals (`client.rs`)

- `ApiBase` enum selects between market data and trader base URLs
- `endpoint_url()` joins the base URL with a relative path
- `send_json()` - GET request, deserialize response as JSON
- `send_empty()` - send request, return `()` on success
- `send_empty_with_location()` - send request, extract and return the `Location` header value
- All methods attach bearer token via `Authorization` header
- HTTP errors map to `Error::HttpStatus { code, body }` with body truncated/redacted in Debug

## Config (`config.rs`)

- `Config::new(bearer_token)` sets defaults for both API base URLs
- `base_url()` and `trader_base_url()` override the defaults
- `normalize_base_url()` ensures trailing slash removal and valid URL parsing
- `Debug` impl redacts `bearer_token` field

## Error Handling (`error.rs`)

- `Error` enum uses `thiserror` derive
- Variants: `HttpStatus`, `Reqwest`, `SerdeJson`, `UrlParse`, `Header`, `Io`, `Auth`, `Token`, `Tls`, `Redirect`
- Manual `Debug` impl on `Error`: redacts `body` field in `HttpStatus` to `[REDACTED]`
- `Result<T>` is `std::result::Result<T, Error>`
- Never expose raw HTTP response bodies in error messages or debug output

## Auth Module (`auth.rs`)

Largest module (1400+ lines). Handles the full OAuth2 PKCE flow:

- `AuthConfig` - credentials and OAuth settings
- `authorize_url()` - build the Schwab authorization URL with PKCE challenge
- `login()` / `start_login()` - spawn a local HTTPS callback server (self-signed TLS), open browser, exchange code
- `TokenStore` trait - async read/write interface for token persistence
- `FileTokenStore` - file-based with strict permissions (dir 0o700, file 0o600)
- `MemoryTokenStore` - in-memory for testing
- `Provider` - wraps a `Client` + `TokenStore`, auto-refreshes expired tokens
- Callback server restricted to `https://127.0.0.1` only

## Options Builders (`options.rs`)

Builder types for query parameter construction. Each has a `new()` constructor and chained setter methods:

- `QuoteOptions` - fields, indicative
- `OptionChainOptions` - symbol, contract type, strike count, strategy, range, dates, volatility, etc.
- `MoverOptions` - index, sort, frequency
- `PriceHistoryOptions` - symbol, period, frequency, date range, extended hours
- `OrderListOptions` - date range, max results, status, order filtering
- `TransactionListOptions` - account, date range, transaction type, symbol

All builders produce a `Vec<(&str, String)>` consumed by query string assembly.

## Order Builder (`order_builder.rs`)

`OrderBuilder` constructs equity order JSON payloads. Factory methods:

- `market_buy()`, `market_sell()` - market orders
- `limit_buy()`, `limit_sell()` - limit orders
- `stop_buy()`, `stop_sell()` - stop orders
- `stop_limit_buy()`, `stop_limit_sell()` - stop-limit orders

Produces a `serde_json::Value` via `build()`. Does NOT silently add fields or mutate the payload beyond what the caller specified.

## Query Helpers (`query.rs`)

Internal functions for building query parameter vectors:

- `required_text()` - adds a required string param
- `comma_separated_symbols()` / `comma_separated_symbols_required()` - join symbol lists
- `push_optional()` - conditionally add optional params

## Test Patterns

- Inline `#[cfg(test)] mod tests` blocks in each source file
- `mockito` for HTTP mocking: create a mock server, set expectations, verify request shape
- Tests verify: HTTP method, path, query params, headers, request body, response deserialization
- `test_support.rs` provides:
  - `n(value)` - convert numeric literals to `Number` (works for both f64 and Decimal)
  - `fixture(name)` - load JSON from `tests/fixtures/{name}.json`
- Live integration tests in `tests/integration.rs`, gated behind `#[cfg(feature = "test_online")]`
- Always run tests with both default features and `--features decimal`

## Keeping Documentation Current

When modules, public API methods, or internal patterns change, update this file and the root `AGENTS.md`. Also keep `README.md`, `.coderabbit.yaml`, and `.github/copilot-instructions.md` in sync with the actual code.
