use std::fmt;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
#[cfg_attr(feature = "schema", schemars(transparent))]
pub struct TableName(String);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
#[cfg_attr(feature = "schema", schemars(transparent))]
pub struct ColumnName(String);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
#[cfg_attr(feature = "schema", schemars(transparent))]
pub struct IndexName(String);
macro_rules! impl_name_newtype {
($ty:ident) => {
impl $ty {
#[must_use]
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for $ty {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for $ty {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<$ty> for String {
fn from(t: $ty) -> Self {
t.0
}
}
impl From<&$ty> for String {
fn from(t: &$ty) -> Self {
t.0.clone()
}
}
impl AsRef<str> for $ty {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for $ty {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl fmt::Display for $ty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl fmt::Debug for $ty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl std::borrow::Borrow<str> for $ty {
fn borrow(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for $ty {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for $ty {
fn eq(&self, other: &&str) -> bool {
&self.0 == *other
}
}
impl PartialEq<String> for $ty {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
};
}
impl_name_newtype!(TableName);
impl_name_newtype!(ColumnName);
impl_name_newtype!(IndexName);
impl TableName {
#[must_use]
pub fn with_prefix(self, prefix: &str) -> Self {
if prefix.is_empty() {
return self;
}
let mut s = self.0;
s.insert_str(0, prefix);
Self(s)
}
}
#[must_use]
pub fn names_to_strings<T: ToString>(names: &[T]) -> Vec<String> {
names.iter().map(ToString::to_string).collect()
}
#[must_use]
pub fn join_column_names(columns: &[ColumnName], separator: &str) -> String {
let cap = columns.iter().map(|c| c.as_str().len()).sum::<usize>()
+ separator.len() * columns.len().saturating_sub(1);
let mut out = String::with_capacity(cap);
for (i, c) in columns.iter().enumerate() {
if i > 0 {
out.push_str(separator);
}
out.push_str(c.as_str());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_name_new_constructs_from_str_literal() {
let name = TableName::new("user");
assert_eq!(name.as_str(), "user");
}
#[test]
fn column_name_new_constructs_from_owned_string() {
let name = ColumnName::new(String::from("email"));
assert_eq!(name.as_str(), "email");
}
#[test]
fn index_name_new_constructs_from_str_ref() {
let owned = "ix_user__email".to_string();
let name = IndexName::new(&*owned);
assert_eq!(name.as_str(), "ix_user__email");
}
#[test]
fn table_name_into_string_via_from() {
let name = TableName::new("orders");
let s: String = String::from(name);
assert_eq!(s, "orders");
}
#[test]
fn column_name_into_string_via_from() {
let name = ColumnName::new("created_at");
let s: String = String::from(name);
assert_eq!(s, "created_at");
}
#[test]
fn index_name_into_string_via_from() {
let name = IndexName::new("ix_orders__id");
let s: String = String::from(name);
assert_eq!(s, "ix_orders__id");
}
#[test]
fn into_inner_consumes_newtype_back_into_string() {
assert_eq!(TableName::new("user").into_inner(), "user");
assert_eq!(ColumnName::new("email").into_inner(), "email");
assert_eq!(
IndexName::new("ix_user__email").into_inner(),
"ix_user__email"
);
}
#[test]
fn with_prefix_empty_is_a_no_op() {
let unchanged = TableName::new("user").with_prefix("");
assert_eq!(unchanged.as_str(), "user");
}
#[test]
fn with_prefix_prepends_in_place() {
let prefixed = TableName::new("user").with_prefix("tenant_");
assert_eq!(prefixed.as_str(), "tenant_user");
}
#[test]
fn join_column_names_empty_returns_empty_string() {
let cols: Vec<ColumnName> = vec![];
assert_eq!(join_column_names(&cols, ", "), "");
assert_eq!(join_column_names(&cols, "_"), "");
}
#[test]
fn join_column_names_comma_separator() {
let cols: Vec<ColumnName> = vec!["a".into(), "b".into(), "c".into()];
assert_eq!(join_column_names(&cols, ", "), "a, b, c");
let single: Vec<ColumnName> = vec!["only".into()];
assert_eq!(join_column_names(&single, ", "), "only");
}
#[test]
fn join_column_names_underscore_separator() {
let cols: Vec<ColumnName> = vec!["fk".into(), "id".into()];
assert_eq!(join_column_names(&cols, "_"), "fk_id");
let composite: Vec<ColumnName> = vec!["tenant_id".into(), "user_id".into()];
assert_eq!(join_column_names(&composite, "_"), "tenant_id_user_id");
}
#[test]
fn names_to_strings_empty_returns_empty_vec() {
let cols: Vec<ColumnName> = vec![];
assert_eq!(names_to_strings(&cols), Vec::<String>::new());
}
#[test]
fn names_to_strings_two_element_case() {
let cols: Vec<ColumnName> = vec!["a".into(), "b".into()];
let out = names_to_strings(&cols);
assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
}
}