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