# Extending the API
This guide covers advanced topics for extending the rust-ibapi functionality.
## Anti-Patterns to Avoid
These examples demonstrate violations of principles in [code-style.md](code-style.md#design-principles).
### Duplicated Logic
```rust
// Bad: duplicated validation in sync and async
impl Client {
pub fn my_func(&self, param: &str) -> Result<Data, Error> {
if param.is_empty() { return Err(Error::InvalidParam); }
// ...
}
}
impl Client {
pub async fn my_func(&self, param: &str) -> Result<Data, Error> {
if param.is_empty() { return Err(Error::InvalidParam); } // duplicate!
// ...
}
}
```
```rust
// Good: shared validation in common/
pub(crate) fn validate_param(param: &str) -> Result<(), Error> {
if param.is_empty() { return Err(Error::InvalidParam); }
Ok(())
}
// Usage in sync.rs and async.rs
validate_param(param)?;
```
### Monolithic Functions
```rust
// Bad: method does encoding, validation, and error handling
impl Client {
pub fn place_order(&self, order: &Order) -> Result<(), Error> {
// 100+ lines of mixed concerns
}
}
```
```rust
// Good: split by responsibility
impl Client {
pub fn place_order(&self, order: &Order) -> Result<(), Error> {
validate_order(order)?;
let request = encode_order(order)?;
send_and_handle_response(self, request)
}
}
```
### Large Parameter Lists
```rust
// Bad: 4+ params signal need for builder.
fn create_order(action: Action, qty: f64, price: f64, tif: TimeInForce,
oca_group: String, oca_type: i32, cond: Option<Condition>) { }
// Good: fluent builder on Client.
client.order(&contract)
.buy(qty)
.limit(price)
.time_in_force(tif)
.oca_group(oca_group, oca_type)
.condition(cond)
.submit()
```
## Module Organization
Each API module follows a consistent structure to support both sync and async modes:
```
src/<module>/
├── mod.rs # Public types and module exports
├── common/ # Shared implementation details
│ ├── mod.rs # Export encoders/decoders
│ ├── encoders.rs # Message encoding functions
│ ├── decoders.rs # Message decoding functions
│ ├── test_tables.rs # Shared test cases (optional)
│ └── test_data.rs # Common test fixtures (optional)
├── sync.rs # Synchronous implementation
└── async.rs # Asynchronous implementation
```
## Module Structure Pattern
Follow this pattern when creating new modules:
```rust
// src/<module>/mod.rs
//! Module description
// Common implementation modules
mod common;
// Feature-specific implementations
#[cfg(feature = "sync")]
mod sync;
#[cfg(feature = "async")]
mod r#async;
// Public types - always available regardless of feature flags
#[derive(Debug)]
pub struct MyData {
pub field: String,
}
```
## Adding New API Functionality
### Step 1: Define Public Types and API Interface
Define data types in the module's `mod.rs` file - these should be available regardless of feature flags. The API is exposed as `impl Client` methods in the domain module's `sync.rs` / `async.rs` files (not in `client/sync.rs` or `client/async.rs`).
### Step 2: Ensure Message Identifiers Are Defined
Make sure the appropriate incoming message and outgoing message identifiers are defined in `src/messages.rs`.
### Step 3: Update Message Type to Request ID Map
When processing messages received from TWS, the request ID needs to be determined. A map of message type to request ID position is maintained in `src/messages.rs` and may need to be updated.
### Step 4: Implement Shared Business Logic
Create the common implementation that both sync and async will use:
```rust
// src/<module>/common/encoders.rs
pub(in crate::<module>) fn encode_my_request(request_id: i32, param: &str) -> Result<RequestMessage, Error> {
let mut message = RequestMessage::new();
message.push_field(&OutgoingMessages::MyRequest);
message.push_field(&request_id);
message.push_field(param);
Ok(message)
}
// src/<module>/common/decoders.rs
//
// Dispatch on wire format. On server_version >= PROTOBUF (201), TWS may send
// either text or protobuf for the same message type — text decoders run on a
// protobuf ResponseMessage will EOF on field 2, because the protobuf form
// carries only [msg_id_str] in `fields` and the payload lives in `raw_bytes`.
pub(in crate::<module>) fn decode_my_response(message: &mut ResponseMessage) -> Result<MyData, Error> {
message.decode_proto_or_text(decode_my_response_proto, |msg| {
msg.skip(); // message type
msg.skip(); // request id (if present in this message type)
Ok(MyData {
field: msg.next_string()?,
})
})
}
pub(crate) fn decode_my_response_proto(bytes: &[u8]) -> Result<MyData, Error> {
let p = crate::proto::MyResponse::decode(bytes)?;
Ok(MyData {
field: p.field.unwrap_or_default(),
})
}
```
For `StreamDecoder::decode`, end the match with `_ => Err(Error::UnexpectedResponse(message.clone()))`. `process_decode_result` skip-classifies `UnexpectedResponse`; `NotImplemented` or `Simple` terminate the subscription on any unknown message type — that's the bug class of issue #508.
### Step 5: Implement Sync Version
Add an `impl Client` block in the domain module's `sync.rs`:
```rust
// src/<module>/sync.rs
use super::common::{encoders, decoders};
use crate::common::request_helpers;
use crate::client::sync::Client;
impl Client {
pub fn my_function(&self, param: &str) -> Result<MyData, Error> {
request_helpers::blocking::one_shot_with_retry(
self,
OutgoingMessages::MyRequest,
|| encoders::encode_my_request(self.next_request_id(), param),
|message| decoders::decode_my_response(message),
|| Err(Error::UnexpectedEndOfStream),
)
}
}
```
### Step 6: Implement Async Version
Add an `impl Client` block in the domain module's `async.rs`:
```rust
// src/<module>/async.rs
use super::common::{encoders, decoders};
use crate::common::request_helpers;
use crate::Client;
impl Client {
pub async fn my_function(&self, param: &str) -> Result<MyData, Error> {
request_helpers::one_shot_with_retry(
self,
OutgoingMessages::MyRequest,
|| encoders::encode_my_request(self.next_request_id(), param),
|message| decoders::decode_my_response(message),
|| Err(Error::UnexpectedEndOfStream),
).await
}
}
```
No module re-exports needed — the `impl Client` methods are automatically available on the Client type.
### Step 8: Add Comprehensive Tests
Create table-driven tests that work for both sync and async:
```rust
// src/<module>/common/test_tables.rs
pub struct MyApiTestCase {
pub name: &'static str,
pub input: &'static str,
pub expected_result: ApiExpectedResult,
}
pub const MY_API_TESTS: &[MyApiTestCase] = &[
MyApiTestCase {
name: "valid request",
input: "test_input",
expected_result: ApiExpectedResult::Success { /* ... */ },
},
// ... more test cases
];
```
### Step 9: Verify Both Modes
Test your implementation in both sync and async modes:
```bash
# Test sync implementation
cargo test <module>::sync --features sync
cargo clippy --features sync
# Test async implementation
cargo test <module>::async --features async
cargo clippy --features async
```
### Step 10: Add Examples
Add examples showing the API usage to the examples folder:
- Sync examples: `examples/sync/my_feature.rs`
- Async examples: `examples/async/my_feature.rs`
Ensure examples are well-documented and demonstrate proper usage patterns.
## Common Utilities
The `src/common/` directory contains shared utilities used by both sync and async implementations:
### Error Helpers (`src/common/error_helpers.rs`)
Provides validation and error handling utilities:
```rust
use crate::common::error_helpers;
// Validate required parameters
let value = error_helpers::require(some_option, "parameter is required")?;
let request_id = error_helpers::require_request_id_for(request_id, "my operation")?;
// Validate ranges
let port = error_helpers::require_range(port, 1, 65535, "port")?;
// Validate with custom logic
})?;
```
### Request Helpers (`src/common/request_helpers.rs`)
Provides common request patterns for both sync and async modes:
```rust
use crate::common::request_helpers;
// For one-shot requests with retry logic (sync, inside impl Client)
pub fn my_api_call(&self) -> Result<MyData, Error> {
request_helpers::blocking::one_shot_with_retry(
self,
OutgoingMessages::MyRequest,
|| encode_my_request(self.next_request_id()),
|message| decode_my_response(message),
|| Err(Error::UnexpectedEndOfStream),
)
}
// For one-shot requests with retry logic (async, inside impl Client)
pub async fn my_api_call(&self) -> Result<MyData, Error> {
request_helpers::one_shot_with_retry(
self,
OutgoingMessages::MyRequest,
|| encode_my_request(self.next_request_id()),
|message| decode_my_response(message),
|| Err(Error::UnexpectedEndOfStream),
).await
}
// For requests with IDs and subscriptions (inside impl Client)
pub fn my_subscription(&self) -> Result<Subscription<MyData>, Error> {
request_helpers::blocking::request_with_id(self, Features::MY_FEATURE, |request_id| {
encode_my_request(request_id)
})
}
```
### Retry Logic (`src/common/retry.rs`)
Handles connection reset scenarios:
```rust
use crate::common::retry;
// Automatically retry on connection reset
let result = retry::retry_on_connection_reset(|| {
// Your operation that might fail due to connection reset
my_operation()
})?;
```