use std::fmt;
use std::marker::PhantomData;
use std::mem;
#[cfg(feature = "chrono")]
use chrono::{DateTime, Utc};
use ruststream::HeaderMap;
use sqlx::{Column, Database, Error, FromRow, Row, ValueRef};
#[cfg(feature = "time")]
use time::OffsetDateTime;
use super::by_name::ByName;
use super::database::RoleColumns;
use super::kinds::{IntKind, Kinds};
use crate::inbox::engine::{Claimed, undecodable};
use crate::inbox::{PayloadLane, PayloadRow, QueueRow};
#[doc(hidden)]
pub struct NamedRow<D> {
id: NamedId,
payload: NamedBytes,
headers: HeaderMap,
key: Option<NamedBytes>,
attempt: Option<u64>,
fits: Fits,
dialect: PhantomData<fn() -> D>,
}
impl<D> fmt::Debug for NamedRow<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedRow")
.field("id", &self.id)
.field("payload", &self.payload)
.field("headers", &self.headers)
.field("key", &self.key)
.field("attempt", &self.attempt)
.field("fits", &self.fits)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Fits {
id: u8,
payload: u8,
key: u8,
attempt: u8,
}
impl<D> NamedRow<D> {
fn unfit(&self, kinds: Kinds) -> Option<(&'static str, &'static str)> {
if self.fits.id & kinds.id.bit() == 0 {
return Some(("id", kinds.id.name()));
}
if self.fits.payload & kinds.payload.bit() == 0 {
return Some(("payload", kinds.payload.name()));
}
if let Some(key) = kinds.key
&& self.fits.key & key.bit() == 0
{
return Some(("partition_key", key.name()));
}
if let Some(attempt) = kinds.attempt
&& !attempt_fits(attempt, self.fits.attempt)
{
return Some(("attempt", attempt.name()));
}
None
}
fn fitting_attempt(&self, kinds: Kinds) -> Option<u64> {
let kind = kinds.attempt?;
self.attempt
.filter(|_| attempt_fits(kind, self.fits.attempt))
}
}
const fn attempt_fits(kind: IntKind, held: u8) -> bool {
held & kind.bit() != 0
}
fn hold_to<D: 'static>(kinds: Kinds, claimed: &mut Claimed<NamedRow<D>>) -> Result<(), Error> {
let Claimed::Row(row) = claimed else {
return Ok(());
};
let Some((alias, read_as)) = row.unfit(kinds) else {
return Ok(());
};
let error = Error::ColumnDecode {
index: format!("{alias:?}"),
source: format!(
"the struct reads this column as {read_as}, which its type in the table does not hold"
)
.into(),
};
if alias == "id" {
return Err(error);
}
let attempt = row.fitting_attempt(kinds);
let id = mem::replace(&mut row.id, NamedId::I64(0));
*claimed = Claimed::Undecodable {
id,
attempt,
error: Box::new(undecodable(error)),
};
Ok(())
}
#[doc(hidden)]
#[derive(PartialEq, Eq, Hash)]
pub enum NamedId {
I16(i16),
I32(i32),
I64(i64),
Text(String),
Bytes(Vec<u8>),
}
impl Clone for NamedId {
fn clone(&self) -> Self {
match self {
Self::I16(id) => Self::I16(*id),
Self::I32(id) => Self::I32(*id),
Self::I64(id) => Self::I64(*id),
Self::Text(id) => Self::Text(id.clone()),
Self::Bytes(id) => Self::Bytes(id.clone()),
}
}
fn clone_from(&mut self, source: &Self) {
match (self, source) {
(Self::Text(kept), Self::Text(id)) => kept.clone_from(id),
(Self::Bytes(kept), Self::Bytes(id)) => kept.clone_from(id),
(kept, id) => *kept = id.clone(),
}
}
}
impl fmt::Debug for NamedId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::I16(id) => fmt::Debug::fmt(id, f),
Self::I32(id) => fmt::Debug::fmt(id, f),
Self::I64(id) => fmt::Debug::fmt(id, f),
Self::Text(id) => fmt::Debug::fmt(id, f),
Self::Bytes(id) => fmt::Debug::fmt(id, f),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NamedBytes {
Bytes(Vec<u8>),
Text(String),
}
impl NamedBytes {
fn as_bytes(&self) -> &[u8] {
match self {
Self::Bytes(bytes) => bytes,
Self::Text(text) => text.as_bytes(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NamedTime {
#[cfg(feature = "chrono")]
Chrono(DateTime<Utc>),
#[cfg(feature = "time")]
Time(OffsetDateTime),
}
fn read<DB, D>(row: &DB::Row) -> Result<NamedRow<D>, Error>
where
DB: RoleColumns,
D: ByName<DB>,
{
let mut id = None;
let mut payload = None;
let mut headers = HeaderMap::new();
let mut key = None;
let mut attempt = None;
let mut fits = Fits {
id: 0,
payload: 0,
key: 0,
attempt: 0,
};
for column in row.columns() {
let alias = column.name();
let failed = |source| Error::ColumnDecode {
index: format!("{alias:?}"),
source,
};
let value = || DB::value(row, column.ordinal());
match alias {
"id" => {
let (read, held) = DB::id(value()?).map_err(failed)?;
(id, fits.id) = (Some(read), held);
}
"payload" => {
let (read, held) = DB::bytes(value()?).map_err(failed)?;
(payload, fits.payload) = (Some(read), held);
}
"partition_key" => {
let value = value()?;
if value.is_null() {
fits.key = u8::MAX;
} else {
let (read, held) = DB::bytes(value).map_err(failed)?;
(key, fits.key) = (Some(read), held);
}
}
"headers" => {
let value = value()?;
if !value.is_null() {
headers = D::headers(value).map_err(failed)?;
}
}
"attempt" => {
let (read, held) = DB::attempt(value()?).map_err(failed)?;
(attempt, fits.attempt) = (Some(read), held);
}
_ => {}
}
}
Ok(NamedRow {
id: id.ok_or_else(|| Error::ColumnNotFound("id".to_owned()))?,
payload: payload.ok_or_else(|| Error::ColumnNotFound("payload".to_owned()))?,
headers,
key,
attempt,
fits,
dialect: PhantomData,
})
}
impl<'r, R, D> FromRow<'r, R> for NamedRow<D>
where
R: Row,
R::Database: RoleColumns + Database<Row = R>,
D: ByName<R::Database>,
{
fn from_row(row: &'r R) -> Result<Self, Error> {
read::<R::Database, D>(row)
}
}
impl<D: 'static> QueueRow for NamedRow<D> {
type Id = NamedId;
type Lane = PayloadLane;
}
impl<D: 'static> PayloadRow for NamedRow<D> {
type Column = NamedBytes;
fn payload(&self) -> &[u8] {
self.payload.as_bytes()
}
}
mod events;
#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use ruststream::HeaderMap;
use ruststream::codec::CodecError;
use sqlx::Error;
use super::{Fits, NamedBytes, NamedId, NamedRow, hold_to};
use crate::inbox::engine::{Claimed, undecodable};
use crate::inbox::named::kinds::{BytesKind, ClockKind, IdKind, IntKind, Kinds};
pub(super) const KINDS: Kinds = Kinds {
clock: ClockKind::System,
id: IdKind::I64,
payload: BytesKind::Bytes,
key: Some(BytesKind::Text),
attempt: Some(IntKind::I16),
retry_after: None,
processed_at: None,
locked_until: None,
};
pub(super) fn row<D>(fits: Fits) -> NamedRow<D> {
NamedRow {
id: NamedId::I64(7),
payload: NamedBytes::Bytes(b"{}".to_vec()),
headers: HeaderMap::new(),
key: Some(NamedBytes::Text("acme".to_owned())),
attempt: Some(2),
fits,
dialect: PhantomData,
}
}
pub(super) const FITTING: Fits = Fits {
id: IdKind::I64.bit(),
payload: BytesKind::Bytes.bit(),
key: BytesKind::Text.bit(),
attempt: IntKind::I16.bit(),
};
fn driver_error(error: &CodecError) -> Option<&Error> {
match error {
CodecError::Decode(source) => source.downcast_ref::<Error>(),
_ => None,
}
}
fn unfit_column(claimed: &Claimed<NamedRow<()>>) -> Option<(String, Option<u64>)> {
match claimed {
Claimed::Undecodable { id, attempt, error } => {
assert_eq!(id, &NamedId::I64(7), "the row keeps its id for the policy");
match driver_error(error) {
Some(Error::ColumnDecode { index, .. }) => Some((index.clone(), *attempt)),
other => panic!("not a column's error: {other:?}"),
}
}
Claimed::Row(_) | Claimed::Missing(_) => None,
}
}
#[test]
fn logs_name_a_row_by_its_id_as_the_table_holds_it() {
let ids = [
NamedId::I16(1),
NamedId::I32(2),
NamedId::I64(3),
NamedId::Text("job-4".to_owned()),
NamedId::Bytes(vec![5]),
];
let logged: Vec<String> = ids.iter().map(|id| format!("{id:?}")).collect();
assert_eq!(logged, ["1", "2", "3", "\"job-4\"", "[5]"]);
}
#[test]
fn an_id_is_copied_into_the_storage_of_the_one_it_replaces() {
let mut kept = NamedId::Text("job-0001".to_owned());
let storage = match &kept {
NamedId::Text(text) => text.as_ptr(),
other => panic!("not a text id: {other:?}"),
};
kept.clone_from(&NamedId::Text("job-0002".to_owned()));
assert!(
matches!(&kept, NamedId::Text(text) if text == "job-0002" && text.as_ptr() == storage),
"{kept:?}"
);
let mut bytes = NamedId::Bytes(vec![1, 2]);
bytes.clone_from(&NamedId::Bytes(vec![3, 4]));
assert_eq!(bytes, NamedId::Bytes(vec![3, 4]));
kept.clone_from(&NamedId::I64(7));
assert_eq!(kept, NamedId::I64(7));
assert_eq!(kept.clone(), NamedId::I64(7));
let ids = [
NamedId::I16(1),
NamedId::I32(2),
NamedId::Text("job-3".to_owned()),
NamedId::Bytes(vec![4]),
];
for id in ids {
assert_eq!(id.clone(), id);
}
}
#[test]
fn a_row_whose_columns_hold_its_structs_types_stays_a_row() -> Result<(), Error> {
let mut claimed = Claimed::Row(row::<()>(FITTING));
hold_to(KINDS, &mut claimed)?;
assert!(matches!(claimed, Claimed::Row(_)));
let mut keyless = Claimed::Row(row::<()>(Fits {
key: u8::MAX,
..FITTING
}));
hold_to(KINDS, &mut keyless)?;
assert!(matches!(keyless, Claimed::Row(_)));
Ok(())
}
#[test]
fn a_column_that_does_not_hold_its_structs_type_sends_the_row_to_the_policy()
-> Result<(), Error> {
let text_payload = Fits {
payload: BytesKind::Text.bit(),
..FITTING
};
let byte_key = Fits {
key: BytesKind::Bytes.bit(),
..FITTING
};
let wide_attempt = Fits {
attempt: IntKind::I64.bit(),
..FITTING
};
let text_payload_wide_attempt = Fits {
attempt: IntKind::I64.bit(),
..text_payload
};
for (fits, column, attempt) in [
(text_payload, "\"payload\"", Some(2)),
(byte_key, "\"partition_key\"", Some(2)),
(wide_attempt, "\"attempt\"", None),
(text_payload_wide_attempt, "\"payload\"", None),
] {
let mut claimed = Claimed::Row(row::<()>(fits));
hold_to(KINDS, &mut claimed)?;
assert_eq!(
unfit_column(&claimed),
Some((column.to_owned(), attempt)),
"{fits:?}"
);
}
Ok(())
}
#[test]
fn an_id_that_does_not_hold_its_structs_type_fails_the_claim() {
let mut claimed = Claimed::Row(row::<()>(Fits {
id: IdKind::I32.bit(),
..FITTING
}));
let failed = hold_to(KINDS, &mut claimed);
assert!(
matches!(&failed, Err(Error::ColumnDecode { index, .. }) if index == "\"id\""),
"{failed:?}"
);
}
#[test]
fn a_row_already_undecodable_is_left_to_the_policy() -> Result<(), Error> {
let mut claimed = Claimed::<NamedRow<()>>::Undecodable {
id: NamedId::I64(7),
attempt: Some(3),
error: Box::new(undecodable(Error::ColumnNotFound("payload".to_owned()))),
};
hold_to(KINDS, &mut claimed)?;
assert!(matches!(
&claimed,
Claimed::Undecodable { attempt: Some(3), error, .. }
if matches!(driver_error(error), Some(Error::ColumnNotFound(_)))
));
Ok(())
}
}