sqlstate-inline 0.1.2

Memory efficient const-friendly types for SQLSTATE codes
Documentation
// © 2022 Christoph Grenz <https://grenz-bonn.de>
//
// SPDX-License-Identifier: MPL-2.0

use crate::error::ParseError;
use core::cmp::Ordering;
use core::hash::{Hash, Hasher};

/// A character `A`-`Z`,`O`-`9`
///
/// The `repr(u8)`-enum form allows treating slices of it like a `u8`/`str` while providing
/// niches for layout optimizations.
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum Char {
	_0 = b'0',
	_1 = b'1',
	_2 = b'2',
	_3 = b'3',
	_4 = b'4',
	_5 = b'5',
	_6 = b'6',
	_7 = b'7',
	_8 = b'8',
	_9 = b'9',
	A = b'A',
	B = b'B',
	C = b'C',
	D = b'D',
	E = b'E',
	F = b'F',
	G = b'G',
	H = b'H',
	I = b'I',
	J = b'J',
	K = b'K',
	L = b'L',
	M = b'M',
	N = b'N',
	O = b'O',
	P = b'P',
	Q = b'Q',
	R = b'R',
	S = b'S',
	T = b'T',
	U = b'U',
	V = b'V',
	W = b'W',
	X = b'X',
	Y = b'Y',
	Z = b'Z',
}

impl Char {
	/// Tries to create a `Char` from a byte
	///
	/// Returns `None` if out of range.
	#[inline]
	#[must_use]
	pub const fn new(byte: u8) -> Option<Self> {
		Some(match byte {
			b'0' => Self::_0,
			b'1' => Self::_1,
			b'2' => Self::_2,
			b'3' => Self::_3,
			b'4' => Self::_4,
			b'5' => Self::_5,
			b'6' => Self::_6,
			b'7' => Self::_7,
			b'8' => Self::_8,
			b'9' => Self::_9,
			b'A' => Self::A,
			b'B' => Self::B,
			b'C' => Self::C,
			b'D' => Self::D,
			b'E' => Self::E,
			b'F' => Self::F,
			b'G' => Self::G,
			b'H' => Self::H,
			b'I' => Self::I,
			b'J' => Self::J,
			b'K' => Self::K,
			b'L' => Self::L,
			b'M' => Self::M,
			b'N' => Self::N,
			b'O' => Self::O,
			b'P' => Self::P,
			b'Q' => Self::Q,
			b'R' => Self::R,
			b'S' => Self::S,
			b'T' => Self::T,
			b'U' => Self::U,
			b'V' => Self::V,
			b'W' => Self::W,
			b'X' => Self::X,
			b'Y' => Self::Y,
			b'Z' => Self::Z,
			_ => return None,
		})
	}

	/// Tries to create a `Char` array from a byte slice.
	///
	/// Returns `Err(ParseError)` if `value`s length doesn't match `N` or at least one byte in the
	/// array is out of range.
	///
	/// This function is optimized for small values of `N` and the details of errors will be
	/// wrong for `N` greater than `255`.
	///
	/// (ugly implementation, but neccessary for today to be compile-time evaluatable )
	#[inline]
	pub const fn new_array<const N: usize>(value: &[u8]) -> Result<[Char; N], ParseError> {
		let mut result = [Self::Z; N];

		let len = value.len();
		if len != N {
			return Err(ParseError::WrongLength {
				expected: N as _,
				got: len,
			});
		}

		let mut i = 0;
		while i < N {
			if let Some(c) = Self::new(value[i]) {
				result[i] = c;
			} else {
				return Err(ParseError::InvalidChar {
					byte: value[i],
					position: i,
				});
			}
			i += 1;
		}
		Ok(result)
	}

	/// Tries to create a `Char` array from a byte slice.
	///
	/// Returns `Err(ParseError)` if `value`s length doesn't match `N` or at least one byte in the
	/// array is out of range.
	///
	/// (ugly implementation, but neccessary for today to be compile-time evaluatable)
	#[inline]
	#[must_use]
	pub const fn new_array_lossy<const N: usize>(value: &[u8], fallback: Self) -> [Char; N] {
		let mut result = [fallback; N];
		let len = value.len();

		let mut i = 0;
		while i < N && i < len {
			if let Some(c) = Self::new(value[i]) {
				result[i] = c;
			}
			i += 1;
		}
		result
	}

	/// Converts `self` into a `byte`
	#[inline]
	#[must_use]
	pub const fn as_byte(self) -> u8 {
		self as u8
	}

	/// Converts `&self` into a byte array reference.
	#[inline]
	#[must_use]
	pub const fn array_as_bytes<const N: usize>(array: &[Self; N]) -> &[u8; N] {
		// SAFETY: All variants of `Char` are valid `u8`, given they're `repr(u8)`.
		unsafe { &*((array as *const [Char; N]).cast()) }
	}

	/// Converts `&self` into a byte array reference.
	#[inline]
	#[must_use]
	pub const fn slice_as_bytes(slice: &[Self]) -> &[u8] {
		// SAFETY: All slices of `Char` have the same layout as `[u8]`, given they're `repr(u8)`.
		unsafe { &*(slice as *const [Char] as *const [u8]) }
	}

	/// Converts `&self` into a str.
	#[inline]
	#[must_use]
	pub const fn array_as_str<const N: usize>(array: &[Self; N]) -> &str {
		// SAFETY: All variants of `Char` and all arrays thereof are valid UTF-8, given
		// they're ASCII codes and `repr(u8)`.
		unsafe { core::str::from_utf8_unchecked(Self::array_as_bytes(array)) }
	}
}

/// `Ord` implementation matching the implementation for `u8`
impl Ord for Char {
	#[inline]
	fn cmp(&self, other: &Self) -> Ordering {
		self.as_byte().cmp(&other.as_byte())
	}
}

/// `PartialOrd` implementation matching the implementation for `u8`
impl PartialOrd for Char {
	#[inline]
	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
		Some(self.cmp(other))
	}
}

/// `Hash` implementation matching the implementation for `u8`
///
/// This will always agree with the compiler-derived `PartialEq` implementation
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for Char {
	#[inline]
	fn hash<H: Hasher>(&self, state: &mut H) {
		self.as_byte().hash(state);
	}
	#[inline]
	fn hash_slice<H: Hasher>(data: &[Self], state: &mut H) {
		u8::hash_slice(Self::slice_as_bytes(data), state);
	}
}

#[cfg(feature = "serde")]
pub(crate) mod de {
	use super::{Char, ParseError};
	use core::fmt;
	use core::marker::PhantomData;
	use serde::de::{self, Unexpected, Visitor};

	/// Visitor type for generic deserialization into a `Char` array.
	pub(crate) struct ArrayVisitor<const N: usize>(PhantomData<[(); N]>);

	impl<const N: usize> ArrayVisitor<N> {
		#[inline]
		pub const fn new() -> Self {
			ArrayVisitor(PhantomData)
		}
	}

	impl<'de, const N: usize> Visitor<'de> for ArrayVisitor<N> {
		type Value = [Char; N];

		fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
			write!(
				f,
				"an ascii string or byte array of length {N} containing only A-Z and 0-9"
			)
		}

		fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
			Char::new_array(value.as_bytes())
				.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
		}

		fn visit_bytes<E: de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
			Char::new_array(&value).map_err(|e| match e {
				ParseError::WrongLength { got, .. } => E::invalid_length(got, &self),
				ParseError::InvalidChar { .. } => E::invalid_value(Unexpected::Bytes(value), &self),
			})
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use core::fmt;

	// A `Debug` impl is only needed for testing, as this type is not exposed normally
	impl fmt::Debug for Char {
		#[cold]
		fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
			f.write_str(Self::array_as_str(core::array::from_ref(self)))
		}
	}

	// Test that only 0-9|A-Z can be converted to `Char` and that they roundtrip correctly
	#[test]
	fn test_char() {
		for c in u8::MIN..u8::MAX {
			match c {
				b'0'..=b'9' | b'A'..=b'Z' => assert_eq!(Char::new(c).unwrap().as_byte(), c),
				_ => assert!(Char::new(c).is_none()),
			}
		}
	}

	// Test that arrays bigger than usual also correctly roundtrip
	#[test]
	fn test_big_array() {
		let value = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		let arr = Char::new_array(value).unwrap();
		assert_eq!(Char::array_as_bytes(&arr), value);

		let value = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
		if let ParseError::InvalidChar { byte, position } =
			Char::new_array::<73>(value).unwrap_err()
		{
			assert_eq!(byte, b'/');
			assert_eq!(position, 72);
		} else {
			unreachable!();
		}

		let value = b"/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		if let ParseError::InvalidChar { byte, position } =
			Char::new_array::<73>(value).unwrap_err()
		{
			assert_eq!(byte, b'/');
			assert_eq!(position, 0);
		} else {
			unreachable!();
		}

		let value = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWX:YZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		if let ParseError::InvalidChar { byte, position } =
			Char::new_array::<73>(value).unwrap_err()
		{
			assert_eq!(byte, b':');
			assert_eq!(position, 34);
		} else {
			unreachable!();
		}
	}

	// Test that zero-length arrays also work
	#[test]
	fn test_zero_array() {
		let value = b"";
		let arr = Char::new_array(value).unwrap();
		assert_eq!(Char::array_as_bytes(&arr), value);
	}

	// Test wrong lengths
	#[test]
	fn test_wrong_lengths() {
		let value = b"01234";
		assert_eq!(Char::new_array::<0>(value).unwrap_err().valid_up_to(), 0);
		assert_eq!(Char::new_array::<2>(value).unwrap_err().valid_up_to(), 2);
		assert_eq!(Char::new_array::<4>(value).unwrap_err().valid_up_to(), 4);
		assert_eq!(Char::new_array::<6>(value).unwrap_err().valid_up_to(), 5);
		assert_eq!(Char::new_array::<10>(value).unwrap_err().valid_up_to(), 5);
		assert_eq!(Char::new_array::<255>(value).unwrap_err().valid_up_to(), 5);
	}

	// Test that string roundtrips work
	#[test]
	fn test_str() {
		let value = "01234";
		let arr = Char::new_array::<5>(value.as_bytes()).unwrap();
		assert_eq!(Char::array_as_str(&arr), value);
	}
}