truefix-twsapi-client 0.1.4

Thin Interactive Brokers TWS/Gateway protocol client for TrueFix.
Documentation
use truefix_twsapi_client::comm;
use truefix_twsapi_client::constants::{UNSET_DOUBLE, UNSET_INTEGER};
use truefix_twsapi_client::message::{Outgoing, PROTOBUF_MSG_ID};
use truefix_twsapi_client::server_versions::{MAX_CLIENT_VER, MIN_CLIENT_VER};

#[test]
fn enhanced_handshake_matches_python_client_shape() {
    let bytes = comm::make_client_handshake(MIN_CLIENT_VER, MAX_CLIENT_VER, None);
    let expected_text = format!("v{MIN_CLIENT_VER}..{MAX_CLIENT_VER}");
    let expected_len = (expected_text.len() as u32).to_be_bytes();

    assert_eq!(&bytes[0..4], b"API\0");
    assert_eq!(&bytes[4..8], &expected_len);
    assert_eq!(&bytes[8..], expected_text.as_bytes());
}

#[test]
fn make_msg_encodes_text_message_id_until_protobuf_server_version() {
    let fields = format!(
        "{}{}",
        comm::make_field(1).unwrap(),
        comm::make_field(7).unwrap()
    );
    let msg = comm::make_msg(Outgoing::ReqIds.id(), false, &fields).unwrap();

    assert_eq!(&msg[0..4], 6_u32.to_be_bytes());
    assert_eq!(&msg[4..], &[b'8', 0, b'1', 0, b'7', 0]);
}

#[test]
fn make_msg_encodes_raw_int_message_id_for_newer_server_versions() {
    let fields = comm::make_field(1).unwrap();
    let msg = comm::make_msg(Outgoing::ReqPositions.id(), true, &fields).unwrap();

    assert_eq!(&msg[0..4], 6_u32.to_be_bytes());
    assert_eq!(&msg[4..8], &61_i32.to_be_bytes());
    assert_eq!(&msg[8..], b"1\0");
}

#[test]
fn make_msg_proto_prefixes_raw_message_id() {
    let msg = comm::make_msg_proto(Outgoing::ReqCurrentTime.protobuf_id(), b"abc");

    assert_eq!(&msg[0..4], 7_u32.to_be_bytes());
    assert_eq!(
        &msg[4..8],
        &(Outgoing::ReqCurrentTime.id() + PROTOBUF_MSG_ID).to_be_bytes()
    );
    assert_eq!(&msg[8..], b"abc");
}

#[test]
fn field_encoding_matches_python_bool_and_empty_sentinel_rules() {
    assert_eq!(comm::make_field(true).unwrap(), "1\0");
    assert_eq!(comm::make_field(false).unwrap(), "0\0");
    assert_eq!(comm::make_field_handle_empty(UNSET_INTEGER).unwrap(), "\0");
    assert_eq!(comm::make_field_handle_empty(UNSET_DOUBLE).unwrap(), "\0");
    assert_eq!(comm::make_field(100.0_f64).unwrap(), "100.0\0");
    assert_eq!(comm::make_field(-0.0_f64).unwrap(), "-0.0\0");
    assert_eq!(comm::make_field(f64::INFINITY).unwrap(), "inf\0");
    assert_eq!(
        comm::make_field_handle_empty(f64::INFINITY).unwrap(),
        "Infinity\0"
    );
}

#[test]
fn read_msg_and_fields_return_complete_frame_and_rest() {
    let mut buf = Vec::new();
    buf.extend_from_slice(&4_u32.to_be_bytes());
    buf.extend_from_slice(b"1\0x\0");
    buf.extend_from_slice(b"rest");

    let frame = comm::read_msg(&buf).unwrap().unwrap();
    assert_eq!(frame.size, 4);
    assert_eq!(frame.payload, b"1\0x\0");
    assert_eq!(frame.rest, b"rest");
    assert_eq!(
        comm::read_fields(frame.payload),
        vec![b"1".as_slice(), b"x".as_slice()]
    );
}

#[test]
fn read_msg_returns_none_for_partial_data_like_python_zero_size_tuple() {
    assert!(comm::read_msg(b"\0\0").unwrap().is_none());

    let mut partial = Vec::new();
    partial.extend_from_slice(&10_u32.to_be_bytes());
    partial.extend_from_slice(b"abc");
    assert!(comm::read_msg(&partial).unwrap().is_none());
}