#![allow(dead_code)]
use convert_case::{Case, Casing};
use prax_schema::ast::*;
use smol_str::SmolStr;
pub fn table_name_to_model_name(table_name: &str) -> String {
let singular = if let Some(stem) = table_name.strip_suffix("ies") {
format!("{}y", stem)
} else if let Some(stem) = table_name.strip_suffix('s')
&& !table_name.ends_with("ss")
{
stem.to_string()
} else {
table_name.to_string()
};
singular.to_case(Case::Pascal)
}
pub fn column_name_to_field_name(column_name: &str) -> String {
column_name.to_case(Case::Camel)
}
pub fn is_id_field(name: &str) -> bool {
name == "id" || name.ends_with("_id")
}
pub fn dummy_span() -> Span {
Span::new(0, 0)
}
pub struct SchemaBuilder {
schema: Schema,
}
impl SchemaBuilder {
pub fn new() -> Self {
Self {
schema: Schema::new(),
}
}
pub fn with_datasource(mut self, provider_str: String, url: String) -> Self {
let provider =
DatabaseProvider::from_str(&provider_str).unwrap_or(DatabaseProvider::PostgreSQL);
let datasource = Datasource {
name: SmolStr::from("db"),
provider,
url: Some(SmolStr::from(url)),
url_env: None,
extensions: vec![],
properties: vec![],
span: dummy_span(),
source_id: None,
};
self.schema.set_datasource(datasource);
self
}
pub fn add_model(&mut self, model: Model) {
self.schema.add_model(model);
}
pub fn add_enum(&mut self, enum_def: Enum) {
self.schema.add_enum(enum_def);
}
pub fn build(self) -> Schema {
self.schema
}
}
impl Default for SchemaBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct ModelBuilder {
name: String,
fields: Vec<Field>,
attributes: Vec<Attribute>,
documentation: Option<String>,
db_name: Option<String>,
}
impl ModelBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
fields: vec![],
attributes: vec![],
documentation: None,
db_name: None,
}
}
pub fn with_db_name(mut self, db_name: impl Into<String>) -> Self {
self.db_name = Some(db_name.into());
self
}
pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
self.documentation = Some(doc.into());
self
}
pub fn add_field(&mut self, field: Field) {
self.fields.push(field);
}
fn add_map_attribute(&mut self, table_name: String) {
let attr = Attribute {
name: Ident::new("map", dummy_span()),
args: vec![AttributeArg::positional(
AttributeValue::String(table_name),
dummy_span(),
)],
span: dummy_span(),
};
self.attributes.push(attr);
}
pub fn add_index(&mut self, fields: Vec<String>, name: Option<String>) {
let field_refs: Vec<SmolStr> = fields.into_iter().map(SmolStr::from).collect();
let mut args = vec![AttributeArg::positional(
AttributeValue::FieldRefList(field_refs),
dummy_span(),
)];
if let Some(name) = name {
args.push(AttributeArg::named(
Ident::new("name", dummy_span()),
AttributeValue::String(name),
dummy_span(),
));
}
let attr = Attribute {
name: Ident::new("index", dummy_span()),
args,
span: dummy_span(),
};
self.attributes.push(attr);
}
pub fn add_unique(&mut self, fields: Vec<String>, name: Option<String>) {
let field_refs: Vec<SmolStr> = fields.into_iter().map(SmolStr::from).collect();
let mut args = vec![AttributeArg::positional(
AttributeValue::FieldRefList(field_refs),
dummy_span(),
)];
if let Some(name) = name {
args.push(AttributeArg::named(
Ident::new("name", dummy_span()),
AttributeValue::String(name),
dummy_span(),
));
}
let attr = Attribute {
name: Ident::new("unique", dummy_span()),
args,
span: dummy_span(),
};
self.attributes.push(attr);
}
pub fn build(mut self) -> Model {
if let Some(db_name) = &self.db_name
&& db_name != &self.name.to_case(Case::Snake)
{
self.add_map_attribute(db_name.clone());
}
let mut model = Model::new(Ident::new(&self.name, dummy_span()), dummy_span());
for field in self.fields {
model.add_field(field);
}
model.attributes = self.attributes;
if let Some(doc) = self.documentation {
model.documentation = Some(Documentation::new(doc, dummy_span()));
}
model
}
}
pub struct FieldBuilder {
name: String,
field_type: FieldType,
modifier: TypeModifier,
attributes: Vec<Attribute>,
documentation: Option<String>,
db_name: Option<String>,
}
impl FieldBuilder {
pub fn new(name: impl Into<String>, field_type: FieldType, modifier: TypeModifier) -> Self {
Self {
name: name.into(),
field_type,
modifier,
attributes: vec![],
documentation: None,
db_name: None,
}
}
pub fn with_db_name(mut self, db_name: impl Into<String>) -> Self {
self.db_name = Some(db_name.into());
self
}
pub fn with_id(mut self) -> Self {
let attr = Attribute {
name: Ident::new("id", dummy_span()),
args: vec![],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_auto(mut self) -> Self {
let attr = Attribute {
name: Ident::new("auto", dummy_span()),
args: vec![],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_updated_at(mut self) -> Self {
let attr = Attribute {
name: Ident::new("updated_at", dummy_span()),
args: vec![],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_unique(mut self) -> Self {
let attr = Attribute {
name: Ident::new("unique", dummy_span()),
args: vec![],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_default(mut self, value: AttributeValue) -> Self {
let attr = Attribute {
name: Ident::new("default", dummy_span()),
args: vec![AttributeArg::positional(value, dummy_span())],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_map(mut self, column_name: String) -> Self {
let attr = Attribute {
name: Ident::new("map", dummy_span()),
args: vec![AttributeArg::positional(
AttributeValue::String(column_name),
dummy_span(),
)],
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_relation(
mut self,
name: Option<String>,
fields: Vec<String>,
references: Vec<String>,
on_delete: Option<String>,
on_update: Option<String>,
map: Option<String>,
) -> Self {
let mut args = vec![];
if let Some(rel_name) = name {
args.push(AttributeArg::positional(
AttributeValue::String(rel_name),
dummy_span(),
));
}
let field_refs: Vec<SmolStr> = fields.into_iter().map(SmolStr::from).collect();
args.push(AttributeArg::named(
Ident::new("fields", dummy_span()),
AttributeValue::FieldRefList(field_refs),
dummy_span(),
));
let ref_refs: Vec<SmolStr> = references.into_iter().map(SmolStr::from).collect();
args.push(AttributeArg::named(
Ident::new("references", dummy_span()),
AttributeValue::FieldRefList(ref_refs),
dummy_span(),
));
if let Some(action) = on_delete {
args.push(AttributeArg::named(
Ident::new("onDelete", dummy_span()),
AttributeValue::Ident(SmolStr::from(action)),
dummy_span(),
));
}
if let Some(action) = on_update {
args.push(AttributeArg::named(
Ident::new("onUpdate", dummy_span()),
AttributeValue::Ident(SmolStr::from(action)),
dummy_span(),
));
}
if let Some(constraint_name) = map {
args.push(AttributeArg::named(
Ident::new("map", dummy_span()),
AttributeValue::String(constraint_name),
dummy_span(),
));
}
let attr = Attribute {
name: Ident::new("relation", dummy_span()),
args,
span: dummy_span(),
};
self.attributes.push(attr);
self
}
pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
self.documentation = Some(doc.into());
self
}
pub fn build(self) -> Field {
let mut builder = self;
if let Some(db_name) = builder.db_name.clone()
&& db_name != builder.name.to_case(Case::Snake)
{
builder = builder.with_map(db_name);
}
let mut field = Field::new(
Ident::new(&builder.name, dummy_span()),
builder.field_type,
builder.modifier,
builder.attributes,
dummy_span(),
);
if let Some(doc) = builder.documentation {
field.documentation = Some(Documentation::new(doc, dummy_span()));
}
field
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_name_to_model_name() {
assert_eq!(table_name_to_model_name("users"), "User");
assert_eq!(table_name_to_model_name("blog_posts"), "BlogPost");
assert_eq!(table_name_to_model_name("categories"), "Category");
assert_eq!(table_name_to_model_name("user_profiles"), "UserProfile");
}
#[test]
fn test_column_name_to_field_name() {
assert_eq!(column_name_to_field_name("id"), "id");
assert_eq!(column_name_to_field_name("created_at"), "createdAt");
assert_eq!(column_name_to_field_name("user_id"), "userId");
}
#[test]
fn test_is_id_field() {
assert!(is_id_field("id"));
assert!(is_id_field("user_id"));
assert!(!is_id_field("email"));
}
#[test]
fn test_schema_builder() {
let builder = SchemaBuilder::new();
let builder = builder.with_datasource(
"postgresql".to_string(),
"postgres://localhost/test".to_string(),
);
let model = ModelBuilder::new("User").with_db_name("users").build();
let mut builder_mut = builder;
builder_mut.add_model(model);
let schema = builder_mut.build();
assert!(schema.datasource.is_some());
assert_eq!(schema.models.len(), 1);
}
#[test]
fn test_model_builder() {
let mut model_builder = ModelBuilder::new("User").with_db_name("users");
let id_field = FieldBuilder::new(
"id",
FieldType::Scalar(ScalarType::Int),
TypeModifier::Required,
)
.with_id()
.with_auto()
.build();
model_builder.add_field(id_field);
let model = model_builder.build();
assert_eq!(model.name().to_string(), "User");
assert_eq!(model.fields.len(), 1);
}
#[test]
fn test_field_builder() {
let field = FieldBuilder::new(
"email",
FieldType::Scalar(ScalarType::String),
TypeModifier::Required,
)
.with_unique()
.with_db_name("email_address")
.build();
assert_eq!(field.name(), "email");
assert!(matches!(
field.field_type,
FieldType::Scalar(ScalarType::String)
));
assert!(field.has_attribute("unique"));
}
}