Skip to content
Merged
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
ignore:
- "**/examples/**/*"
- "**/tests/**/*"

coverage:
status:
patch:
default:
target: 80%
project:
default:
target: auto
1 change: 1 addition & 0 deletions crates/hotfix-dictionary/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ roxmltree.workspace = true
smartstring = { workspace = true, optional = true }
strum.workspace = true
strum_macros.workspace = true
thiserror.workspace = true
35 changes: 31 additions & 4 deletions crates/hotfix-dictionary/src/dictionary.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{Component, ComponentData, Datatype, DatatypeData, Field, FieldData};

use crate::error::ParseError;
use crate::message_definition::{MessageData, MessageDefinition};
use crate::quickfix::{ParseDictionaryError, QuickFixReader};
use crate::quickfix::QuickFixReader;
use crate::string::SmartString;
use fnv::FnvHashMap;

Expand Down Expand Up @@ -52,9 +53,9 @@ impl Dictionary {

/// Attempts to read a QuickFIX-style specification file and convert it into
/// a [`Dictionary`].
pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseDictionaryError> {
pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseError> {
let xml_document =
roxmltree::Document::parse(input).map_err(|_| ParseDictionaryError::InvalidFormat)?;
roxmltree::Document::parse(input).map_err(|_| ParseError::InvalidFormat)?;
QuickFixReader::new(&xml_document)
}

Expand All @@ -71,7 +72,7 @@ impl Dictionary {
self.version.as_str()
}

pub fn load_from_file(path: &str) -> Result<Self, ParseDictionaryError> {
pub fn load_from_file(path: &str) -> Result<Self, ParseError> {
let spec = std::fs::read_to_string(path)
.unwrap_or_else(|_| panic!("unable to read FIX dictionary file at {path}"));
Dictionary::from_quickfix_spec(&spec)
Expand Down Expand Up @@ -304,3 +305,29 @@ impl Dictionary {
.collect()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_load_from_file_success() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/resources/quickfix/FIX-4.4.xml"
);
let dict = Dictionary::load_from_file(path).unwrap();
assert_eq!(dict.version(), "FIX.4.4");
assert!(dict.message_by_name("Heartbeat").is_some());
}

#[test]
fn test_load_from_file_invalid_content() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/test_data/quickfix_specs/empty_file.xml"
);
let result = Dictionary::load_from_file(path);
assert!(result.is_err());
}
}
10 changes: 10 additions & 0 deletions crates/hotfix-dictionary/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pub(crate) type ParseResult<T> = Result<T, ParseError>;

/// The error type that can arise when decoding a QuickFIX Dictionary.
#[derive(Clone, Debug, thiserror::Error)]
pub enum ParseError {
#[error("invalid format")]
InvalidFormat,
#[error("invalid data: {0}")]
InvalidData(String),
}
2 changes: 2 additions & 0 deletions crates/hotfix-dictionary/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod builder;
mod component;
mod datatype;
mod dictionary;
mod error;
mod field;
mod layout;
mod message_definition;
Expand All @@ -14,6 +15,7 @@ use component::{Component, ComponentData};
use datatype::DatatypeData;
pub use datatype::{Datatype, FixDatatype};
pub use dictionary::Dictionary;
pub use error::ParseError;
pub use field::{Field, FieldEnum, FieldLocation, IsFieldDefinition};
use field::{FieldData, FieldEnumData};
use fnv::FnvHashMap;
Expand Down
49 changes: 17 additions & 32 deletions crates/hotfix-dictionary/src/quickfix.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::builder::DictionaryBuilder;
use crate::component::{ComponentData, FixmlComponentAttributes};
use crate::error::{ParseError, ParseResult};
use crate::message_definition::MessageData;
use crate::string::SmartString;
use crate::{
Expand Down Expand Up @@ -29,7 +30,7 @@ impl<'a> QuickFixReader<'a> {
if child.is_element() {
let name = child
.attribute("name")
.ok_or(ParseDictionaryError::InvalidFormat)?
.ok_or(ParseError::InvalidFormat)?
.to_string();
import_component(&mut reader.builder, child, &name)?;
}
Expand Down Expand Up @@ -61,23 +62,17 @@ impl<'a> QuickFixReader<'a> {
let find_tagged_child = |tag: &str| {
root.children()
.find(|n| n.has_tag_name(tag))
.ok_or_else(|| ParseDictionaryError::InvalidData(format!("<{tag}> tag not found")))
.ok_or_else(|| ParseError::InvalidData(format!("<{tag}> tag not found")))
};
let version_type = root
.attribute("type")
.ok_or(ParseDictionaryError::InvalidData(
"No version attribute.".to_string(),
))?;
let version_major = root
.attribute("major")
.ok_or(ParseDictionaryError::InvalidData(
"No major version attribute.".to_string(),
))?;
let version_minor = root
.attribute("minor")
.ok_or(ParseDictionaryError::InvalidData(
"No minor version attribute.".to_string(),
))?;
.ok_or(ParseError::InvalidData("No version attribute.".to_string()))?;
let version_major = root.attribute("major").ok_or(ParseError::InvalidData(
"No major version attribute.".to_string(),
))?;
let version_minor = root.attribute("minor").ok_or(ParseError::InvalidData(
"No minor version attribute.".to_string(),
))?;
let version_sp = root.attribute("servicepack").unwrap_or("0");
let version = format!(
"{}.{}.{}{}",
Expand All @@ -104,19 +99,19 @@ impl<'a> QuickFixReader<'a> {

fn import_field(builder: &mut DictionaryBuilder, node: roxmltree::Node) -> ParseResult<()> {
if node.tag_name().name() != "field" {
return Err(ParseDictionaryError::InvalidFormat);
return Err(ParseError::InvalidFormat);
}
let data_type_name = import_datatype(builder, node);
let value_restrictions = value_restrictions_from_node(node, data_type_name.clone());
let name = node
.attribute("name")
.ok_or(ParseDictionaryError::InvalidFormat)?
.ok_or(ParseError::InvalidFormat)?
.into();
let tag = node
.attribute("number")
.ok_or(ParseDictionaryError::InvalidFormat)?
.ok_or(ParseError::InvalidFormat)?
.parse()
.map_err(|_| ParseDictionaryError::InvalidFormat)?;
.map_err(|_| ParseError::InvalidFormat)?;
let field = FieldData {
name,
tag,
Expand All @@ -142,11 +137,11 @@ fn import_message(builder: &mut DictionaryBuilder, node: roxmltree::Node) -> Par
let message = MessageData {
name: node
.attribute("name")
.ok_or(ParseDictionaryError::InvalidFormat)?
.ok_or(ParseError::InvalidFormat)?
.into(),
msg_type: node
.attribute("msgtype")
.ok_or(ParseDictionaryError::InvalidFormat)?
.ok_or(ParseError::InvalidFormat)?
.into(),
component_id: 0,
layout_items,
Expand Down Expand Up @@ -289,7 +284,7 @@ fn import_layout_item(
}
}
_ => {
return Err(ParseDictionaryError::InvalidFormat);
return Err(ParseError::InvalidFormat);
}
};
let item = LayoutItemData { required, kind };
Expand All @@ -306,13 +301,3 @@ fn panic_missing_tag_in_element(elem: roxmltree::Node, tag: &str) -> ! {
.unwrap_or("Error retrieving element text")
);
}

type ParseError = ParseDictionaryError;
type ParseResult<T> = Result<T, ParseError>;

/// The error type that can arise when decoding a QuickFIX Dictionary.
#[derive(Clone, Debug)]
pub enum ParseDictionaryError {
InvalidFormat,
InvalidData(String),
}
Loading