1use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, OnceLock};
15
16use crate::formats::SpecError;
17
18pub const MAX_FILE: u64 = 64 << 20;
20const MAX_DEPTH: usize = 64;
22pub const MAX_NAME: usize = 256;
24pub const MAX_TAG: u32 = i32::MAX as u32;
26
27const BUILT_IN: &str = include_str!("dictionary.tsv");
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FixType {
33 Int,
34 Float,
35 Timestamp,
37 Date,
39 Bool,
41 Length,
43 Data,
45 Text,
46}
47
48impl FixType {
49 pub fn parse(name: &str) -> Option<Self> {
51 Some(match name.to_ascii_uppercase().as_str() {
52 "INT" | "SEQNUM" | "NUMINGROUP" | "TAGNUM" | "DAYOFMONTH" => Self::Int,
53 "LENGTH" => Self::Length,
54 "FLOAT" | "PRICE" | "QTY" | "AMT" | "PRICEOFFSET" | "PERCENTAGE" | "QUANTITY" => {
55 Self::Float
56 }
57 "UTCTIMESTAMP" | "TIMESTAMP" | "DATETIME" => Self::Timestamp,
58 "UTCDATEONLY" | "UTCDATE" | "LOCALMKTDATE" | "DATE" => Self::Date,
59 "BOOLEAN" | "BOOL" => Self::Bool,
60 "DATA" | "XMLDATA" => Self::Data,
61 "STRING"
62 | "CHAR"
63 | "CURRENCY"
64 | "EXCHANGE"
65 | "COUNTRY"
66 | "MONTHYEAR"
67 | "MULTIPLECHARVALUE"
68 | "MULTIPLESTRINGVALUE"
69 | "MULTIPLEVALUESTRING"
70 | "XID"
71 | "XIDREF"
72 | "LANGUAGE"
73 | "LOCALMKTTIME"
74 | "UTCTIMEONLY"
75 | "TZTIMEONLY"
76 | "TZTIMESTAMP"
77 | "TEXT"
78 | "STR" => Self::Text,
79 _ => return None,
80 })
81 }
82
83 pub fn reads(self, value: &str) -> bool {
85 match self {
86 Self::Int | Self::Length => value.parse::<i64>().is_ok(),
87 Self::Float => value.parse::<f64>().is_ok_and(f64::is_finite),
88 Self::Timestamp => parse_timestamp(value).is_some(),
89 Self::Date => parse_date(value).is_some(),
90 Self::Bool => matches!(value, "Y" | "N"),
91 Self::Data | Self::Text => true,
92 }
93 }
94}
95
96pub fn parse_timestamp(text: &str) -> Option<i64> {
99 let (date, time) = text.split_once('-')?;
100 let days = parse_date(date)?;
101 let (hms, fraction) = match time.split_once('.') {
102 Some((hms, f)) => (hms, Some(f)),
103 None => (time, None),
104 };
105 let b = hms.as_bytes();
106 if b.len() != 8 || b[2] != b':' || b[5] != b':' {
107 return None;
108 }
109 let two = |i: usize| -> Option<i64> {
110 let (a, b) = (b[i], b[i + 1]);
111 (a.is_ascii_digit() && b.is_ascii_digit()).then(|| i64::from((a - b'0') * 10 + (b - b'0')))
112 };
113 let (h, m, s) = (two(0)?, two(3)?, two(6)?);
114 if h > 23 || m > 59 || s > 59 {
115 return None;
116 }
117 let nanos = match fraction {
118 None => 0,
119 Some(f) if !f.is_empty() && f.len() <= 9 && f.bytes().all(|c| c.is_ascii_digit()) => {
120 f.parse::<i64>().ok()? * 10i64.pow(9 - f.len() as u32)
121 }
122 Some(_) => return None,
123 };
124 days.checked_mul(86_400_000_000_000)?
126 .checked_add((h * 3600 + m * 60 + s) * 1_000_000_000 + nanos)
127}
128
129pub fn parse_date(text: &str) -> Option<i64> {
131 if text.len() != 8 || !text.bytes().all(|b| b.is_ascii_digit()) {
132 return None;
133 }
134 let y: i32 = text[..4].parse().ok()?;
135 let m: u32 = text[4..6].parse().ok()?;
136 let d: u32 = text[6..].parse().ok()?;
137 let date = chrono::NaiveDate::from_ymd_opt(y, m, d)?;
138 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?;
139 Some((date - epoch).num_days())
140}
141
142#[derive(Debug, Clone, Default, PartialEq)]
144pub struct TagDef {
145 pub name: Option<String>,
146 pub ty: Option<FixType>,
147 pub enums: HashMap<String, String>,
149 pub data: Option<u32>,
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
155pub struct Match {
156 pub sender: Option<String>,
158 pub target: Option<String>,
160 pub begin_string: Option<String>,
162}
163
164impl Match {
165 pub fn matches(&self, sender: Option<&str>, target: Option<&str>, begin: Option<&str>) -> bool {
167 let one = |want: &Option<String>, have: Option<&str>| {
168 want.as_deref().is_none_or(|want| Some(want) == have)
169 };
170 one(&self.sender, sender) && one(&self.target, target) && one(&self.begin_string, begin)
171 }
172
173 pub fn summary(&self) -> String {
175 let mut parts = Vec::new();
176 if let Some(s) = &self.sender {
177 parts.push(format!("sender {s}"));
178 }
179 if let Some(t) = &self.target {
180 parts.push(format!("target {t}"));
181 }
182 if let Some(b) = &self.begin_string {
183 parts.push(format!("begin string {b}"));
184 }
185 parts.join(", ")
186 }
187}
188
189#[derive(Debug, Clone, PartialEq)]
191pub struct Dictionary {
192 pub name: String,
194 pub path: Option<PathBuf>,
195 pub matcher: Match,
196 pub tags: HashMap<u32, TagDef>,
197}
198
199pub fn built_in() -> Arc<Dictionary> {
201 static DICT: OnceLock<Arc<Dictionary>> = OnceLock::new();
202 DICT.get_or_init(|| Arc::new(parse_table(BUILT_IN))).clone()
203}
204
205fn parse_table(text: &str) -> Dictionary {
206 let mut tags = HashMap::new();
207 for line in text
208 .lines()
209 .filter(|l| !l.starts_with('#') && !l.is_empty())
210 {
211 let mut cols = line.split('\t');
212 let (Some(tag), Some(name), Some(ty), data, enums) = (
213 cols.next().and_then(|t| t.parse::<u32>().ok()),
214 cols.next(),
215 cols.next(),
216 cols.next().and_then(|d| d.parse::<u32>().ok()),
217 cols.next().unwrap_or_default(),
218 ) else {
219 continue;
220 };
221 let enums = enums
222 .split(';')
223 .filter_map(|pair| pair.split_once('='))
224 .map(|(code, name)| (code.to_string(), name.to_string()))
225 .collect();
226 tags.insert(
227 tag,
228 TagDef {
229 name: Some(name.to_string()),
230 ty: FixType::parse(ty),
231 enums,
232 data,
233 },
234 );
235 }
236 Dictionary {
237 name: "fix".to_string(),
238 path: None,
239 matcher: Match::default(),
240 tags,
241 }
242}
243
244fn error(path: Option<&Path>, line: usize, column: usize, message: impl Into<String>) -> SpecError {
245 SpecError {
246 path: path.map(Path::to_path_buf),
247 line,
248 column,
249 message: message.into(),
250 }
251}
252
253fn line_column(text: &str, offset: usize) -> (usize, usize) {
255 let mut offset = offset.min(text.len());
256 while !text.is_char_boundary(offset) {
257 offset -= 1;
258 }
259 let before = &text[..offset];
260 let line = before.matches('\n').count() + 1;
261 let column = before
262 .rsplit('\n')
263 .next()
264 .unwrap_or_default()
265 .chars()
266 .count()
267 + 1;
268 (line, column)
269}
270
271pub fn is_fix_toml(text: &str) -> bool {
273 text.parse::<toml::Table>()
274 .is_ok_and(|t| t.get("kind").and_then(|k| k.as_str()) == Some("fix"))
275}
276
277fn short(name: &str) -> String {
278 let mut cut = name.len().min(MAX_NAME);
279 while !name.is_char_boundary(cut) {
280 cut -= 1;
281 }
282 name[..cut].to_string()
283}
284
285impl Dictionary {
286 pub fn load(path: &Path) -> Result<Option<Self>, SpecError> {
289 let size = std::fs::metadata(path)
290 .map_err(|e| {
291 error(
292 Some(path),
293 0,
294 0,
295 format!(
296 "could not read it. {}",
297 crate::error_display::user_message_from_io(&e, None)
298 ),
299 )
300 })?
301 .len();
302 if size > MAX_FILE {
303 return Err(error(
304 Some(path),
305 0,
306 0,
307 format!(
308 "{} MiB is past the {} MiB a dictionary may be",
309 size >> 20,
310 MAX_FILE >> 20
311 ),
312 ));
313 }
314 let text = std::fs::read_to_string(path).map_err(|e| {
315 error(
316 Some(path),
317 0,
318 0,
319 format!(
320 "could not read it. {}",
321 crate::error_display::user_message_from_io(&e, None)
322 ),
323 )
324 })?;
325 let xml = path
326 .extension()
327 .is_some_and(|e| e.eq_ignore_ascii_case("xml"));
328 if xml {
329 let stem = path
330 .file_stem()
331 .map(|s| s.to_string_lossy().into_owned())
332 .unwrap_or_default();
333 Self::parse_xml(&text, &stem, Some(path))
334 } else if is_fix_toml(&text) {
335 Self::parse_toml(&text, Some(path)).map(Some)
336 } else {
337 Ok(None)
338 }
339 }
340
341 pub fn parse_toml(text: &str, path: Option<&Path>) -> Result<Self, SpecError> {
343 let table: toml::Table = text.parse().map_err(|e: toml::de::Error| {
344 let (line, column) = e
345 .span()
346 .map_or((0, 0), |span| line_column(text, span.start));
347 error(path, line, column, e.message().to_string())
348 })?;
349 let at = |key: &str| {
350 text.lines()
352 .position(|l| l.trim_start().starts_with(key))
353 .map_or(0, |i| i + 1)
354 };
355 let fail = |key: &str, message: String| error(path, at(key), 1, message);
356 for key in table.keys() {
357 if !matches!(
358 key.as_str(),
359 "name" | "kind" | "description" | "match" | "tags"
360 ) {
361 return Err(fail(
362 key,
363 format!(
364 "unknown key `{key}`; a dictionary of kind = \"fix\" has name, kind, description, match and tags"
365 ),
366 ));
367 }
368 }
369 let name = match table.get("name") {
370 Some(toml::Value::String(name)) if crate::formats::is_spec_name(name) => name.clone(),
371 Some(_) => {
372 return Err(fail(
373 "name",
374 "name: expected a namespaced name such as acme.fix.broker-x".into(),
375 ));
376 }
377 None => {
378 return Err(error(
379 path,
380 0,
381 0,
382 "missing `name`, such as name = \"acme.fix.broker-x\"",
383 ));
384 }
385 };
386 let mut matcher = Match::default();
387 if let Some(m) = table.get("match") {
388 let Some(m) = m.as_table() else {
389 return Err(fail(
390 "match",
391 "match: expected a table such as { sender = \"BROKERX\" }".into(),
392 ));
393 };
394 for (key, value) in m {
395 let Some(value) = value.as_str() else {
396 return Err(fail("match", format!("match.{key}: expected a string")));
397 };
398 let slot = match key.as_str() {
399 "sender" => &mut matcher.sender,
400 "target" => &mut matcher.target,
401 "begin_string" => &mut matcher.begin_string,
402 _ => {
403 return Err(fail(
404 "match",
405 format!("match.{key}: expected sender, target or begin_string"),
406 ));
407 }
408 };
409 *slot = Some(value.to_string());
410 }
411 }
412 let mut tags = HashMap::new();
413 if let Some(t) = table.get("tags") {
414 let Some(t) = t.as_table() else {
415 return Err(fail(
416 "tags",
417 "tags: expected a table such as { 9001 = \"AlgoName\" }".into(),
418 ));
419 };
420 for (key, value) in t {
421 let tag = key
422 .parse::<u32>()
423 .ok()
424 .filter(|t| (1..=MAX_TAG).contains(t))
425 .ok_or_else(|| fail("tags", format!("tags.{key}: expected a tag number")))?;
426 let def = match value {
427 toml::Value::String(name) => TagDef {
428 name: Some(short(name)),
429 ..TagDef::default()
430 },
431 toml::Value::Table(def) => tag_def(def, key).map_err(|m| fail("tags", m))?,
432 _ => {
433 return Err(fail(
434 "tags",
435 format!(
436 "tags.{key}: expected a name or a table with name, type and enum"
437 ),
438 ));
439 }
440 };
441 tags.insert(tag, def);
442 }
443 }
444 Ok(Self {
445 name,
446 path: path.map(Path::to_path_buf),
447 matcher,
448 tags,
449 })
450 }
451
452 pub fn parse_xml(
456 text: &str,
457 name: &str,
458 path: Option<&Path>,
459 ) -> Result<Option<Self>, SpecError> {
460 let mut stack: Vec<String> = Vec::new();
461 let mut tags = HashMap::new();
462 let mut field: Option<(u32, TagDef)> = None;
463 let mut root = None;
464 let mut pos = 0;
465 let bytes = text.as_bytes();
466 while let Some(open) = text[pos..].find('<').map(|i| pos + i) {
467 let rest = &text[open..];
468 let fail = |message: &str| {
469 let (line, column) = line_column(text, open);
470 error(path, line, column, message.to_string())
471 };
472 if rest.starts_with("<!--") {
473 pos = match rest.find("-->") {
474 Some(end) => open + end + 3,
475 None => return Err(fail("a comment is not closed")),
476 };
477 continue;
478 }
479 if rest.starts_with("<?") || rest.starts_with("<!") {
480 pos = match rest.find('>') {
481 Some(end) => open + end + 1,
482 None => return Err(fail("markup is not closed")),
483 };
484 continue;
485 }
486 let Some(end) = markup_end(&bytes[open..]) else {
487 return Err(fail("a tag is not closed"));
488 };
489 let inner = &text[open + 1..open + end - 1];
490 pos = open + end;
491 if let Some(closing) = inner.strip_prefix('/') {
492 let closing = closing.trim();
493 if stack.last().map(String::as_str) != Some(closing) {
494 return Err(fail(&format!("</{closing}> closes no <{closing}>")));
495 }
496 stack.pop();
497 if closing == "field"
498 && let Some((tag, def)) = field.take()
499 {
500 tags.insert(tag, def);
501 }
502 continue;
503 }
504 let empty = inner.ends_with('/');
505 let inner = inner.strip_suffix('/').unwrap_or(inner);
506 let (element, attrs) = match inner.find(|c: char| c.is_ascii_whitespace()) {
507 Some(i) => (&inner[..i], &inner[i..]),
508 None => (inner, ""),
509 };
510 let attrs = attributes(attrs).map_err(|m| fail(&m))?;
511 let get = |key: &str| {
512 attrs
513 .iter()
514 .find(|(k, _)| k == key)
515 .map(|(_, v)| v.as_str())
516 };
517 if root.is_none() {
518 if element != "fix" {
519 return Ok(None);
520 }
521 let begin = match (get("type"), get("major"), get("minor")) {
522 (_, Some("5"), _) | (Some("FIXT"), _, _) => Some("FIXT.1.1".to_string()),
523 (_, Some(major), Some(minor)) => Some(format!("FIX.{major}.{minor}")),
524 _ => None,
525 };
526 root = Some(begin);
527 }
528 let in_fields = stack.last().map(String::as_str) == Some("fields");
529 if element == "field" && in_fields {
530 let tag = get("number")
531 .and_then(|n| n.parse::<u32>().ok())
532 .filter(|t| (1..=MAX_TAG).contains(t))
533 .ok_or_else(|| fail("<field> needs a number"))?;
534 let def = TagDef {
535 name: get("name").map(short),
536 ty: get("type").and_then(FixType::parse),
537 enums: HashMap::new(),
538 data: None,
539 };
540 if empty {
541 tags.insert(tag, def);
542 } else {
543 field = Some((tag, def));
544 }
545 } else if element == "value"
546 && stack.last().map(String::as_str) == Some("field")
547 && let Some((_, def)) = field.as_mut()
548 && let (Some(code), Some(description)) = (get("enum"), get("description"))
549 {
550 def.enums.insert(short(code), camel(description));
551 }
552 if !empty {
553 if stack.len() >= MAX_DEPTH {
554 return Err(fail("elements nest too deep"));
555 }
556 stack.push(element.to_string());
557 }
558 }
559 let Some(begin) = root else {
560 return Ok(None);
561 };
562 let by_name: HashMap<String, u32> = tags
564 .iter()
565 .filter(|(_, d)| d.ty == Some(FixType::Data))
566 .filter_map(|(t, d)| Some((d.name.clone()?, *t)))
567 .collect();
568 for def in tags.values_mut() {
569 if def.ty == Some(FixType::Length)
570 && let Some(name) = &def.name
571 {
572 def.data = ["Length", "Len"]
573 .iter()
574 .find_map(|s| name.strip_suffix(s))
575 .and_then(|stem| by_name.get(stem).copied());
576 }
577 }
578 Ok(Some(Self {
579 name: name.to_string(),
580 path: path.map(Path::to_path_buf),
581 matcher: Match {
582 begin_string: begin,
583 ..Match::default()
584 },
585 tags,
586 }))
587 }
588}
589
590fn tag_def(def: &toml::Table, key: &str) -> Result<TagDef, String> {
593 let mut out = TagDef::default();
594 for (k, v) in def {
595 match k.as_str() {
596 "name" => {
597 out.name =
598 Some(short(v.as_str().ok_or_else(|| {
599 format!("tags.{key}.name: expected a string")
600 })?));
601 }
602 "type" => {
603 let name = v
604 .as_str()
605 .ok_or_else(|| format!("tags.{key}.type: expected a string"))?;
606 out.ty = Some(FixType::parse(name).ok_or_else(|| {
607 format!(
608 "tags.{key}.type: expected int, float, price, qty, string, char, timestamp, date, bool, length or data"
609 )
610 })?);
611 }
612 "enum" => {
613 let table = v.as_table().ok_or_else(|| {
614 format!("tags.{key}.enum: expected a table such as {{ 1 = \"Low\" }}")
615 })?;
616 for (code, name) in table {
617 let name = name
618 .as_str()
619 .ok_or_else(|| format!("tags.{key}.enum.{code}: expected a string"))?;
620 out.enums.insert(short(code), short(name));
621 }
622 }
623 "data" => {
624 let tag = v
625 .as_integer()
626 .and_then(|t| u32::try_from(t).ok())
627 .filter(|t| (1..=MAX_TAG).contains(t))
628 .ok_or_else(|| {
629 format!("tags.{key}.data: expected the tag number this length sizes")
630 })?;
631 out.data = Some(tag);
632 out.ty.get_or_insert(FixType::Length);
633 }
634 other => {
635 return Err(format!(
636 "tags.{key}.{other}: unknown key; a tag has name, type, enum and data"
637 ));
638 }
639 }
640 }
641 Ok(out)
642}
643
644fn markup_end(bytes: &[u8]) -> Option<usize> {
646 let mut quote = None;
647 for (i, &b) in bytes.iter().enumerate().skip(1) {
648 match (quote, b) {
649 (None, b'"' | b'\'') => quote = Some(b),
650 (Some(q), b) if b == q => quote = None,
651 (None, b'>') => return Some(i + 1),
652 _ => {}
653 }
654 }
655 None
656}
657
658fn attributes(text: &str) -> Result<Vec<(String, String)>, String> {
660 let mut out = Vec::new();
661 let mut rest = text.trim_start();
662 while !rest.is_empty() {
663 let eq = rest.find('=').ok_or("an attribute has no value")?;
664 let key = rest[..eq].trim().to_string();
665 let after = rest[eq + 1..].trim_start();
666 let quote = after.chars().next().ok_or("an attribute has no value")?;
667 if quote != '"' && quote != '\'' {
668 return Err("an attribute's value is not quoted".into());
669 }
670 let close = after[1..]
671 .find(quote)
672 .ok_or("an attribute's quote is not closed")?;
673 out.push((key, unescape(&after[1..1 + close])));
674 rest = after[close + 2..].trim_start();
675 if out.len() > 64 {
676 return Err("too many attributes".into());
677 }
678 }
679 Ok(out)
680}
681
682fn unescape(text: &str) -> String {
683 if !text.contains('&') {
684 return text.to_string();
685 }
686 text.replace("<", "<")
687 .replace(">", ">")
688 .replace(""", "\"")
689 .replace("'", "'")
690 .replace("&", "&")
691}
692
693pub fn camel(words: &str) -> String {
695 if words.chars().any(|c| c.is_lowercase()) {
696 return short(
697 &words
698 .split('_')
699 .map(|p| {
700 let mut c = p.chars();
701 c.next()
702 .map(|f| f.to_uppercase().chain(c).collect::<String>())
703 .unwrap_or_default()
704 })
705 .collect::<String>(),
706 );
707 }
708 short(
709 &words
710 .split('_')
711 .filter(|p| !p.is_empty())
712 .map(|p| {
713 let lower = p.to_lowercase();
714 let mut c = lower.chars();
715 c.next()
716 .map(|f| f.to_uppercase().chain(c).collect::<String>())
717 .unwrap_or_default()
718 })
719 .collect::<String>(),
720 )
721}
722
723#[derive(Debug, Clone)]
726pub struct Layers {
727 pub dicts: Vec<Arc<Dictionary>>,
728 data: HashMap<u32, u32>,
730}
731
732impl Default for Layers {
733 fn default() -> Self {
734 Self::new(Vec::new())
735 }
736}
737
738#[derive(Debug, Clone, Default, PartialEq)]
740pub struct Resolved {
741 pub name: Option<String>,
742 pub named_by: Option<usize>,
744 pub ty: Option<FixType>,
745 pub enums: HashMap<String, String>,
746}
747
748impl Layers {
749 pub fn new(custom: Vec<Arc<Dictionary>>) -> Self {
751 let mut dicts = vec![built_in()];
752 dicts.extend(custom);
753 let mut data = HashMap::new();
754 for dict in &dicts {
755 for (tag, def) in &dict.tags {
756 if let Some(d) = def.data {
757 data.insert(*tag, d);
758 }
759 }
760 }
761 Self { dicts, data }
762 }
763
764 pub fn data_tag(&self, length: u32) -> Option<u32> {
766 self.data.get(&length).copied()
767 }
768
769 pub fn applying(
772 &self,
773 sender: Option<&str>,
774 target: Option<&str>,
775 begin: Option<&str>,
776 ) -> Vec<usize> {
777 (0..self.dicts.len())
778 .filter(|&i| self.dicts[i].matcher.matches(sender, target, begin))
779 .collect()
780 }
781
782 pub fn resolve(&self, applying: &[usize], tag: u32) -> Resolved {
784 let mut out = Resolved::default();
785 for &i in applying {
786 let Some(def) = self.dicts[i].tags.get(&tag) else {
787 continue;
788 };
789 if let Some(name) = &def.name {
790 out.name = Some(name.clone());
791 out.named_by = Some(i);
792 }
793 if def.ty.is_some() {
794 out.ty = def.ty;
795 }
796 for (code, name) in &def.enums {
797 out.enums.insert(code.clone(), name.clone());
798 }
799 }
800 out
801 }
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807
808 #[test]
809 fn the_built_in_dictionary_names_tags_and_codes() {
810 let dict = built_in();
811 let msg_type = &dict.tags[&35];
812 assert_eq!(msg_type.name.as_deref(), Some("MsgType"));
813 assert_eq!(msg_type.enums["D"], "NewOrderSingle");
814 assert_eq!(dict.tags[&54].enums["1"], "Buy");
815 assert!(
816 dict.tags[&8].enums.is_empty(),
817 "FIX.4.4 is not renamed Fix44"
818 );
819 assert_eq!(dict.tags[&44].ty, Some(FixType::Float));
820 assert_eq!(dict.tags[&52].ty, Some(FixType::Timestamp));
821 assert_eq!(dict.tags[&95].data, Some(96));
822 assert_eq!(dict.tags[&212].data, Some(213));
823 assert_eq!(dict.tags[&93].data, Some(89));
824 }
825
826 #[test]
827 fn timestamps_and_dates() {
828 assert_eq!(parse_timestamp("19700101-00:00:01"), Some(1_000_000_000));
829 assert_eq!(parse_timestamp("19700101-00:00:00.5"), Some(500_000_000));
830 assert_eq!(
831 parse_timestamp("19700102-00:00:00.000000001"),
832 Some(86_400_000_000_001)
833 );
834 assert_eq!(parse_timestamp("19700101-24:00:00"), None);
835 assert_eq!(parse_timestamp("19700101-00:00:00."), None);
836 assert_eq!(parse_timestamp("19701301-00:00:00"), None);
837 assert_eq!(parse_date("19700201"), Some(31));
838 assert_eq!(parse_timestamp("99991231-23:59:59"), None, "past 2262");
839 assert_eq!(parse_timestamp("00010101-00:00:00"), None, "before 1677");
840 }
841
842 const TOML: &str = r#"
843name = "acme.fix.broker-x"
844kind = "fix"
845match = { sender = "BROKERX", begin_string = "FIX.4.4" }
846tags = { 9001 = "AlgoName", 9002 = { name = "Urgency", type = "int", enum = { 1 = "Low", 2 = "High" } }, 54 = { enum = { Z = "Zap" } } }
847"#;
848
849 #[test]
850 fn a_toml_dictionary() {
851 assert!(is_fix_toml(TOML));
852 let dict = Dictionary::parse_toml(TOML, None).unwrap();
853 assert_eq!(dict.name, "acme.fix.broker-x");
854 assert_eq!(dict.matcher.sender.as_deref(), Some("BROKERX"));
855 assert_eq!(dict.tags[&9001].name.as_deref(), Some("AlgoName"));
856 assert_eq!(dict.tags[&9002].ty, Some(FixType::Int));
857 assert_eq!(dict.tags[&9002].enums["2"], "High");
858
859 let layers = Layers::new(vec![Arc::new(dict)]);
860 let broker = layers.applying(Some("BROKERX"), None, Some("FIX.4.4"));
861 let other = layers.applying(Some("OTHER"), None, Some("FIX.4.4"));
862 assert_eq!(broker, [0, 1]);
863 assert_eq!(other, [0]);
864 let side = layers.resolve(&broker, 54);
865 assert_eq!(side.name.as_deref(), Some("Side"));
866 assert_eq!(side.enums["Z"], "Zap");
867 assert_eq!(side.enums["1"], "Buy");
868 assert_eq!(layers.resolve(&other, 9001).name, None);
869 }
870
871 #[test]
872 fn toml_errors_say_where() {
873 for (text, said) in [
874 (
875 "name = \"a.b\"\nkind = \"fix\"\ncolour = 1",
876 "Unknown key `colour`",
877 ),
878 ("name = \"plain\"\nkind = \"fix\"", "namespaced"),
879 (
880 "name = \"a.b\"\nkind = \"fix\"\ntags = { x = \"A\" }",
881 "tags.x: expected a tag number",
882 ),
883 (
884 "name = \"a.b\"\nkind = \"fix\"\ntags = { 1 = { type = \"blob\" } }",
885 "tags.1.type",
886 ),
887 (
888 "name = \"a.b\"\nkind = \"fix\"\nmatch = { venue = \"X\" }",
889 "match.venue",
890 ),
891 ("name = \"a.b\"\nkind = \"fix\"\n[[", ""),
892 ] {
893 let e = Dictionary::parse_toml(text, Some(Path::new("d.toml"))).unwrap_err();
894 let e = e.to_string();
895 assert!(e.contains(said), "{e}");
896 assert!(e.starts_with("\"d.toml\":"), "{e}");
897 }
898 }
899
900 const XML: &str = r#"<?xml version="1.0"?>
901<!-- custom fields -->
902<fix type='FIX' major='4' minor='4' servicepack='0'>
903 <header><field name='BeginString' required='Y'/></header>
904 <messages><message name='Heartbeat' msgtype='0' msgcat='admin'><field name='TestReqID' required='N'/></message></messages>
905 <fields>
906 <field number='5001' name='Strategy' type='STRING'>
907 <value enum='V' description='VWAP_PLUS' />
908 <value enum='T' description="TWAP & MORE" />
909 </field>
910 <field number='5002' name='SpecialLen' type='LENGTH'/>
911 <field number='5003' name='Special' type='DATA'/>
912 </fields>
913</fix>"#;
914
915 #[test]
916 fn a_quickfix_xml_dictionary() {
917 let dict = Dictionary::parse_xml(XML, "custom44", None)
918 .unwrap()
919 .unwrap();
920 assert_eq!(dict.matcher.begin_string.as_deref(), Some("FIX.4.4"));
921 assert_eq!(dict.tags.len(), 3);
922 assert_eq!(dict.tags[&5001].enums["V"], "VwapPlus");
923 assert_eq!(dict.tags[&5001].enums["T"], "Twap & more");
924 assert_eq!(dict.tags[&5002].data, Some(5003));
925 assert!(
926 Dictionary::parse_xml("<gpx></gpx>", "x", None)
927 .unwrap()
928 .is_none()
929 );
930 assert!(
931 Dictionary::parse_xml("<fix><fields><field number='x'/></fields></fix>", "x", None)
932 .is_err()
933 );
934 assert!(Dictionary::parse_xml("<fix><fields></field></fix>", "x", None).is_err());
935 }
936}