This guide covers advanced topics for extending the rust-ibapi functionality.
These examples demonstrate violations of principles in code-style.md.
// 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!
// ...
}
}// 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)?;// Bad: method does encoding, validation, and error handling
impl Client {
pub fn place_order(&self, order: &Order) -> Result<(), Error> {
// 100+ lines of mixed concerns
}
}// 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)
}
}// 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()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
Follow this pattern when creating new modules:
// 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,
}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).
Make sure the appropriate incoming message and outgoing message identifiers are defined in src/messages.rs.
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.
Create the common implementation that both sync and async will use:
// 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.
Add an impl Client block in the domain module's sync.rs:
// 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),
)
}
}Add an impl Client block in the domain module's async.rs:
// 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.
Create table-driven tests that work for both sync and async:
// 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
];Test your implementation in both sync and async modes:
# 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 asyncAdd 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.
The src/common/ directory contains shared utilities used by both sync and async implementations:
Provides validation and error handling utilities:
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
let valid_value = error_helpers::require_with(some_option, || {
"value must meet custom criteria".to_string()
})?;Provides common request patterns for both sync and async modes:
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)
})
}Handles connection reset scenarios:
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()
})?;