use std::collections::BTreeMap;
use crate::{RqsError, RqsResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueKind {
Text,
Integer,
Float,
Boolean,
Date,
DateTime,
Uuid,
}
impl ValueKind {
#[must_use]
pub fn expected_name(self) -> &'static str {
match self {
Self::Text => "text",
Self::Integer => "integer",
Self::Float => "float",
Self::Boolean => "boolean",
Self::Date => "date",
Self::DateTime => "datetime",
Self::Uuid => "uuid",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
public_name: String,
column_name: String,
value_kind: ValueKind,
regex_allowed: bool,
}
impl Field {
pub fn new(
public_name: impl Into<String>,
column_name: impl Into<String>,
value_kind: ValueKind,
) -> RqsResult<Self> {
let public_name = public_name.into();
let column_name = column_name.into();
validate_public_name(&public_name)?;
validate_column_name(&column_name)?;
Ok(Self {
public_name,
column_name,
value_kind,
regex_allowed: false,
})
}
#[must_use]
pub fn allow_regex(mut self) -> Self {
self.regex_allowed = true;
self
}
#[must_use]
pub fn public_name(&self) -> &str {
&self.public_name
}
#[must_use]
pub fn column_name(&self) -> &str {
&self.column_name
}
#[must_use]
pub fn value_kind(&self) -> ValueKind {
self.value_kind
}
#[must_use]
pub fn regex_allowed(&self) -> bool {
self.regex_allowed
}
pub(crate) fn to_ref(&self) -> FieldRef {
FieldRef {
public_name: self.public_name.clone(),
column_name: self.column_name.clone(),
value_kind: self.value_kind,
regex_allowed: self.regex_allowed,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldRef {
public_name: String,
column_name: String,
value_kind: ValueKind,
regex_allowed: bool,
}
impl FieldRef {
#[must_use]
pub fn public_name(&self) -> &str {
&self.public_name
}
#[must_use]
pub fn column_name(&self) -> &str {
&self.column_name
}
#[must_use]
pub fn value_kind(&self) -> ValueKind {
self.value_kind
}
#[must_use]
pub fn regex_allowed(&self) -> bool {
self.regex_allowed
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FieldCatalog {
fields: BTreeMap<String, Field>,
}
impl FieldCatalog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn allow(mut self, field: Field) -> RqsResult<Self> {
let name = field.public_name().to_owned();
self.fields.insert(name, field);
Ok(self)
}
pub fn allow_text(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Text)
}
pub fn allow_integer(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Integer)
}
pub fn allow_float(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Float)
}
pub fn allow_boolean(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Boolean)
}
pub fn allow_date(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Date)
}
pub fn allow_datetime(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::DateTime)
}
pub fn allow_uuid(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
) -> RqsResult<Self> {
self.allow_kind(public_name, column_name, ValueKind::Uuid)
}
#[must_use]
pub fn get(&self, public_name: &str) -> Option<&Field> {
self.fields.get(public_name)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.fields.len()
}
fn allow_kind(
self,
public_name: impl Into<String>,
column_name: impl Into<String>,
value_kind: ValueKind,
) -> RqsResult<Self> {
let field = Field::new(public_name, column_name, value_kind)?;
self.allow(field)
}
}
#[cfg(test)]
impl FieldRef {
pub(crate) fn new_for_test(
public_name: &str,
column_name: &str,
value_kind: ValueKind,
regex_allowed: bool,
) -> Self {
Self {
public_name: public_name.to_owned(),
column_name: column_name.to_owned(),
value_kind,
regex_allowed,
}
}
}
fn validate_public_name(name: &str) -> RqsResult<()> {
if is_dotted_identifier(name) {
Ok(())
} else {
Err(RqsError::InvalidFieldName {
field: name.to_owned(),
})
}
}
fn validate_column_name(name: &str) -> RqsResult<()> {
if is_dotted_identifier(name) {
Ok(())
} else {
Err(RqsError::InvalidColumnName {
column: name.to_owned(),
})
}
}
fn is_dotted_identifier(value: &str) -> bool {
!value.is_empty() && value.split('.').all(is_identifier)
}
fn is_identifier(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
}