use std::str::FromStr;
use serde::{Deserialize, Serialize};
use case::CaseExt;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum Element {
Enum(Enum),
Table(Table),
}
impl Element {
pub fn create(&self) -> String {
match self {
Element::Enum(e) => e.create_type(),
Element::Table(t) => t.create_table(),
}
}
pub fn drop(&self) -> String {
match self {
Element::Enum(e) => e.drop_type(),
Element::Table(t) => t.drop_table(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Enum {
pub name: String,
pub variants: Vec<String>,
}
impl Enum {
pub fn create_type(&self) -> String {
format!(
"CREATE TYPE \"{}\" AS ENUM ('{}');\n",
self.name,
self.variants.join("', '")
)
}
pub fn drop_type(&self) -> String {
format!("DROP TYPE {};", self.name)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Table {
pub name: String,
pub columns: Vec<Column>,
}
impl Table {
pub fn new(name: &str) -> Table {
Table {
name: name.into(),
columns: vec![],
}
}
pub fn create_table(&self) -> String {
format!(
"CREATE TABLE \"{}\" (\n {}\n);\n",
self.name,
self.columns
.iter()
.map(|x| format!(
"\"{}\" {}{}",
x.name,
x.ty.to_postgres(),
if x.unique { " UNIQUE" } else { "" }
))
.collect::<Vec<_>>()
.join(",\n ")
)
}
pub fn drop_table(&self) -> String {
format!("DROP TABLE \"{}\" CASCADE;", self.name)
}
pub fn current_migration() -> Table {
Table {
name: "ergol".into(),
columns: vec![Column::new("migration", Ty::I32, false)],
}
}
pub fn dependencies(&self) -> Vec<String> {
self.columns
.iter()
.filter_map(|x| match &x.ty {
Ty::Reference(t) => Some(t.clone()),
_ => None,
})
.collect()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Column {
pub name: String,
pub ty: Ty,
pub unique: bool,
}
impl Column {
pub fn new(name: &str, ty: Ty, unique: bool) -> Column {
Column {
name: name.into(),
ty,
unique,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Ty {
Id,
I32,
Bool,
String,
Json,
BitVec,
NaiveDateTime,
DateTimeUtc,
DateTimeLocal,
DateTimeFixedOffset,
NaiveDate,
NaiveTime,
MacAddress,
Point,
Rect,
LineString,
Uuid,
PrimitiveDateTime,
OffsetDateTime,
Date,
Time,
Option(Box<Ty>),
Enum(String),
Reference(String),
}
impl Ty {
pub fn to_postgres(&self) -> String {
match self {
Ty::Id => "SERIAL PRIMARY KEY".to_owned(),
Ty::String => "VARCHAR NOT NULL".to_owned(),
Ty::I32 => "INT NOT NULL".to_owned(),
Ty::Bool => "BOOL NOT NULL".to_owned(),
Ty::Json => "JSON NOT NULL".to_owned(),
Ty::BitVec => "VARBIT NOT NULL".to_owned(),
Ty::NaiveDateTime => "TIMESTAMP NOT NULL".to_owned(),
Ty::DateTimeUtc | Ty::DateTimeLocal | Ty::DateTimeFixedOffset => {
"TIMESTAMP WITH TIME ZONE NOT NULL".to_owned()
}
Ty::NaiveDate => "DATE NOT NULL".to_owned(),
Ty::NaiveTime => "TIME NOT NULL".to_owned(),
Ty::MacAddress => "MACADDR NOT NULL".to_owned(),
Ty::Point => "POINT NULL NULL".to_owned(),
Ty::Rect => "BOX NOT NULL".to_owned(),
Ty::LineString => "PATH NOT NULL".to_owned(),
Ty::Uuid => "UUID NOT NULL".to_owned(),
Ty::PrimitiveDateTime => "TIMESTAMP NOT NULL".to_owned(),
Ty::OffsetDateTime => "TIMESTAMP WITH TIME ZONE NOT NULL".to_owned(),
Ty::Date => "DATE NOT NULL".to_owned(),
Ty::Time => "TIME NOT NULL".to_owned(),
Ty::Option(ty) => {
let current = ty.to_postgres();
debug_assert!(current.ends_with(" NOT NULL"));
current[0..current.len() - 9].to_owned()
}
Ty::Enum(s) => format!("{} NOT NULL", s.to_snake()),
Ty::Reference(s) => format!(
"INT NOT NULL REFERENCES {} (id) ON DELETE CASCADE",
s.to_snake()
),
}
}
}
fn extract_chevrons(pattern: &str) -> Option<&str> {
Some(pattern.split("<").nth(1)?.split(">").nth(0)?.trim())
}
impl FromStr for Ty {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"String" => return Ok(Ty::String),
"i32" => return Ok(Ty::I32),
"bool" => return Ok(Ty::Bool),
"BitVec" => return Ok(Ty::BitVec),
"NaiveDateTime" => return Ok(Ty::NaiveDateTime),
"NaiveDate" => return Ok(Ty::NaiveDate),
"NaiveTime" => return Ok(Ty::NaiveTime),
"MacAddress" => return Ok(Ty::MacAddress),
"Uuid" => return Ok(Ty::Uuid),
"PrimitiveDateTime" => return Ok(Ty::PrimitiveDateTime),
"OffsetDateTime" => return Ok(Ty::OffsetDateTime),
"Date" => return Ok(Ty::Date),
"Time" => return Ok(Ty::Time),
_ => (),
}
if s.starts_with("Option <") {
Self::from_str(extract_chevrons(s).ok_or(())?).map(|x| Ty::Option(Box::new(x)))
} else if s.starts_with("Json <") {
Ok(Ty::Json)
} else if s.starts_with("Point <") && extract_chevrons(s) == Some("f64") {
Ok(Ty::Point)
} else if s.starts_with("Rect <") && extract_chevrons(s) == Some("f64") {
Ok(Ty::Rect)
} else if s.starts_with("LineString <") && extract_chevrons(s) == Some("f64") {
Ok(Ty::LineString)
} else if s.starts_with("DateTime <") {
match extract_chevrons(s).ok_or(())? {
"Utc" | "chrono :: Utc" => Ok(Ty::DateTimeUtc),
"Local" | "chrono :: Local" => Ok(Ty::DateTimeLocal),
"FixedOffset" | "chrono :: FixedOffset" => Ok(Ty::DateTimeFixedOffset),
_ => Err(()),
}
} else {
Ok(Ty::Enum(s.to_snake()))
}
}
}