Skip to content

Commit

Permalink
✨ Add quickfix-spec-parser
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurlm committed Jan 10, 2024
1 parent 57c4113 commit 03abc40
Show file tree
Hide file tree
Showing 24 changed files with 1,383 additions and 11 deletions.
50 changes: 40 additions & 10 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[workspace]
resolver = "2"
members = ["quickfix-ffi", "quickfix"]
members = ["quickfix-ffi", "quickfix-spec-parser", "quickfix"]
10 changes: 10 additions & 0 deletions quickfix-spec-parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "quickfix-spec-parser"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
bytes = "1.5.0"
quick-xml = "0.31.0"
thiserror = "1.0.56"
23 changes: 23 additions & 0 deletions quickfix-spec-parser/examples/spec_read_write.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::fs;

use quickfix_spec_parser::{parse_spec, write_spec};

fn main() {
// Parse XML spec.
let spec = parse_spec(include_bytes!(
"../../quickfix-ffi/libquickfix/spec/FIXT11.xml"
))
.unwrap();

// Print it.
println!("spec: {spec:#?}");

// Rewrite it as XML and apply change to make it match with original spec format.
let out = write_spec(&spec).unwrap();
let txt = String::from_utf8(out.to_vec())
.unwrap()
.replace('\"', "'")
.replace("/>", " />");

fs::write("out.xml", txt).unwrap();
}
36 changes: 36 additions & 0 deletions quickfix-spec-parser/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::{num::ParseIntError, string::FromUtf8Error};

use thiserror::Error;

#[derive(Debug, Error, PartialEq, Eq)]
pub enum FixSpecError {
#[error("invalid document: {0}")]
InvalidDocument(&'static str),

#[error("invalid attribute: {0}")]
InvalidAttribute(String),

#[error("invalid content: {0}")]
InvalidContent(String),

#[error("xml error: {0}")]
Xml(String),
}

impl From<FromUtf8Error> for FixSpecError {
fn from(err: FromUtf8Error) -> Self {
Self::InvalidContent(err.to_string())
}
}

impl From<ParseIntError> for FixSpecError {
fn from(err: ParseIntError) -> Self {
Self::InvalidContent(err.to_string())
}
}

impl From<quick_xml::Error> for FixSpecError {
fn from(err: quick_xml::Error) -> Self {
Self::Xml(err.to_string())
}
}
59 changes: 59 additions & 0 deletions quickfix-spec-parser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use bytes::Bytes;
use quick_xml::{events::Event, Reader, Writer};

mod error;
mod model {
mod component;
mod component_spec;
mod field;
mod field_allowed_value;
mod field_spec;
mod field_type;
mod field_value;
mod group;
mod message;
mod message_category;
mod spec;

pub use self::component::Component;
pub use self::component_spec::ComponentSpec;
pub use self::field::Field;
pub use self::field_allowed_value::FieldAllowedValue;
pub use self::field_spec::FieldSpec;
pub use self::field_type::FieldType;
pub use self::field_value::FieldValue;
pub use self::group::Group;
pub use self::message::Message;
pub use self::message_category::MessageCategory;
pub use self::spec::FixSpec;
}
mod xml_ext;

pub use error::*;
pub use model::*;
pub use xml_ext::*;

type XmlWriter = Writer<Vec<u8>>;
type XmlReader<'a> = Reader<&'a [u8]>;

pub fn parse_spec(input: &[u8]) -> Result<FixSpec, FixSpecError> {
let mut reader = Reader::from_reader(input);
reader.trim_text(true);

match reader.read_event()? {
// If we are at start of FIX spec.
Event::Start(e) if e.name().as_ref() == FixSpec::TAG_NAME.as_bytes() => {
FixSpec::parse_xml_tree(&e, &mut reader)
}
// Otherwise document is invalid
_ => Err(FixSpecError::InvalidDocument("invalid root")),
}
}

pub fn write_spec(spec: &FixSpec) -> Result<Bytes, FixSpecError> {
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 1);

spec.write_xml(&mut writer)?;

Ok(Bytes::from(writer.into_inner()))
}
31 changes: 31 additions & 0 deletions quickfix-spec-parser/src/model/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use quick_xml::events::BytesStart;

use crate::{read_attribute, FixSpecError, XmlObject, XmlReadable, XmlWritable, XmlWriter};

#[derive(Debug)]
pub struct Component {
pub name: String,
pub required: bool,
}

impl XmlObject for Component {
const TAG_NAME: &'static str = "component";
}

impl XmlReadable for Component {
fn parse_xml_node(element: &BytesStart) -> Result<Self, FixSpecError> {
let name = read_attribute(element, "name")?;
let required = read_attribute(element, "required")? == "Y";
Ok(Self { name, required })
}
}

impl XmlWritable for Component {
fn write_xml<'a>(&self, writer: &'a mut XmlWriter) -> quick_xml::Result<&'a mut XmlWriter> {
writer
.create_element(Self::TAG_NAME)
.with_attribute(("name", self.name.as_str()))
.with_attribute(("required", if self.required { "Y" } else { "N" }))
.write_empty()
}
}
46 changes: 46 additions & 0 deletions quickfix-spec-parser/src/model/component_spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use quick_xml::events::BytesStart;

use crate::{
read_attribute, write_xml_list, FieldValue, FixSpecError, XmlObject, XmlReadable, XmlReader,
XmlWritable, XmlWriter,
};

#[derive(Debug)]
pub struct ComponentSpec {
pub name: String,
pub values: Vec<FieldValue>,
}

impl XmlObject for ComponentSpec {
const TAG_NAME: &'static str = "component";
}

impl XmlReadable for ComponentSpec {
fn parse_xml_node(element: &BytesStart) -> Result<Self, FixSpecError> {
let name = read_attribute(element, "name")?;
Ok(Self {
name,
values: Vec::new(),
})
}

fn parse_xml_tree(element: &BytesStart, reader: &mut XmlReader) -> Result<Self, FixSpecError> {
let mut output = Self::parse_xml_node(element)?;
output.values = FieldValue::parse_xml_tree(reader, Self::TAG_NAME)?;
Ok(output)
}
}

impl XmlWritable for ComponentSpec {
fn write_xml<'a>(&self, writer: &'a mut XmlWriter) -> quick_xml::Result<&'a mut XmlWriter> {
let element = writer
.create_element(Self::TAG_NAME)
.with_attribute(("name", self.name.as_str()));

if self.values.is_empty() {
element.write_empty()
} else {
element.write_inner_content(|writer| write_xml_list(writer, &self.values))
}
}
}
31 changes: 31 additions & 0 deletions quickfix-spec-parser/src/model/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use quick_xml::events::BytesStart;

use crate::{read_attribute, FixSpecError, XmlObject, XmlReadable, XmlWritable, XmlWriter};

#[derive(Debug)]
pub struct Field {
pub name: String,
pub required: bool,
}

impl XmlObject for Field {
const TAG_NAME: &'static str = "field";
}

impl XmlReadable for Field {
fn parse_xml_node(element: &BytesStart) -> Result<Self, FixSpecError> {
let name = read_attribute(element, "name")?;
let required = read_attribute(element, "required")? == "Y";
Ok(Self { name, required })
}
}

impl XmlWritable for Field {
fn write_xml<'a>(&self, writer: &'a mut XmlWriter) -> quick_xml::Result<&'a mut XmlWriter> {
writer
.create_element(Self::TAG_NAME)
.with_attribute(("name", self.name.as_str()))
.with_attribute(("required", if self.required { "Y" } else { "N" }))
.write_empty()
}
}
31 changes: 31 additions & 0 deletions quickfix-spec-parser/src/model/field_allowed_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use quick_xml::events::BytesStart;

use crate::{read_attribute, FixSpecError, XmlObject, XmlReadable, XmlWritable, XmlWriter};

#[derive(Debug)]
pub struct FieldAllowedValue {
pub value: String,
pub description: String,
}

impl XmlObject for FieldAllowedValue {
const TAG_NAME: &'static str = "value";
}

impl XmlReadable for FieldAllowedValue {
fn parse_xml_node(element: &BytesStart) -> Result<Self, FixSpecError> {
let value = read_attribute(element, "enum")?;
let description = read_attribute(element, "description")?;
Ok(Self { value, description })
}
}

impl XmlWritable for FieldAllowedValue {
fn write_xml<'a>(&self, writer: &'a mut XmlWriter) -> quick_xml::Result<&'a mut XmlWriter> {
writer
.create_element(Self::TAG_NAME)
.with_attribute(("enum", self.value.as_str()))
.with_attribute(("description", self.description.as_str()))
.write_empty()
}
}
Loading

0 comments on commit 03abc40

Please sign in to comment.