Skip to main content

ib_flex/
version.rs

1//! FLEX schema version detection
2
3use crate::error::{ParseError, Result};
4use crate::StatementType;
5
6/// FLEX schema versions supported by this library
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FlexSchemaVersion {
9    /// FLEX schema version 3 (current)
10    V3,
11    /// Unknown or unspecified version (treated as V3)
12    Unknown,
13}
14
15/// Detect FLEX schema version from XML
16///
17/// Examines the XML to identify the FLEX schema version attribute.
18/// If no version is specified or cannot be detected, defaults to V3.
19///
20/// # Arguments
21///
22/// * `xml` - XML string from IB FLEX query
23///
24/// # Returns
25///
26/// * `Ok(FlexSchemaVersion)` - Detected schema version (or Unknown if undetectable)
27///
28/// # Errors
29///
30/// Returns `ParseError` if the XML cannot be parsed or is malformed.
31///
32/// # Example
33///
34/// ```rust
35/// use ib_flex::version::detect_version;
36///
37/// let xml = r#"<FlexQueryResponse queryName="test" type="AF" version="3">"#;
38/// let version = detect_version(xml).unwrap();
39/// ```
40pub fn detect_version(xml: &str) -> Result<FlexSchemaVersion> {
41    // Look for version attribute in FlexQueryResponse or FlexStatement
42    if let Some(pos) = xml.find("version=\"") {
43        let version_start = pos + 9; // length of "version=\""
44        if let Some(version_end) = xml[version_start..].find('"') {
45            let version_str = &xml[version_start..version_start + version_end];
46            return match version_str {
47                "3" => Ok(FlexSchemaVersion::V3),
48                _ => Ok(FlexSchemaVersion::Unknown),
49            };
50        }
51    }
52
53    // If no version attribute found, assume V3 (most common)
54    Ok(FlexSchemaVersion::V3)
55}
56
57/// Detect FLEX statement type from XML
58///
59/// Examines the XML structure to determine whether it's an Activity FLEX
60/// or Trade Confirmation FLEX statement by looking at the root element.
61///
62/// # Arguments
63///
64/// * `xml` - XML string from IB FLEX query
65///
66/// # Returns
67///
68/// * `Ok(StatementType)` - Detected statement type
69/// * `Err(ParseError)` - If type cannot be determined
70///
71/// # Example
72///
73/// ```rust
74/// use ib_flex::{detect_statement_type, StatementType};
75///
76/// let xml = r#"<FlexQueryResponse><FlexStatements><FlexStatement ... /></FlexStatements></FlexQueryResponse>"#;
77/// let stmt_type = detect_statement_type(xml).unwrap();
78/// assert_eq!(stmt_type, StatementType::Activity);
79/// ```
80pub fn detect_statement_type(xml: &str) -> Result<StatementType> {
81    // Remove XML declaration and whitespace for easier parsing
82    let xml_trimmed = xml
83        .trim_start()
84        .trim_start_matches("<?xml")
85        .trim_start()
86        .trim_start_matches(|c: char| c != '<');
87
88    // Check root element
89    if xml_trimmed.starts_with("<FlexQueryResponse") {
90        // Activity FLEX uses FlexQueryResponse wrapper
91        Ok(StatementType::Activity)
92    } else if xml_trimmed.starts_with("<TradeConfirmationStatement") {
93        // Trade Confirmation uses TradeConfirmationStatement
94        Ok(StatementType::TradeConfirmation)
95    } else if xml_trimmed.starts_with("<FlexStatement") {
96        // Direct FlexStatement is Activity FLEX
97        Ok(StatementType::Activity)
98    } else {
99        Err(ParseError::XmlError {
100            message: format!(
101                "Cannot detect statement type from XML root element: {}",
102                &xml_trimmed.chars().take(100).collect::<String>()
103            ),
104            location: None,
105        })
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_detect_version_v3() {
115        let xml = r#"<FlexQueryResponse version="3" queryName="test">"#;
116        let version = detect_version(xml).unwrap();
117        assert_eq!(version, FlexSchemaVersion::V3);
118    }
119
120    #[test]
121    fn test_detect_version_no_attribute() {
122        let xml = r#"<FlexQueryResponse queryName="test">"#;
123        let version = detect_version(xml).unwrap();
124        // Should default to V3
125        assert_eq!(version, FlexSchemaVersion::V3);
126    }
127
128    #[test]
129    fn test_detect_version_unknown() {
130        let xml = r#"<FlexQueryResponse version="4" queryName="test">"#;
131        let version = detect_version(xml).unwrap();
132        assert_eq!(version, FlexSchemaVersion::Unknown);
133    }
134
135    #[test]
136    fn test_detect_statement_type_activity() {
137        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
138<FlexQueryResponse queryName="test" type="AF">
139    <FlexStatements />
140</FlexQueryResponse>"#;
141        let stmt_type = detect_statement_type(xml).unwrap();
142        assert_eq!(stmt_type, StatementType::Activity);
143    }
144
145    #[test]
146    fn test_detect_statement_type_trade_confirmation() {
147        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
148<TradeConfirmationStatement accountId="U123">
149    <Trades />
150</TradeConfirmationStatement>"#;
151        let stmt_type = detect_statement_type(xml).unwrap();
152        assert_eq!(stmt_type, StatementType::TradeConfirmation);
153    }
154
155    #[test]
156    fn test_detect_statement_type_direct_flex_statement() {
157        let xml = r#"<FlexStatement accountId="U123" fromDate="2025-01-01" toDate="2025-01-31">"#;
158        let stmt_type = detect_statement_type(xml).unwrap();
159        assert_eq!(stmt_type, StatementType::Activity);
160    }
161
162    #[test]
163    fn test_detect_statement_type_invalid() {
164        let xml = r#"<Invalid>XML</Invalid>"#;
165        let result = detect_statement_type(xml);
166        assert!(result.is_err());
167    }
168}