├── .dockerignore ├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── examples └── from-stdin.rs ├── readme.md ├── src └── lib.rs └── tests └── tests.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | * 3 | !src/ 4 | !tests/ 5 | !Cargo.toml 6 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | build-and-test-fixparser: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: whoan/docker-build-with-cache-action@v5 13 | with: 14 | username: whoan 15 | password: "${{ secrets.DOCKER_PASSWORD }}" 16 | image_name: whoan/fixparser 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fixparser" 3 | version = "0.1.5" 4 | authors = ["Juan Eugenio Abadie "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "A Rust/WASM library to parse FIX messages." 8 | documentation = "https://docs.rs/fixparser" 9 | homepage = "https://github.com/whoan/fixparser" 10 | repository = "https://github.com/whoan/fixparser" 11 | readme = "readme.md" 12 | keywords = ["fix", "fix-parser", "fix-protocol", "json", "wasm"] 13 | 14 | [dependencies] 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = { version = "1.0", features = ["preserve_order"] } 17 | regex = "1" 18 | wasm-bindgen = "0.2" 19 | 20 | [lib] 21 | crate-type = ["cdylib", "rlib"] 22 | 23 | [features] 24 | debugging = [] 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY Cargo.toml /app/ 6 | # create empty lib.rs to allow "build" command to download and compile dependencies in a separate layer. 7 | # note that I am not building the actual code yet 8 | RUN mkdir /app/src && \ 9 | echo > /app/src/lib.rs && \ 10 | cargo build && \ 11 | rm -r src/ 12 | 13 | # Avoid test errors for having two linkages 14 | RUN sed -i '/crate-type/d' Cargo.toml 15 | 16 | # build actual code 17 | COPY src /app/src 18 | RUN cargo build 19 | 20 | # test 21 | COPY tests /app/tests 22 | RUN cargo test 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Juan Eugenio Abadie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/from-stdin.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead}; 2 | 3 | fn main() -> io::Result<()> { 4 | println!("Give me a tag-value FIX message, and I will give you a JSON."); 5 | println!("Do you need help? You can try this one: 8=FIX.4.2 | 10=209"); 6 | 7 | let stdin = io::stdin(); 8 | let handle = stdin.lock(); 9 | 10 | for line in handle.lines() { 11 | if let Some(fix_message) = fixparser::FixMessage::from_tag_value(&line?) { 12 | println!("{}", fix_message.to_json()); 13 | } else { 14 | println!("Are your sure you gave me a valid FIX message?"); 15 | } 16 | } 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fixparser 2 | 3 | ![](https://github.com/whoan/fixparser/workflows/build-and-test/badge.svg) 4 | [![Crates.io](https://img.shields.io/crates/v/fixparser.svg)](https://crates.io/crates/fixparser) 5 | [![Docs.rs](https://docs.rs/fixparser/badge.svg)](https://docs.rs/fixparser) 6 | 7 | Parse FIX messages without a FIX dictionary universally thanks to Rust + WASM. 8 | 9 | ``` 10 | [dependencies] 11 | fixparser = "0.1.5" 12 | ``` 13 | 14 | It currently supports the following input/output formats: 15 | 16 | **Input:** 17 | 18 | - [FIX Tag=Value (classic FIX)](https://www.fixtrading.org/standards/tagvalue/) 19 | 20 | **Output:** 21 | 22 | - Json (`serde_json::value::Value`) 23 | 24 | > **In WASM, the output is a JSON string.** 25 | 26 | ## Goal 27 | 28 | To have a universal low-level mechanism to convert FIX messages to something easier to consume by higher-level tools. In such tools, you can combine the output of this library (json) with a FIX dictionary and let your dreams come true :nerd_face:. 29 | 30 | ## Examples 31 | 32 | ### Rust 33 | 34 | ```rust 35 | let input = "Recv | 8=FIX.4.4 | 555=2 | 600=CGY | 604=2 | 605=F7 | 605=CGYU0 | 600=CGY | 10=209"; 36 | println!("{}", fixparser::FixMessage::from_tag_value(&input).unwrap().to_json()); 37 | ``` 38 | 39 | ```rust 40 | // this input has the non-printable SOH character (0x01) as the separator of the fields 41 | let input = "8=FIX.4.4555=2600=CGY604=2605=F7605=CGYU0600=CGY10=209"; 42 | println!("{}", fixparser::FixMessage::from_tag_value(&input).unwrap().to_json()); 43 | ``` 44 | 45 | For any of those examples you will have this output: 46 | 47 | ``` 48 | {"8":"FIX.4.4","555":[{"600":"CGY","604":[{"605":"F7"},{"605":"CGYU0"}]},{"600":"CGY"}],"10":"209"} 49 | ``` 50 | 51 | Or prettier (`jq`'ed): 52 | 53 | ``` 54 | { 55 | "8": "FIX.4.4", 56 | "555": [ 57 | { 58 | "600": "CGY", 59 | "604": [ 60 | { 61 | "605": "F7" 62 | }, 63 | { 64 | "605": "CGYU0" 65 | } 66 | ] 67 | }, 68 | { 69 | "600": "CGY" 70 | } 71 | ], 72 | "10": "209" 73 | } 74 | ``` 75 | 76 | Give it a try: 77 | 78 | ```bash 79 | cargo run --example from-stdin 80 | ``` 81 | 82 | ### WASM / JS 83 | 84 | ```bash 85 | yarn add @whoan/fixparser 86 | ``` 87 | 88 | ```js 89 | const js = import('@whoan/fixparser') 90 | js.then(fixparser => console.log(fixparser.from_tag_value_to_json('8=FIX.4.4 | 10=909'))) 91 | ``` 92 | ``` 93 | {"8":"FIX.4.4","10":"909"} 94 | ``` 95 | 96 | ## Goodies 97 | 98 | - It supports repeating groups 99 | - You don't need a FIX dictionary. It is easy to create a tool to combine the output (json) with a dictionary 100 | - You don't need to specify the separator of the input string as long as they are consistent. eg: 0x01, |, etc... 101 | - You don't need to trim the input string as the lib detects the beginning and end of the message 102 | - You don't need a delimiter (eg: SOH) in the last field 103 | - It makes minimal validations on the message to allow parsing FIX messages with wrong values 104 | - It has WASM bindings to use the library universally (eg: with [wasmer](https://wasmer.io)) 105 | 106 | ## Features 107 | 108 | You can debug the library using the `debugging` feature: 109 | 110 | ``` 111 | fixparser = { version = "", features = ["debugging"] } 112 | ``` 113 | 114 | ## Nive-to-have features 115 | 116 | - Support [data fields](https://www.onixs.biz/fix-dictionary/5.0.SP2/index.html): data, and XMLData 117 | - Support more [input encodings](https://www.fixtrading.org/standards/) 118 | 119 | ## Limitations 120 | 121 | - There is a scenario where the library needs to make assumptions as it can't guess the format without a dictionary. Example: 122 | 123 | ``` 124 | 8=FIX.4.4 | 1000=2 | 1001=1 | 1002=2 | 1001=10 | 1002=20 | 1003=30 | 10=209 125 | ^ ^ 126 | group 1000 does 1003 belong to the second repetition of group 1000? 127 | ``` 128 | 129 | In such a scenario, it will assume *1003* does NOT belong to the group. Doing so, it's easier to fix it with the help of other tools which use FIX dictionaries (coming soon? let's see). 130 | 131 | ## License 132 | 133 | [MIT](https://github.com/whoan/fixparser/blob/master/LICENSE) 134 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `fixparser` is a Rust library to decode FIX (Financial Information eXchange) messages. 2 | //! 3 | //! - It supports groups and you don't need a FIX dictionary 4 | //! - You don't need to specify the separator of the input string as long as they are consistent. eg: 0x01, |, etc... 5 | //! - You don't need to "trim" the input string as the lib detects the beginning and end of the message 6 | //! 7 | //! Currently supported input: 8 | //! 9 | //! - [FIX Tag=Value (classic FIX)](https://www.fixtrading.org/standards/tagvalue/) 10 | //! 11 | //! Currently supported output: 12 | //! 13 | //! - Json (serde_json::value::Value) 14 | 15 | use serde::{ser::SerializeMap, Serialize, Serializer}; 16 | use std::collections::{HashMap, HashSet, VecDeque}; 17 | use wasm_bindgen::prelude::*; 18 | 19 | #[wasm_bindgen] 20 | pub fn from_tag_value_to_json(input_message: &str) -> String { 21 | match FixMessage::from_tag_value(input_message) { 22 | Some(fix_message) => fix_message.to_json().to_string(), 23 | None => String::from(r#"{"error": "Could not parse the given input. Is it a valid FIX message?"}"#) 24 | } 25 | } 26 | 27 | #[cfg(feature = "debugging")] 28 | macro_rules! debug { 29 | ($($arg:tt)*) => { println!($($arg)*); } 30 | } 31 | 32 | #[cfg(not(feature = "debugging"))] 33 | macro_rules! debug { 34 | ($($arg:tt)*) => {}; 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | enum FixEntity { 39 | Field(i32, String), 40 | Group(FixGroup), 41 | } 42 | 43 | impl FixEntity { 44 | fn get_tag(&self) -> i32 { 45 | match self { 46 | FixEntity::Field(tag, _dummy) => *tag, 47 | FixEntity::Group(group) => group.no_tag, 48 | } 49 | } 50 | 51 | fn get_field_value_i32(&self) -> i32 { 52 | if let FixEntity::Field(_dummy, value) = self { 53 | return value.parse().unwrap(); 54 | } 55 | panic!("A Field was expected"); 56 | } 57 | } 58 | 59 | #[derive(Debug, Clone)] 60 | struct FixComponent { 61 | entities: Vec, 62 | } 63 | 64 | impl FixComponent { 65 | fn new(entities: Vec) -> Self { 66 | Self { entities } 67 | } 68 | } 69 | 70 | impl Serialize for FixComponent { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: Serializer, 74 | { 75 | let mut map = serializer.serialize_map(Some(self.entities.len()))?; 76 | for entity in &self.entities { 77 | match entity { 78 | FixEntity::Field(ref tag, ref value) => { 79 | map.serialize_entry(tag, value)?; 80 | } 81 | FixEntity::Group(ref group) => { 82 | map.serialize_entry(&group.no_tag, &group.instances)?; 83 | } 84 | } 85 | } 86 | map.end() 87 | } 88 | } 89 | 90 | #[derive(Debug, Clone)] 91 | struct FixGroup { 92 | delimiter: i32, // first tag of each group instance 93 | no_tag: i32, // tag which contains the number of repetitions 94 | repetitions: i32, 95 | current_iteration: i32, 96 | known_tags: HashSet, // tags we know that belong to this group 97 | instances: Vec, 98 | } 99 | 100 | impl FixGroup { 101 | fn new(delimiter: i32, index_first_delimiter: usize, component: &mut FixComponent) -> Self { 102 | let group_instance = 103 | FixComponent::new(component.entities.drain(index_first_delimiter..).collect()); 104 | let no_tag_field = component.entities.pop().unwrap(); 105 | 106 | Self { 107 | no_tag: no_tag_field.get_tag(), // bad variable name, as in FIX 108 | delimiter, 109 | repetitions: no_tag_field.get_field_value_i32(), 110 | current_iteration: 1, 111 | known_tags: Self::get_known_tags(&group_instance), 112 | instances: vec![group_instance], 113 | } 114 | } 115 | 116 | fn get_known_tags(group_instance: &FixComponent) -> HashSet { 117 | let mut known_tags = HashSet::::new(); 118 | group_instance 119 | .entities 120 | .iter() 121 | .for_each(|entity| match entity { 122 | FixEntity::Field(tag, _value) => { 123 | known_tags.insert(*tag); 124 | } 125 | FixEntity::Group(group) => { 126 | group.known_tags.iter().for_each(|known_tag| { 127 | known_tags.insert(*known_tag); 128 | }); 129 | } 130 | }); 131 | known_tags 132 | } 133 | 134 | fn create_new_instance(&mut self) { 135 | self.instances.push(FixComponent::new(Vec::new())); 136 | } 137 | 138 | fn insert_known_tag(&mut self, tag: i32) { 139 | self.known_tags.insert(tag); 140 | } 141 | } 142 | 143 | #[derive(Debug)] 144 | struct TagValue<'a>(i32, &'a str); 145 | 146 | /// This is the interface you interact with. 147 | /// 148 | /// The internal message is represented as follows: 149 | /// 150 | /// ```ignore 151 | /// FixMessage := FixComponent 152 | /// FixComponent := FixEntity* 153 | /// 154 | /// FixEntity := Field | Group 155 | /// 156 | /// Field := (tag: i32, value: String) 157 | /// Group := FixComponent* 158 | /// ``` 159 | pub struct FixMessage { 160 | root_component: FixComponent, 161 | pending_tag_indices: HashMap>, 162 | candidate_indices: Vec>, // store indices of tags of potential nested group 163 | active_groups: Vec, // contains the groups currently being parsed 164 | } 165 | 166 | impl FixMessage { 167 | fn new() -> Self { 168 | let mut candidate_indices = Vec::new(); 169 | candidate_indices.push(HashMap::new()); 170 | Self { 171 | root_component: FixComponent::new(Vec::new()), 172 | pending_tag_indices: HashMap::new(), 173 | candidate_indices, 174 | active_groups: Vec::new(), 175 | } 176 | } 177 | 178 | /// Creates a FixMessage from an input string encoded in [FIX Tag=Value (classic FIX)](https://www.fixtrading.org/standards/tagvalue/). 179 | /// 180 | /// # Example 181 | /// 182 | /// ```rust 183 | /// let input = "Recv | 8=FIX.4.4 | 555=2 | 600=CGY | 604=2 | 605=F7 | 605=CGYU0 | 600=CGY | 10=209"; 184 | /// println!("{}", fixparser::FixMessage::from_tag_value(&input).unwrap().to_json()); 185 | /// ``` 186 | pub fn from_tag_value(input_message: &str) -> Option { 187 | let tag_values = FixMessage::pre_process_message(&input_message)?; 188 | let mut message = FixMessage::new(); 189 | 190 | for (index, tag_value) in tag_values.iter().enumerate() { 191 | message 192 | .pending_tag_indices 193 | .entry(tag_value.0) 194 | .or_insert_with(VecDeque::new) 195 | .push_back(index); 196 | } 197 | message.check_message_is_valid(); 198 | 199 | for (index, tag_value) in tag_values.iter().enumerate() { 200 | message.add_tag_value(tag_value.0, String::from(tag_value.1), index); 201 | } 202 | message.clean(); 203 | 204 | Some(message) 205 | } 206 | 207 | /// Get a representation of the message in json string format. 208 | /// 209 | /// # Example 210 | /// 211 | /// ```rust 212 | /// // this input has the non-printable character 0x01 as the separator of the fields 213 | /// let input = "8=FIX.4.4555=2600=CGY604=2605=F7605=CGYU0600=CGY10=209"; 214 | /// println!("{}", fixparser::FixMessage::from_tag_value(&input).unwrap().to_json()); 215 | /// ``` 216 | /// 217 | /// ```ignore 218 | /// {"8":"FIX.4.4","555":[{"600":"CGY","604":[{"605":"F7"},{"605":"CGYU0"}]},{"600":"CGY"}],"10":"209"} 219 | /// ``` 220 | pub fn to_json(&self) -> serde_json::value::Value { 221 | serde_json::json!(&self.root_component) 222 | } 223 | 224 | // from tag value encoding to a list of TagValue's 225 | fn pre_process_message<'a>(input_message: &'a str) -> Option>> { 226 | const SHORTEST_MESSAGE_LENGTH: usize = 12; // len(8=FIX.N.M|X=) -> invalid still parsable 227 | // trim input 228 | let input_message = &input_message[input_message.find("8=")?..]; 229 | if input_message.len() < SHORTEST_MESSAGE_LENGTH { 230 | return None; 231 | } 232 | 233 | let mut end_of_message_found = false; 234 | input_message 235 | .split(&Self::get_separator(input_message)?) 236 | .map(|tag_value| { 237 | tag_value.split_at(tag_value.find('=').unwrap_or_else(|| tag_value.len())) 238 | }) 239 | .inspect(|tag_value| { 240 | if tag_value.1.is_empty() { 241 | eprintln!("WARNING: Ignoring [{}]", tag_value.0); 242 | } else if tag_value.1.len() == 1 { 243 | eprintln!("WARNING: Tag {} has no value", tag_value.0); 244 | } 245 | }) 246 | .filter(|tag_value| !tag_value.1.is_empty()) 247 | .map(|tag_value| { 248 | let tag = tag_value.0.parse().unwrap_or(0); 249 | let value = &tag_value.1[1..]; 250 | if tag == 10 && value.len() > 3 { 251 | debug!("Ignoring characters after checksum [{}]", &value[3..]); 252 | return TagValue(tag, &value[0..3]); 253 | } 254 | if tag == 0 { 255 | eprintln!("WARNING: Ignoring [{}={}]", tag_value.0, value); 256 | } 257 | TagValue(tag, value) 258 | }) 259 | .take_while(|tag_value| { 260 | if end_of_message_found { 261 | eprintln!("WARNING: Detected tag after tag 10: {}", tag_value.0); 262 | return false; 263 | } 264 | end_of_message_found = tag_value.0 == 10; 265 | true 266 | }) 267 | .filter(|tag_value| tag_value.0 != 0) 268 | .map(Some) 269 | .collect() 270 | } 271 | 272 | // get FIX values separator: eg: 0x01 or | 273 | fn get_separator(fix_msg: &str) -> Option { 274 | let fix_version_re = regex::Regex::new(r"^8=FIXT?.\d{1}.\d{1}").unwrap(); 275 | let field_separator = &fix_msg[fix_version_re.shortest_match(fix_msg)?..] 276 | .chars() 277 | .take_while(|char| !char.is_digit(10)) 278 | .collect::(); 279 | 280 | if field_separator == "" { 281 | return None; 282 | } 283 | Some(field_separator.to_string()) 284 | } 285 | 286 | fn check_message_is_valid(&self) { 287 | if self.pending_tag_indices.get(&10).is_none() { 288 | eprintln!("WARNING: Message is incomplete (missing tag 10)"); 289 | } 290 | } 291 | 292 | #[allow(unused_variables)] 293 | fn add_tag_value(&mut self, tag: i32, value: String, index: usize) { 294 | debug!( 295 | "{}Index {} - Add {} - {}", 296 | self.get_spaces(), 297 | index, 298 | tag, 299 | value 300 | ); 301 | self.remove_pending_tag(tag); 302 | 303 | while self.is_parsing_group() && !self.tag_in_group(tag) { 304 | self.close_group(); 305 | } 306 | 307 | if self.repeated_candidate(tag) { 308 | self.open_group(tag); 309 | } 310 | 311 | if self.is_parsing_group() { 312 | self.set_known_tag_in_group(tag); 313 | } 314 | 315 | if self.is_new_iteration(tag) { 316 | self.create_new_group_instance(); 317 | } else { 318 | self.register_candidate(tag); 319 | } 320 | 321 | self.get_entities().push(FixEntity::Field(tag, value)); 322 | } 323 | 324 | fn clean(&mut self) { 325 | self.pending_tag_indices.clear(); 326 | self.candidate_indices.clear(); 327 | self.active_groups.clear(); 328 | } 329 | 330 | fn open_group(&mut self, group_delimiter: i32) { 331 | debug!("{}INFO: Group detected", self.get_spaces()); 332 | let group = FixGroup::new( 333 | group_delimiter, 334 | self.get_index_of_candidate(group_delimiter), 335 | self.get_component(), 336 | ); 337 | self.active_groups.push(group); 338 | self.candidate_indices.push(HashMap::new()); 339 | } 340 | 341 | fn get_candidates(&self) -> &HashMap { 342 | self.candidate_indices.last().unwrap() 343 | } 344 | 345 | fn get_candidates_mut(&mut self) -> &mut HashMap { 346 | self.candidate_indices.last_mut().unwrap() 347 | } 348 | 349 | fn get_index_of_candidate(&self, tag: i32) -> usize { 350 | *self.get_candidates().get(&tag).unwrap() 351 | } 352 | 353 | // must be called before new insertion 354 | fn register_candidate(&mut self, tag: i32) { 355 | let candidate_index = self.get_entities().len(); 356 | self.get_candidates_mut().insert(tag, candidate_index); 357 | } 358 | 359 | fn repeated_candidate(&mut self, tag: i32) -> bool { 360 | self.get_candidates().contains_key(&tag) 361 | } 362 | 363 | fn get_next_index_of_pending_tag(&self, tag: i32) -> Option<&usize> { 364 | self.pending_tag_indices.get(&tag).unwrap().front() 365 | } 366 | 367 | fn remove_pending_tag(&mut self, tag: i32) { 368 | self.pending_tag_indices.get_mut(&tag).unwrap().pop_front(); 369 | } 370 | 371 | fn close_group(&mut self) { 372 | debug!("{}INFO: Stop parsing group\n", self.get_spaces()); 373 | let closed_group = self.active_groups.pop().unwrap(); 374 | self.get_component() 375 | .entities 376 | .push(FixEntity::Group(closed_group)); 377 | self.candidate_indices.pop(); 378 | } 379 | 380 | fn is_new_iteration(&self, tag: i32) -> bool { 381 | self.is_parsing_group() && tag == self.active_group().delimiter 382 | } 383 | 384 | fn increment_iteration(&mut self) { 385 | debug!( 386 | "{}-- repetition {} --", 387 | self.get_spaces(), 388 | self.active_group().current_iteration + 1 389 | ); 390 | self.active_group_mut().current_iteration += 1 391 | } 392 | 393 | fn get_entities(&mut self) -> &mut Vec { 394 | &mut self.get_component().entities 395 | } 396 | 397 | fn create_new_group_instance(&mut self) { 398 | self.get_candidates_mut().clear(); 399 | self.increment_iteration(); 400 | self.active_group_mut().create_new_instance(); 401 | } 402 | 403 | #[allow(dead_code)] 404 | fn get_spaces(&self) -> String { 405 | " ".repeat(self.active_groups.len() * 2) 406 | } 407 | 408 | fn set_known_tag_in_group(&mut self, tag: i32) { 409 | self.active_group_mut().insert_known_tag(tag); 410 | } 411 | 412 | fn get_component(&mut self) -> &mut FixComponent { 413 | if self.is_parsing_group() { 414 | self.active_group_mut().instances.last_mut().unwrap() 415 | } else { 416 | &mut self.root_component 417 | } 418 | } 419 | 420 | fn is_parsing_group(&self) -> bool { 421 | !self.active_groups.is_empty() 422 | } 423 | 424 | fn active_group(&self) -> &FixGroup { 425 | self.active_groups.last().unwrap() 426 | } 427 | 428 | fn active_group_mut(&mut self) -> &mut FixGroup { 429 | self.active_groups.last_mut().unwrap() 430 | } 431 | 432 | fn tag_in_group(&mut self, tag: i32) -> bool { 433 | if tag == 10 { 434 | eprintln!("WARNING: End of message detected while parsing group"); 435 | return false; 436 | } 437 | // from cheaper to more expensive check 438 | !self.is_last_iteration() 439 | || self.is_known_group_tag(tag) 440 | || self.pending_tag_in_last_instance() 441 | } 442 | 443 | fn pending_tag_in_last_instance(&mut self) -> bool { 444 | self.active_group().known_tags.iter().any(|known_tag| { 445 | if let Some(tag_index) = self.get_next_index_of_pending_tag(*known_tag) { 446 | return self.index_belongs_to_current_group(*tag_index); 447 | } 448 | false 449 | }) 450 | } 451 | 452 | fn index_belongs_to_current_group(&self, tag_index: usize) -> bool { 453 | if let Some(delimiter_index) = 454 | self.get_next_index_of_pending_tag(self.active_group().delimiter) 455 | { 456 | return tag_index < *delimiter_index; 457 | } 458 | true 459 | } 460 | 461 | fn is_known_group_tag(&self, tag: i32) -> bool { 462 | self.active_group().known_tags.contains(&tag) 463 | } 464 | 465 | fn is_last_iteration(&self) -> bool { 466 | self.active_group().current_iteration == self.active_group().repetitions 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | use fixparser::FixMessage; 2 | 3 | #[test] 4 | fn minimal_length() { 5 | let input = "8=FIX.4.4|10=209"; 6 | let output = r#"{"8":"FIX.4.4","10":"209"}"#; 7 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 8 | } 9 | 10 | #[test] 11 | fn prefixed() { 12 | let input = "Recv | 8=FIX.4.4 | 9=something | 10=209"; 13 | let output = r#"{"8":"FIX.4.4","9":"something","10":"209"}"#; 14 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 15 | } 16 | 17 | #[test] 18 | fn control_a_separator() { 19 | let input = "8=FIX.4.4^A9=something^A10=209"; 20 | let output = r#"{"8":"FIX.4.4","9":"something","10":"209"}"#; 21 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 22 | } 23 | 24 | #[test] 25 | fn nested_nested_groups_1() { 26 | let input = "8=FIX.4.4 | 555=2 | 604=2 | 605=F7 | 605=CGYU0 | 604=2 | 605=F7 | 605=CGYM0 | 10=209"; 27 | let output = r#"{"8":"FIX.4.4","555":[{"604":[{"605":"F7"},{"605":"CGYU0"}]},{"604":[{"605":"F7"},{"605":"CGYM0"}]}],"10":"209"}"#; 28 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 29 | } 30 | 31 | #[test] 32 | fn nested_nested_groups_2() { 33 | let input = "8=FIX.4.4 | 555=2 | 600=CGY | 604=2 | 605=F7 | 605=CGYU0 | 600=CGY | 604=2 | 605=F7 | 605=CGYM0 | 10=209"; 34 | let output = r#"{"8":"FIX.4.4","555":[{"600":"CGY","604":[{"605":"F7"},{"605":"CGYU0"}]},{"600":"CGY","604":[{"605":"F7"},{"605":"CGYM0"}]}],"10":"209"}"#; 35 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 36 | } 37 | 38 | #[test] 39 | fn nested_nested_groups_3() { 40 | let input = "8=FIX.4.49=0062435=AB49=Sender56=Target34=000003058369=00000005452=20200424-13:54:17.519142=US,NY11=158773500012960=20200424-13:54:17.51848=2D3D22=855=2D3D461=FMMXSX167=FUT555=3600=2D602=1M2MN0603=5608=ACMXSX609=FUT610=202007611=20200730624=49623=1566=3204600=2D602=M2MQ0603=5608=ACMXSX609=FUT610=202008611=20200831624=49623=1566=3204600=2D602=M2MU0603=5608=ACMXSX609=FUT610=202009630=hello631=yes632=it633=works611=20200930624=49623=1566=320444=320438=254=140=277=O59=01028=Y21=110=100"; 41 | let output = r#"{"8":"FIX.4.4","9":"00624","35":"AB","49":"Sender","56":"Target","34":"000003058","369":"000000054","52":"20200424-13:54:17.519","142":"US,NY","11":"1587735000129","60":"20200424-13:54:17.518","48":"2D3D","22":"8","55":"2D3D","461":"FMMXSX","167":"FUT","555":[{"600":"2D","602":"1M2MN0","603":"5","608":"ACMXSX","609":"FUT","610":"202007","611":"20200730","624":"49","623":"1","566":"3204"},{"600":"2D","602":"M2MQ0","603":"5","608":"ACMXSX","609":"FUT","610":"202008","611":"20200831","624":"49","623":"1","566":"3204"},{"600":"2D","602":"M2MU0","603":"5","608":"ACMXSX","609":"FUT","610":"202009","630":"hello","631":"yes","632":"it","633":"works","611":"20200930","624":"49","623":"1","566":"3204"}],"44":"3204","38":"2","54":"1","40":"2","77":"O","59":"0","1028":"Y","21":"1","10":"100"}"#; 42 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 43 | } 44 | 45 | #[test] 46 | fn more_tags() { 47 | let input = "8=FIX.4.4 | 10=209 | 11=some"; 48 | let output = r#"{"8":"FIX.4.4","10":"209"}"#; 49 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 50 | } 51 | 52 | #[test] 53 | fn value_with_equal() { 54 | let input = "8=FIX.4.4 | 50=there is an = here | 10=209"; 55 | let output = r#"{"8":"FIX.4.4","50":"there is an = here","10":"209"}"#; 56 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 57 | } 58 | 59 | #[test] 60 | fn big_msg() { 61 | let input = "8=FIX.4.4 | 9=01944 | 35=8 | 49=sender | 56=target | 34=3951 | 50=O001 | 142=US,NY | 52=20200520-19:15:45.134 | 116=john | 129=taylor | 37=07491773 | 198=78652655716 | 526=1589738524192 | 527=07491773-88e4a2169:4 | 11=1589997254902 | 41=19997254901 | 10011=42 | 453=2 | 448=1 | 452=205 | 447=D | 448=FIX_OUT | 452=83 | 447=D | 17=78663 | 150=Z | 18=2 | 39=0 | 1=out | 55=3D | 107=long value here | 460=14 | 48=16735443526687 | 167=MLEG | 762=Strip | 200=202007 | 541=20200701 | 205=1 | 207=IEX | 461=FMMXSX | 15=USD | 54=18765 | 38=10 | 40=2 | 44=2900 | 59=0 | 151=10 | 14=0 | 6=0 | 60=20200520-19:15:45.099000 | 77=O | 442=3 | 1028=N | 582=1 | 21=1 | 454=4 | 455=PA | 456=99 | 455=some-here | 456=98 | 455=3D something | 456=97 | 455=106723 | 456=8 | 555=3 | 600=3D | 620=some long value | 607=14 | 602=168921002590820 | 603=96 | 609=FUT | 610=202007 | 611=20200730 | 616=IEX | 608=FMXSX | 624=1 | 623=1 | 556=USD | 654=1 | 604=5 | 605=PA | 606=99 | 605=2DN0 | 606=98 | 605=3D Jul20 | 606=97 | 605=1M2MN0 | 606=5 | 605=48304 | 606=8 | 600=3D | 620=some long value | 607=14 | 602=1287304730621 | 603=96 | 609=FUT | 610=202008 | 611=20200831 | 616=IEX | 608=FMXSX | 624=1 | 623=1 | 556=USD | 654=2 | 604=5 | 605=PA | 606=99 | 605=2DQ0 | 606=98 | 605=3D Aug20 | 606=97 | 605=1M2MQ0 | 606=5 | 605=48610 | 606=8 | 600=3D | 620=long value | 607=14 | 602=78779119978 | 603=96 | 609=FUT | 610=202009 | 611=20200930 | 616=IEX | 608=FMXSX | 624=1 | 623=1 | 556=USD | 654=3 | 604=5 | 605=PA | 606=99 | 605=2DU0 | 606=98 | 605=3D some | 606=97 | 605=1M2MU0 | 606=5 | 605=45945 | 606=8 | 30=HJGU | 1031=W | 10=139 | "; 62 | let output = r#"{"8":"FIX.4.4","9":"01944","35":"8","49":"sender","56":"target","34":"3951","50":"O001","142":"US,NY","52":"20200520-19:15:45.134","116":"john","129":"taylor","37":"07491773","198":"78652655716","526":"1589738524192","527":"07491773-88e4a2169:4","11":"1589997254902","41":"19997254901","10011":"42","453":[{"448":"1","452":"205","447":"D"},{"448":"FIX_OUT","452":"83","447":"D"}],"17":"78663","150":"Z","18":"2","39":"0","1":"out","55":"3D","107":"long value here","460":"14","48":"16735443526687","167":"MLEG","762":"Strip","200":"202007","541":"20200701","205":"1","207":"IEX","461":"FMMXSX","15":"USD","54":"18765","38":"10","40":"2","44":"2900","59":"0","151":"10","14":"0","6":"0","60":"20200520-19:15:45.099000","77":"O","442":"3","1028":"N","582":"1","21":"1","454":[{"455":"PA","456":"99"},{"455":"some-here","456":"98"},{"455":"3D something","456":"97"},{"455":"106723","456":"8"}],"555":[{"600":"3D","620":"some long value","607":"14","602":"168921002590820","603":"96","609":"FUT","610":"202007","611":"20200730","616":"IEX","608":"FMXSX","624":"1","623":"1","556":"USD","654":"1","604":[{"605":"PA","606":"99"},{"605":"2DN0","606":"98"},{"605":"3D Jul20","606":"97"},{"605":"1M2MN0","606":"5"},{"605":"48304","606":"8"}]},{"600":"3D","620":"some long value","607":"14","602":"1287304730621","603":"96","609":"FUT","610":"202008","611":"20200831","616":"IEX","608":"FMXSX","624":"1","623":"1","556":"USD","654":"2","604":[{"605":"PA","606":"99"},{"605":"2DQ0","606":"98"},{"605":"3D Aug20","606":"97"},{"605":"1M2MQ0","606":"5"},{"605":"48610","606":"8"}]},{"600":"3D","620":"long value","607":"14","602":"78779119978","603":"96","609":"FUT","610":"202009","611":"20200930","616":"IEX","608":"FMXSX","624":"1","623":"1","556":"USD","654":"3","604":[{"605":"PA","606":"99"},{"605":"2DU0","606":"98"},{"605":"3D some","606":"97"},{"605":"1M2MU0","606":"5"},{"605":"45945","606":"8"}]}],"30":"HJGU","1031":"W","10":"139"}"#; 63 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 64 | } 65 | 66 | #[test] 67 | fn fix_5_spx() { 68 | let input = "8=FIXT.1.1 | 10=209"; 69 | let output = r#"{"8":"FIXT.1.1","10":"209"}"#; 70 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 71 | } 72 | 73 | #[test] 74 | fn soh_separator() { 75 | let input = "8=FIX.4.410=209"; 76 | let output = r#"{"8":"FIX.4.4","10":"209"}"#; 77 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 78 | } 79 | 80 | // invalid still parsable messages 81 | 82 | #[test] 83 | fn missinig_repeating_group() { 84 | // WARNING: the lib should generate an output although there is a missing repetition 85 | let input = "8=FIX.4.4 | 555=3 | 600=QWE | 600=RTY | 10=209"; 86 | let output = r#"{"8":"FIX.4.4","555":[{"600":"QWE"},{"600":"RTY"}],"10":"209"}"#; 87 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 88 | } 89 | 90 | #[test] 91 | fn value_with_separator() { 92 | // WARNING: anything after a separator in the value of the field, will be truncated 93 | let input = "8=FIX.4.4 | 50=there is a | here | 10=209"; 94 | let output = r#"{"8":"FIX.4.4","50":"there is a","10":"209"}"#; 95 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 96 | } 97 | 98 | #[test] 99 | fn invalid_tag() { 100 | // WARNING: invalid tags are just ignored together with its value (if any) 101 | let input = "8=FIX.4.4 | 9=some | thing=wrong | 10=209"; 102 | let output = r#"{"8":"FIX.4.4","9":"some","10":"209"}"#; 103 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 104 | } 105 | 106 | #[test] 107 | fn missinig_checksum_tag() { 108 | let input = "8=FIX.4.4 | 9=some"; 109 | let output = r#"{"8":"FIX.4.4","9":"some"}"#; 110 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 111 | } 112 | 113 | #[test] 114 | fn missing_checksum_value() { 115 | let input = "8=FIX.4.4 | 9=some | 10="; 116 | let output = r#"{"8":"FIX.4.4","9":"some","10":""}"#; 117 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 118 | } 119 | 120 | #[test] 121 | fn shortest_parsable() { 122 | let input = "8=FIX.4.4|1="; 123 | let output = r#"{"8":"FIX.4.4","1":""}"#; 124 | assert_eq!(output, FixMessage::from_tag_value(&input).unwrap().to_json().to_string()); 125 | } 126 | 127 | // invalid cases from here 128 | 129 | #[test] 130 | #[should_panic] 131 | fn too_short() { 132 | let input = "8=FIX.4.4|1"; 133 | FixMessage::from_tag_value(&input).unwrap().to_json().to_string(); 134 | } 135 | 136 | #[test] 137 | #[should_panic] 138 | fn missing_fix_version_1() { 139 | let input = "8= | 10=123"; 140 | FixMessage::from_tag_value(&input).unwrap().to_json().to_string(); 141 | } 142 | 143 | #[test] 144 | #[should_panic] 145 | fn missing_fix_version_2() { 146 | let input = "8= | 9=somethinghere | 10=123"; 147 | FixMessage::from_tag_value(&input).unwrap().to_json().to_string(); 148 | } 149 | --------------------------------------------------------------------------------