use crate::error::{DecodeError, EncodeError, InvalidFieldTag};
use bytes::Bytes;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
pub const USER_DEFINED_TAG_MIN: u32 = 5000;
pub const USER_DEFINED_TAG_MAX: u32 = 9999;
pub const USER_DEFINED_EXT_TAG_MIN: u32 = 20000;
pub const USER_DEFINED_EXT_TAG_MAX: u32 = 39999;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct FieldTag(u32);
impl FieldTag {
#[inline]
#[must_use]
pub const fn new(tag: u32) -> Self {
Self(tag)
}
#[inline]
pub const fn try_new(tag: u32) -> Result<Self, InvalidFieldTag> {
if tag == 0 {
return Err(InvalidFieldTag::new(tag));
}
Ok(Self(tag))
}
#[inline]
#[must_use]
pub const fn value(self) -> u32 {
self.0
}
#[inline]
#[must_use]
pub const fn is_valid(self) -> bool {
self.0 >= 1
}
#[inline]
#[must_use]
pub const fn is_standard(self) -> bool {
self.0 >= 1 && self.0 < USER_DEFINED_TAG_MIN
}
#[inline]
#[must_use]
pub const fn is_user_defined(self) -> bool {
(self.0 >= USER_DEFINED_TAG_MIN && self.0 <= USER_DEFINED_TAG_MAX)
|| (self.0 >= USER_DEFINED_EXT_TAG_MIN && self.0 <= USER_DEFINED_EXT_TAG_MAX)
}
}
impl From<u32> for FieldTag {
fn from(tag: u32) -> Self {
Self(tag)
}
}
impl From<FieldTag> for u32 {
fn from(tag: FieldTag) -> Self {
tag.0
}
}
impl fmt::Display for FieldTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FieldRef<'a> {
pub tag: u32,
pub value: &'a [u8],
}
impl<'a> FieldRef<'a> {
#[inline]
#[must_use]
pub const fn new(tag: u32, value: &'a [u8]) -> Self {
Self { tag, value }
}
#[inline]
#[must_use]
pub const fn tag(&self) -> FieldTag {
FieldTag(self.tag)
}
pub fn as_str(&self) -> Result<&'a str, DecodeError> {
std::str::from_utf8(self.value).map_err(DecodeError::from)
}
pub fn to_string(&self) -> Result<String, DecodeError> {
self.as_str().map(String::from)
}
pub fn parse<T: FromStr>(&self) -> Result<T, DecodeError> {
let s = self.as_str()?;
s.parse().map_err(|_| DecodeError::InvalidFieldValue {
tag: self.tag,
reason: format!("failed to parse '{}' as {}", s, std::any::type_name::<T>()),
})
}
pub fn as_u64(&self) -> Result<u64, DecodeError> {
self.parse()
}
pub fn as_i64(&self) -> Result<i64, DecodeError> {
self.parse()
}
pub fn as_decimal(&self) -> Result<Decimal, DecodeError> {
self.parse()
}
pub fn as_bool(&self) -> Result<bool, DecodeError> {
match self.value {
b"Y" => Ok(true),
b"N" => Ok(false),
_ => Err(DecodeError::InvalidFieldValue {
tag: self.tag,
reason: "expected 'Y' or 'N'".to_string(),
}),
}
}
pub fn as_char(&self) -> Result<char, DecodeError> {
if self.value.len() == 1 && self.value[0].is_ascii() {
Ok(self.value[0] as char)
} else {
Err(DecodeError::InvalidFieldValue {
tag: self.tag,
reason: "expected single ASCII character".to_string(),
})
}
}
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &'a [u8] {
self.value
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.value.len()
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.value.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldValue {
String(String),
Int(i64),
UInt(u64),
Decimal(Decimal),
Bool(bool),
Char(char),
Data(Bytes),
}
impl FieldValue {
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
#[must_use]
pub const fn as_i64(&self) -> Option<i64> {
match self {
Self::Int(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub const fn as_u64(&self) -> Option<u64> {
match self {
Self::UInt(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub const fn as_decimal(&self) -> Option<Decimal> {
match self {
Self::Decimal(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub const fn as_char(&self) -> Option<char> {
match self {
Self::Char(v) => Some(*v),
_ => None,
}
}
}
impl fmt::Display for FieldValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::String(s) => write!(f, "{}", s),
Self::Int(v) => write!(f, "{}", v),
Self::UInt(v) => write!(f, "{}", v),
Self::Decimal(v) => write!(f, "{}", v),
Self::Bool(v) => write!(f, "{}", if *v { "Y" } else { "N" }),
Self::Char(c) => write!(f, "{}", c),
Self::Data(d) => write!(f, "<{} bytes>", d.len()),
}
}
}
pub trait FixField: Sized {
const TAG: u32;
type Value;
fn decode(bytes: &[u8]) -> Result<Self::Value, DecodeError>;
fn encode(value: &Self::Value, buf: &mut Vec<u8>) -> Result<(), EncodeError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn ok<T, E: fmt::Debug>(result: Result<T, E>, what: &str) -> T {
match result {
Ok(value) => value,
Err(err) => panic!("{what}: {err:?}"),
}
}
#[track_caller]
fn assert_invalid_field_value<T: fmt::Debug>(result: Result<T, DecodeError>, tag: u32) {
match result {
Err(DecodeError::InvalidFieldValue { tag: actual, .. }) => assert_eq!(actual, tag),
other => panic!("expected InvalidFieldValue for tag {tag}, got {other:?}"),
}
}
#[test]
fn test_field_tag_standard_range() {
let tag = FieldTag::new(35);
assert_eq!(tag.value(), 35);
assert!(tag.is_valid());
assert!(tag.is_standard());
assert!(!tag.is_user_defined());
}
#[test]
fn test_field_tag_zero_is_neither_standard_nor_user_defined() {
let zero = FieldTag::new(0);
assert!(!zero.is_valid());
assert!(!zero.is_standard());
assert!(!zero.is_user_defined());
assert_eq!(FieldTag::try_new(0), Err(InvalidFieldTag::new(0)));
}
#[test]
fn test_field_tag_boundary_4999_is_standard() {
let tag = ok(FieldTag::try_new(4999), "4999 is a legal tag");
assert!(tag.is_standard());
assert!(!tag.is_user_defined());
}
#[test]
fn test_field_tag_boundary_5000_is_user_defined() {
let tag = ok(FieldTag::try_new(5000), "5000 is a legal tag");
assert!(!tag.is_standard());
assert!(tag.is_user_defined());
assert_eq!(tag.value(), USER_DEFINED_TAG_MIN);
}
#[test]
fn test_field_tag_boundary_9999_is_user_defined() {
let tag = ok(FieldTag::try_new(9999), "9999 is a legal tag");
assert!(!tag.is_standard());
assert!(tag.is_user_defined());
assert_eq!(tag.value(), USER_DEFINED_TAG_MAX);
}
#[test]
fn test_field_tag_boundary_10000_is_not_user_defined() {
let tag = ok(FieldTag::try_new(10000), "10000 is a legal tag");
assert!(!tag.is_user_defined());
assert!(!tag.is_standard());
}
#[test]
fn test_field_tag_extended_user_defined_range_boundaries() {
let cases = [
(19999u32, false),
(20000, true),
(USER_DEFINED_EXT_TAG_MIN, true),
(30000, true),
(39999, true),
(USER_DEFINED_EXT_TAG_MAX, true),
(40000, false),
];
for (tag_num, expected) in cases {
let tag = ok(FieldTag::try_new(tag_num), "high tag is legal");
assert_eq!(
tag.is_user_defined(),
expected,
"is_user_defined({tag_num}) should be {expected}"
);
assert!(!tag.is_standard(), "{tag_num} is not a standard low tag");
}
}
#[test]
fn test_field_tag_reserved_high_range_is_not_user_defined() {
for tag_num in [40000, 40001, 49999, 50000] {
let tag = ok(FieldTag::try_new(tag_num), "reserved high tag is legal");
assert!(
!tag.is_user_defined(),
"reserved tag {tag_num} must not be user-defined"
);
}
}
#[test]
fn test_field_ref_as_str() {
let field = FieldRef::new(11, b"ORDER123");
assert_eq!(field.as_str(), Ok("ORDER123"));
}
#[test]
fn test_field_ref_as_u64() {
let field = FieldRef::new(34, b"12345");
assert_eq!(field.as_u64(), Ok(12345));
}
#[test]
fn test_field_ref_as_u64_non_numeric_is_typed_error() {
assert_invalid_field_value(FieldRef::new(34, b"abc").as_u64(), 34);
}
#[test]
fn test_field_ref_as_u64_negative_is_typed_error() {
assert_invalid_field_value(FieldRef::new(34, b"-1").as_u64(), 34);
}
#[test]
fn test_field_ref_as_u64_empty_is_typed_error() {
assert_invalid_field_value(FieldRef::new(34, b"").as_u64(), 34);
}
#[test]
fn test_field_ref_as_i64_accepts_negative() {
assert_eq!(FieldRef::new(14, b"-42").as_i64(), Ok(-42));
}
#[test]
fn test_field_ref_as_decimal_parses_price() {
let field = FieldRef::new(44, b"123.45");
let price = ok(field.as_decimal(), "123.45 is a valid price");
assert_eq!(price, Decimal::new(12345, 2));
}
#[test]
fn test_field_ref_as_decimal_preserves_scale() {
let field = FieldRef::new(44, b"1.500");
let price = ok(field.as_decimal(), "1.500 is a valid price");
assert_eq!(price.to_string(), "1.500");
}
#[test]
fn test_field_ref_as_decimal_negative_price() {
let field = FieldRef::new(44, b"-0.01");
let price = ok(field.as_decimal(), "-0.01 is a valid price");
assert_eq!(price, Decimal::new(-1, 2));
}
#[test]
fn test_field_ref_as_decimal_two_points_is_typed_error() {
assert_invalid_field_value(FieldRef::new(44, b"1.2.3").as_decimal(), 44);
}
#[test]
fn test_field_ref_as_decimal_garbage_is_typed_error() {
assert_invalid_field_value(FieldRef::new(44, b"abc").as_decimal(), 44);
assert_invalid_field_value(FieldRef::new(44, b"").as_decimal(), 44);
assert_invalid_field_value(FieldRef::new(44, b"1,50").as_decimal(), 44);
}
#[test]
fn test_field_ref_as_decimal_invalid_utf8_is_typed_error() {
let field = FieldRef::new(44, &[0xFF, 0xFE]);
assert!(matches!(
field.as_decimal(),
Err(DecodeError::InvalidUtf8(_))
));
}
#[test]
fn test_field_ref_as_bool() {
assert_eq!(FieldRef::new(141, b"Y").as_bool(), Ok(true));
assert_eq!(FieldRef::new(141, b"N").as_bool(), Ok(false));
}
#[test]
fn test_field_ref_as_bool_rejects_lowercase_and_words() {
assert_invalid_field_value(FieldRef::new(141, b"y").as_bool(), 141);
assert_invalid_field_value(FieldRef::new(141, b"n").as_bool(), 141);
assert_invalid_field_value(FieldRef::new(141, b"YES").as_bool(), 141);
assert_invalid_field_value(FieldRef::new(141, b"").as_bool(), 141);
}
#[test]
fn test_field_ref_as_char() {
assert_eq!(FieldRef::new(54, b"1").as_char(), Ok('1'));
}
#[test]
fn test_field_ref_as_char_rejects_multi_byte_and_non_ascii() {
assert_invalid_field_value(FieldRef::new(54, b"12").as_char(), 54);
assert_invalid_field_value(FieldRef::new(54, b"").as_char(), 54);
assert_invalid_field_value(FieldRef::new(54, "ñ".as_bytes()).as_char(), 54);
assert_invalid_field_value(FieldRef::new(54, &[0x80]).as_char(), 54);
}
#[test]
fn test_field_ref_invalid_utf8() {
let field = FieldRef::new(1, &[0xFF, 0xFE]);
assert!(matches!(field.as_str(), Err(DecodeError::InvalidUtf8(_))));
}
#[test]
fn test_field_value_display() {
assert_eq!(FieldValue::String("test".to_string()).to_string(), "test");
assert_eq!(FieldValue::Int(42).to_string(), "42");
assert_eq!(FieldValue::Bool(true).to_string(), "Y");
assert_eq!(FieldValue::Bool(false).to_string(), "N");
}
}