use std::collections::{BTreeMap, HashSet};
use std::fmt::Write as _;
use quickfix_tokio::datadictionary::{DataDictionary, FieldDef, FieldType, GroupDef, MessageDef};
use quickfix_tokio::message::Tag;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 3 {
eprintln!("usage: generate-fix <spec.xml> <out_dir>");
std::process::exit(2);
}
let spec_path = &args[1];
let out_dir = std::path::Path::new(&args[2]);
let text = std::fs::read_to_string(spec_path).expect("read spec");
let dd = DataDictionary::parse(&text).expect("parse spec");
std::fs::create_dir_all(out_dir).expect("create out dir");
let generator = Generator { dd };
std::fs::write(out_dir.join("fields.rs"), generator.gen_fields()).expect("write fields.rs");
std::fs::write(out_dir.join("messages.rs"), generator.gen_messages()).expect("write messages.rs");
std::fs::write(out_dir.join("mod.rs"), generator.gen_mod()).expect("write mod.rs");
println!(
"generated {} fields, {} messages from {} into {}",
generator.dd.fields_by_tag.len(),
generator.dd.messages.len(),
spec_path,
out_dir.display()
);
}
struct Generator {
dd: DataDictionary,
}
const HEADER: &str = "//! GENERATED by `generate-fix` — do not edit by hand.\n\
#![allow(clippy::all, unused_imports, unused_mut, dead_code, non_upper_case_globals)]\n\n";
fn value_type(ft: FieldType) -> &'static str {
match ft {
FieldType::Int
| FieldType::Length
| FieldType::SeqNum
| FieldType::NumInGroup
| FieldType::DayOfMonth => "i64",
FieldType::Float
| FieldType::Qty
| FieldType::Price
| FieldType::PriceOffset
| FieldType::Amt
| FieldType::Percentage => "crate::Amount",
FieldType::Char => "char",
FieldType::Boolean => "bool",
FieldType::UtcTimestamp => "crate::UtcTimestamp",
FieldType::UtcDateOnly | FieldType::LocalMktDate => "crate::FixDate",
FieldType::Data => "Vec<u8>",
_ => "String",
}
}
fn param_style(ft: FieldType) -> (String, &'static str) {
match value_type(ft) {
"String" => ("impl Into<String>".into(), ".into()"),
"Vec<u8>" => ("impl Into<Vec<u8>>".into(), ".into()"),
other => (other.into(), ""),
}
}
const KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",
"where", "while", "async", "await", "abstract", "become", "box", "do", "final", "macro",
"override", "priv", "typeof", "unsized", "virtual", "yield", "try", "gen",
];
fn snake(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
let mut out = String::new();
for (i, &c) in chars.iter().enumerate() {
if c.is_ascii_uppercase() {
let prev_lower = i > 0 && (chars[i - 1].is_ascii_lowercase() || chars[i - 1].is_ascii_digit());
let next_lower = chars.get(i + 1).is_some_and(|n| n.is_ascii_lowercase());
let prev_upper = i > 0 && chars[i - 1].is_ascii_uppercase();
let acronym_plural = chars.get(i + 1) == Some(&'s')
&& !chars.get(i + 2).is_some_and(|n| n.is_ascii_lowercase());
if prev_lower || (prev_upper && next_lower && !acronym_plural) {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
if KEYWORDS.contains(&out.as_str()) {
out.push('_');
}
out
}
fn const_name(value: &str, description: &str) -> String {
let base = if description.is_empty() { format!("VALUE_{value}") } else { description.into() };
let mut out = String::new();
for c in base.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_uppercase());
} else if !out.ends_with('_') {
out.push('_');
}
}
let out = out.trim_matches('_').to_string();
if out.is_empty() {
return format!("VALUE_{}", value.replace(|c: char| !c.is_ascii_alphanumeric(), "_"));
}
if out.chars().next().unwrap().is_ascii_digit() { format!("_{out}") } else { out }
}
impl Generator {
fn fields_sorted(&self) -> BTreeMap<Tag, &FieldDef> {
self.dd.fields_by_tag.iter().map(|(t, f)| (*t, f)).collect()
}
fn field(&self, tag: Tag) -> &FieldDef {
&self.dd.fields_by_tag[&tag]
}
fn gen_fields(&self) -> String {
let mut out = String::from(HEADER);
out.push_str("//! Typed field markers: `fields::ClOrdID::TAG` etc, with enum value\n//! constants attached (`fields::Side::BUY`).\n\n");
out.push_str("use crate::field_map::Field;\nuse crate::message::Tag;\n\n");
for (tag, f) in self.fields_sorted() {
let vt = value_type(f.field_type);
writeln!(out, "/// {} ({tag}).\npub struct {};", f.name, f.name).unwrap();
writeln!(
out,
"impl Field for {} {{ const TAG: Tag = {tag}; type Value = {vt}; }}",
f.name
)
.unwrap();
if !f.enum_values.is_empty() && matches!(vt, "char" | "String" | "i64") {
let mut seen = HashSet::new();
writeln!(out, "impl {} {{", f.name).unwrap();
for (value, description) in &f.enum_values {
let name = const_name(value, description);
if !seen.insert(name.clone()) {
continue;
}
match vt {
"char" if value.len() == 1 => writeln!(
out,
" pub const {name}: char = '{}';",
value.replace('\'', "\\'")
)
.unwrap(),
"i64" if value.parse::<i64>().is_ok() => {
writeln!(out, " pub const {name}: i64 = {value};").unwrap()
}
_ => writeln!(
out,
" pub const {name}: &'static str = {value:?};"
)
.unwrap(),
}
}
out.push_str("}\n");
}
out.push('\n');
}
out
}
fn gen_messages(&self) -> String {
let mut out = String::from(HEADER);
out.push_str("//! One module per message; structs wrap [`crate::Message`] and expose\n//! typed accessors. Constructors take the message's required fields.\n\n");
let by_name: BTreeMap<&str, &MessageDef> =
self.dd.messages.values().map(|m| (m.name.as_str(), m)).collect();
for def in by_name.values() {
let mod_name = snake(&def.name);
writeln!(out, "pub mod {mod_name} {{").unwrap();
out.push_str(
" use crate::field_map::{Field, FieldMap};\n use crate::error::ConversionError;\n use super::super::fields;\n\n",
);
self.gen_message_struct(&mut out, def);
for counter in def.field_order.iter().filter(|t| def.groups.contains_key(t)) {
let group = &def.groups[counter];
self.gen_group_struct(&mut out, group, "");
}
out.push_str("}\n");
writeln!(out, "pub use {mod_name}::{};\n", def.name).unwrap();
}
out
}
fn gen_message_struct(&self, out: &mut String, def: &MessageDef) {
let name = &def.name;
writeln!(out, " /// {} (35={}).", name, def.msg_type).unwrap();
writeln!(out, " #[derive(Debug, Clone)]\n pub struct {name}(pub crate::Message);").unwrap();
writeln!(out, " impl {name} {{").unwrap();
writeln!(out, " pub const MSG_TYPE: &'static str = {:?};", def.msg_type).unwrap();
let required: Vec<Tag> = def
.required
.iter()
.filter(|t| !def.groups.contains_key(t))
.copied()
.collect();
let args = required
.iter()
.map(|&t| {
let f = self.field(t);
let (pt, _) = param_style(f.field_type);
format!("{}: {pt}", snake(&f.name))
})
.collect::<Vec<_>>()
.join(", ");
writeln!(out, " #[allow(clippy::too_many_arguments)]").unwrap();
writeln!(out, " pub fn new({args}) -> Self {{").unwrap();
writeln!(out, " let mut m = crate::Message::with_type(Self::MSG_TYPE);").unwrap();
for &t in &required {
let f = self.field(t);
let (_, conv) = param_style(f.field_type);
writeln!(
out,
" m.body.set_field::<fields::{}>({}{conv});",
f.name,
snake(&f.name)
)
.unwrap();
}
out.push_str(" Self(m)\n }\n\n");
writeln!(
out,
" /// Wrap a generic message; fails unless 35={:?}.\n \
pub fn from_message(msg: crate::Message) -> Result<Self, crate::Message> {{\n \
if msg.msg_type().ok().as_deref() == Some(Self::MSG_TYPE) {{ Ok(Self(msg)) }} else {{ Err(msg) }}\n }}\n",
def.msg_type
)
.unwrap();
for &t in &def.field_order {
if def.groups.contains_key(&t) {
self.gen_group_accessors(out, &def.groups[&t], "self.0.body");
} else {
self.gen_field_accessors(out, t, "self.0.body");
}
}
out.push_str(" }\n");
writeln!(
out,
" impl std::ops::Deref for {name} {{\n type Target = crate::Message;\n \
fn deref(&self) -> &crate::Message {{ &self.0 }}\n }}\n \
impl std::ops::DerefMut for {name} {{\n \
fn deref_mut(&mut self) -> &mut crate::Message {{ &mut self.0 }}\n }}\n \
impl From<{name}> for crate::Message {{\n \
fn from(m: {name}) -> crate::Message {{ m.0 }}\n }}\n"
)
.unwrap();
}
fn gen_field_accessors(&self, out: &mut String, tag: Tag, map: &str) {
let f = self.field(tag);
let method = snake(&f.name);
let vt = value_type(f.field_type);
let (pt, conv) = param_style(f.field_type);
writeln!(
out,
" pub fn {method}(&self) -> Result<{vt}, ConversionError> {{ {map}.get_field::<fields::{}>() }}",
f.name
)
.unwrap();
writeln!(
out,
" pub fn set_{method}(&mut self, v: {pt}) {{ {map}.set_field::<fields::{}>(v{conv}); }}",
f.name
)
.unwrap();
writeln!(
out,
" pub fn has_{method}(&self) -> bool {{ {map}.has_field::<fields::{}>() }}",
f.name
)
.unwrap();
}
fn gen_group_accessors(&self, out: &mut String, group: &GroupDef, map: &str) {
let counter_field = self.field(group.counter);
let method = snake(&counter_field.name);
let struct_name = &counter_field.name;
writeln!(
out,
" pub fn {method}(&self) -> Result<Vec<{struct_name}>, ConversionError> {{\n \
Ok({map}.read_groups(&{method}_template())?.into_iter().map({struct_name}).collect())\n }}",
)
.unwrap();
writeln!(
out,
" pub fn set_{method}(&mut self, groups: impl IntoIterator<Item = {struct_name}>) {{\n \
let maps: Vec<FieldMap> = groups.into_iter().map(|g| g.0).collect();\n \
{map}.write_groups(&{method}_template(), &maps);\n }}",
)
.unwrap();
}
fn gen_group_struct(&self, out: &mut String, group: &GroupDef, prefix: &str) {
let counter_field = self.field(group.counter);
let struct_name = format!("{prefix}{}", counter_field.name);
let method = snake(&struct_name);
let mut members: Vec<Tag> = Vec::new();
flatten_members(group, &mut members);
writeln!(
out,
"\n pub(crate) fn {method}_template() -> crate::GroupTemplate {{\n \
crate::GroupTemplate::new({}, vec![{}])\n }}",
group.counter,
members.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(", ")
)
.unwrap();
writeln!(
out,
" /// Repeating group counted by {} ({}). Set the delimiter\n /// field ({}) first.\n \
#[derive(Debug, Clone, Default)]\n pub struct {struct_name}(pub FieldMap);",
counter_field.name, group.counter, group.delimiter
)
.unwrap();
writeln!(out, " impl {struct_name} {{").unwrap();
writeln!(out, " pub fn new() -> Self {{ Self(FieldMap::new()) }}").unwrap();
for &t in &group.member_order {
if group.groups.contains_key(&t) {
let nested = &group.groups[&t];
let nested_field = self.field(nested.counter);
let nested_struct = format!("{}{}", counter_field.name, nested_field.name);
let nested_method = snake(&nested_struct);
writeln!(
out,
" pub fn {nested_method}(&self) -> Result<Vec<{nested_struct}>, ConversionError> {{\n \
Ok(self.0.read_groups(&{nested_method}_template())?.into_iter().map({nested_struct}).collect())\n }}",
)
.unwrap();
writeln!(
out,
" pub fn set_{nested_method}(&mut self, groups: impl IntoIterator<Item = {nested_struct}>) {{\n \
let maps: Vec<FieldMap> = groups.into_iter().map(|g| g.0).collect();\n \
self.0.write_groups(&{nested_method}_template(), &maps);\n }}",
)
.unwrap();
} else {
self.gen_field_accessors(out, t, "self.0");
}
}
out.push_str(" }\n");
for &t in &group.member_order {
if let Some(nested) = group.groups.get(&t) {
self.gen_group_struct(out, nested, &counter_field.name);
}
}
}
fn gen_mod(&self) -> String {
let mut out = String::from(HEADER);
writeln!(
out,
"//! Typed messages for {} — generated from its spec XML.\n",
self.dd.begin_string
)
.unwrap();
out.push_str("pub mod fields;\npub mod messages;\npub use messages::*;\n\n");
writeln!(out, "pub const BEGIN_STRING: &str = {:?};\n", self.dd.begin_string).unwrap();
let by_name: BTreeMap<&str, &MessageDef> =
self.dd.messages.values().map(|m| (m.name.as_str(), m)).collect();
out.push_str("/// Every message type of this FIX version, for typed dispatch.\n#[derive(Debug, Clone)]\npub enum AnyMessage {\n");
for def in by_name.values() {
writeln!(out, " {}(messages::{}::{}),", def.name, snake(&def.name), def.name).unwrap();
}
out.push_str(" /// MsgType not defined by this dictionary.\n Unknown(crate::Message),\n}\n\n");
out.push_str(
"/// Classify a generic message into its typed representation.\npub fn classify(msg: crate::Message) -> AnyMessage {\n match msg.msg_type().ok().as_deref().unwrap_or(\"\") {\n",
);
for def in by_name.values() {
writeln!(
out,
" {:?} => AnyMessage::{}(messages::{}::{}(msg)),",
def.msg_type,
def.name,
snake(&def.name),
def.name
)
.unwrap();
}
out.push_str(" _ => AnyMessage::Unknown(msg),\n }\n}\n");
out
}
}
fn flatten_members(group: &GroupDef, out: &mut Vec<Tag>) {
for &t in &group.member_order {
out.push(t);
if let Some(nested) = group.groups.get(&t) {
flatten_members(nested, out);
}
}
}