#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgTypeName(String);
impl PgTypeName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgTypeError> {
validate_type_label(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn built_in(ty: PgBuiltInType) -> Self {
Self(ty.as_str().to_owned())
}
#[must_use]
pub fn array_of(element: &Self) -> Self {
Self(format!("{}[]", element.as_str()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_array_label(&self) -> bool {
self.0.ends_with("[]")
}
}
impl AsRef<str> for PgTypeName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for PgTypeName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgTypeName {
type Err = PgTypeError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for PgTypeName {
type Error = PgTypeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgTypeCategory {
#[default]
UserDefined,
Boolean,
Numeric,
String,
Binary,
DateTime,
Uuid,
Json,
Network,
Array,
Enum,
Composite,
Domain,
Range,
Pseudo,
}
impl PgTypeCategory {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::UserDefined => "user-defined",
Self::Boolean => "boolean",
Self::Numeric => "numeric",
Self::String => "string",
Self::Binary => "binary",
Self::DateTime => "date-time",
Self::Uuid => "uuid",
Self::Json => "json",
Self::Network => "network",
Self::Array => "array",
Self::Enum => "enum",
Self::Composite => "composite",
Self::Domain => "domain",
Self::Range => "range",
Self::Pseudo => "pseudo",
}
}
}
impl fmt::Display for PgTypeCategory {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgBuiltInType {
#[default]
Text,
Bool,
SmallInt,
Integer,
BigInt,
Numeric,
Real,
DoublePrecision,
Varchar,
Char,
Bytea,
Date,
Time,
Timestamp,
TimestampTz,
Uuid,
Json,
Jsonb,
Inet,
Cidr,
Macaddr,
Macaddr8,
Array,
}
impl PgBuiltInType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Bool => "boolean",
Self::SmallInt => "smallint",
Self::Integer => "integer",
Self::BigInt => "bigint",
Self::Numeric => "numeric",
Self::Real => "real",
Self::DoublePrecision => "double precision",
Self::Varchar => "character varying",
Self::Char => "character",
Self::Bytea => "bytea",
Self::Date => "date",
Self::Time => "time",
Self::Timestamp => "timestamp",
Self::TimestampTz => "timestamp with time zone",
Self::Uuid => "uuid",
Self::Json => "json",
Self::Jsonb => "jsonb",
Self::Inet => "inet",
Self::Cidr => "cidr",
Self::Macaddr => "macaddr",
Self::Macaddr8 => "macaddr8",
Self::Array => "array",
}
}
#[must_use]
pub const fn category(self) -> PgTypeCategory {
match self {
Self::Bool => PgTypeCategory::Boolean,
Self::SmallInt
| Self::Integer
| Self::BigInt
| Self::Numeric
| Self::Real
| Self::DoublePrecision => PgTypeCategory::Numeric,
Self::Text | Self::Varchar | Self::Char => PgTypeCategory::String,
Self::Bytea => PgTypeCategory::Binary,
Self::Date | Self::Time | Self::Timestamp | Self::TimestampTz => {
PgTypeCategory::DateTime
}
Self::Uuid => PgTypeCategory::Uuid,
Self::Json | Self::Jsonb => PgTypeCategory::Json,
Self::Inet | Self::Cidr | Self::Macaddr | Self::Macaddr8 => PgTypeCategory::Network,
Self::Array => PgTypeCategory::Array,
}
}
}
impl fmt::Display for PgBuiltInType {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgBuiltInType {
type Err = PgTypeError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_type_label(input)?.as_str() {
"bool" | "boolean" => Ok(Self::Bool),
"smallint" | "int2" => Ok(Self::SmallInt),
"integer" | "int" | "int4" => Ok(Self::Integer),
"bigint" | "int8" => Ok(Self::BigInt),
"numeric" | "decimal" => Ok(Self::Numeric),
"real" | "float4" => Ok(Self::Real),
"double precision" | "float8" => Ok(Self::DoublePrecision),
"text" => Ok(Self::Text),
"varchar" | "character varying" => Ok(Self::Varchar),
"char" | "character" => Ok(Self::Char),
"bytea" => Ok(Self::Bytea),
"date" => Ok(Self::Date),
"time" | "time without time zone" => Ok(Self::Time),
"timestamp" | "timestamp without time zone" => Ok(Self::Timestamp),
"timestamptz" | "timestamp with time zone" => Ok(Self::TimestampTz),
"uuid" => Ok(Self::Uuid),
"json" => Ok(Self::Json),
"jsonb" => Ok(Self::Jsonb),
"inet" => Ok(Self::Inet),
"cidr" => Ok(Self::Cidr),
"macaddr" => Ok(Self::Macaddr),
"macaddr8" => Ok(Self::Macaddr8),
"array" | "anyarray" => Ok(Self::Array),
_ => Err(PgTypeError::UnknownBuiltInType),
}
}
}
impl TryFrom<&str> for PgBuiltInType {
type Error = PgTypeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgTypeOid(u32);
impl PgTypeOid {
pub const fn new(value: u32) -> Result<Self, PgTypeError> {
if value == 0 {
Err(PgTypeError::InvalidOid)
} else {
Ok(Self(value))
}
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
impl fmt::Display for PgTypeOid {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PgTypeError {
Empty,
ControlCharacter,
UnknownBuiltInType,
InvalidOid,
}
impl fmt::Display for PgTypeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("PostgreSQL type label cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("PostgreSQL type label cannot contain control characters")
}
Self::UnknownBuiltInType => {
formatter.write_str("unknown PostgreSQL built-in type label")
}
Self::InvalidOid => {
formatter.write_str("PostgreSQL type OID must be greater than zero")
}
}
}
}
impl Error for PgTypeError {}
fn validate_type_label(input: &str) -> Result<&str, PgTypeError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(PgTypeError::Empty);
}
if trimmed.chars().any(char::is_control) {
return Err(PgTypeError::ControlCharacter);
}
Ok(trimmed)
}
fn normalized_type_label(input: &str) -> Result<String, PgTypeError> {
let trimmed = validate_type_label(input)?;
Ok(trimmed
.replace('_', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::{PgBuiltInType, PgTypeCategory, PgTypeError, PgTypeName, PgTypeOid};
#[test]
fn parses_common_built_in_types() -> Result<(), PgTypeError> {
assert_eq!("bool".parse::<PgBuiltInType>()?, PgBuiltInType::Bool);
assert_eq!("int4".parse::<PgBuiltInType>()?, PgBuiltInType::Integer);
assert_eq!(
"double precision".parse::<PgBuiltInType>()?,
PgBuiltInType::DoublePrecision
);
assert_eq!(
"timestamptz".parse::<PgBuiltInType>()?,
PgBuiltInType::TimestampTz
);
assert_eq!("jsonb".parse::<PgBuiltInType>()?, PgBuiltInType::Jsonb);
Ok(())
}
#[test]
fn renders_canonical_labels_and_categories() {
assert_eq!(PgBuiltInType::Varchar.to_string(), "character varying");
assert_eq!(PgBuiltInType::Inet.category(), PgTypeCategory::Network);
assert_eq!(PgTypeCategory::Array.to_string(), "array");
}
#[test]
fn creates_type_names_and_arrays() {
let text = PgTypeName::built_in(PgBuiltInType::Text);
let array = PgTypeName::array_of(&text);
assert_eq!(text.as_str(), "text");
assert_eq!(array.to_string(), "text[]");
assert!(array.is_array_label());
assert_eq!(PgTypeName::new(""), Err(PgTypeError::Empty));
}
#[test]
fn wraps_oids_without_binding_catalog_meaning() -> Result<(), PgTypeError> {
let oid = PgTypeOid::new(23)?;
assert_eq!(oid.get(), 23);
assert_eq!(oid.to_string(), "23");
assert_eq!(PgTypeOid::new(0), Err(PgTypeError::InvalidOid));
Ok(())
}
}