pgtask-core 0.2.0

Core types and state model for pgtask
Documentation
use std::{fmt, num::NonZeroU32, str::FromStr};

use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

const MAX_QUEUE_NAME_BYTES: usize = 128;
const MAX_TASK_NAME_BYTES: usize = 255;

#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum NameError {
    #[error("{kind} must not be empty")]
    Empty { kind: &'static str },
    #[error("{kind} must be at most {maximum} bytes, got {actual}")]
    TooLong {
        kind: &'static str,
        maximum: usize,
        actual: usize,
    },
    #[error("{kind} contains unsupported character {character:?}")]
    UnsupportedCharacter { kind: &'static str, character: char },
}

fn validate_name(value: &str, kind: &'static str, maximum: usize) -> Result<(), NameError> {
    if value.is_empty() {
        return Err(NameError::Empty { kind });
    }
    if value.len() > maximum {
        return Err(NameError::TooLong {
            kind,
            maximum,
            actual: value.len(),
        });
    }
    if let Some(character) = value
        .chars()
        .find(|character| !(character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | ':' | '-')))
    {
        return Err(NameError::UnsupportedCharacter { kind, character });
    }
    Ok(())
}

macro_rules! name_type {
    ($name:ident, $kind:literal, $maximum:expr) => {
        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
        #[serde(try_from = "String", into = "String")]
        pub struct $name(String);

        impl $name {
            pub fn new(value: impl Into<String>) -> Result<Self, NameError> {
                let value = value.into();
                validate_name(&value, $kind, $maximum)?;
                Ok(Self(value))
            }

            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                self.as_str()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(self.as_str())
            }
        }

        impl TryFrom<String> for $name {
            type Error = NameError;

            fn try_from(value: String) -> Result<Self, Self::Error> {
                Self::new(value)
            }
        }

        impl From<$name> for String {
            fn from(value: $name) -> Self {
                value.0
            }
        }
    };
}

name_type!(QueueName, "queue name", MAX_QUEUE_NAME_BYTES);
name_type!(ScheduleName, "schedule name", MAX_TASK_NAME_BYTES);
name_type!(SignalName, "signal name", MAX_TASK_NAME_BYTES);
name_type!(StepName, "step name", MAX_TASK_NAME_BYTES);
name_type!(TaskName, "task name", MAX_TASK_NAME_BYTES);

impl Default for QueueName {
    fn default() -> Self {
        Self("default".to_owned())
    }
}

macro_rules! uuid_type {
    ($name:ident) => {
        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
        #[serde(transparent)]
        pub struct $name(Uuid);

        impl $name {
            pub fn new() -> Self {
                Self(Uuid::now_v7())
            }

            pub const fn from_uuid(value: Uuid) -> Self {
                Self(value)
            }

            pub const fn as_uuid(self) -> Uuid {
                self.0
            }
        }

        impl Default for $name {
            fn default() -> Self {
                Self::new()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                self.0.fmt(formatter)
            }
        }

        impl FromStr for $name {
            type Err = uuid::Error;

            fn from_str(value: &str) -> Result<Self, Self::Err> {
                Uuid::parse_str(value).map(Self)
            }
        }

        impl From<$name> for Uuid {
            fn from(value: $name) -> Self {
                value.0
            }
        }
    };
}

uuid_type!(TaskId);
uuid_type!(ScheduleId);
uuid_type!(WorkerId);
uuid_type!(LeaseToken);

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HandlerVersion(NonZeroU32);

impl HandlerVersion {
    pub const fn new(value: NonZeroU32) -> Self {
        Self(value)
    }

    pub const fn get(self) -> u32 {
        self.0.get()
    }
}

impl Default for HandlerVersion {
    fn default() -> Self {
        Self(NonZeroU32::MIN)
    }
}