From 049f500bde9a42943dbf34359eca4950cb263a7d Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Fri, 10 Jan 2025 16:29:26 +0200 Subject: [PATCH] Add support for additional model field types and arrays --- Cargo.toml | 3 +- loco-gen/Cargo.toml | 1 + loco-gen/src/lib.rs | 7 -- loco-gen/src/mappings.json | 206 ++++++++++++++++++++++++++++--------- loco-gen/src/model.rs | 136 +++++++++++++++++++++++- src/schema.rs | 113 +++++++++++++++++++- 6 files changed, 407 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c264918cf..c512aaf98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ fs-err = "2.11.0" # mailer tera = "1.19.1" thousands = "0.2.0" -heck = "0.4.0" +heck = { workspace = true } cruet = "0.13.0" lettre = { version = "0.11.4", default-features = false, features = [ "builder", @@ -179,6 +179,7 @@ tower-http = { version = "0.6.1", features = [ "set-header", "compression-full", ] } +heck = "0.4.0" [dependencies.sea-orm-migration] optional = true diff --git a/loco-gen/Cargo.toml b/loco-gen/Cargo.toml index a0d83fbeb..424ea6a0a 100644 --- a/loco-gen/Cargo.toml +++ b/loco-gen/Cargo.toml @@ -23,6 +23,7 @@ regex = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } colored = { workspace = true } +heck = { workspace = true } clap = { version = "4.4.7", features = ["derive"] } duct = "0.13" diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index fc26b8f67..dcb8463ea 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -107,13 +107,6 @@ impl Mappings { .map(|f| &f.name) .collect::>() } - pub fn rust_fields(&self) -> Vec<&String> { - self.field_types - .iter() - .filter(|f| f.rust.is_some()) - .map(|f| &f.name) - .collect::>() - } } static MAPPINGS: OnceLock = OnceLock::new(); diff --git a/loco-gen/src/mappings.json b/loco-gen/src/mappings.json index 9b1418555..88b7c4a08 100644 --- a/loco-gen/src/mappings.json +++ b/loco-gen/src/mappings.json @@ -2,219 +2,248 @@ "field_types": [ { "name": "uuid", - "rust": "Uuid", "schema": "uuid_uniq", "col_type": "UuidUniq" }, { "name": "uuid_col", - "rust": "Option", "schema": "uuid_null", "col_type": "UuidNull" }, { "name": "uuid_col!", - "rust": "Uuid", "schema": "uuid", "col_type": "Uuid" }, { "name": "string", - "rust": "Option", "schema": "string_null", "col_type": "StringNull" }, { "name": "string!", - "rust": "String", "schema": "string", "col_type": "String" }, { "name": "string^", - "rust": "String", "schema": "string_uniq", "col_type": "StringUniq" }, { "name": "text", - "rust": "Option", "schema": "text_null", "col_type": "TextNull" }, { "name": "text!", - "rust": "String", "schema": "text", "col_type": "Text" }, + { + "name": "tiny_unsigned", + "schema": "tiny_unsigned_null", + "col_type": "TinyUnsignedNull" + }, + { + "name": "tiny_unsigned!", + "schema": "tiny_unsigned", + "col_type": "TinyUnsigned" + }, + { + "name": "tiny_unsigned^", + "schema": "tiny_unsigned_uniq", + "col_type": "TinyUnsignedUniq" + }, + { + "name": "small_unsigned", + "schema": "small_unsigned_null", + "col_type": "SmallUnsignedNull" + }, + { + "name": "small_unsigned!", + "schema": "small_unsigned", + "col_type": "SmallUnsigned" + }, + { + "name": "small_unsigned^", + "schema": "small_unsigned_uniq", + "col_type": "SmallUnsignedUniq" + }, + { + "name": "big_unsigned", + "schema": "big_unsigned_null", + "col_type": "BigUnsignedNull" + }, + { + "name": "big_unsigned!", + "schema": "big_unsigned", + "col_type": "BigUnsigned" + }, + { + "name": "big_unsigned^", + "schema": "big_unsigned_uniq", + "col_type": "BigUnsignedUniq" + }, { "name": "tiny_int", - "rust": "Option", "schema": "tiny_integer_null", "col_type": "TinyIntegerNull" }, { "name": "tiny_int!", - "rust": "i16", "schema": "tiny_integer", "col_type": "TinyInteger" }, { "name": "tiny_int^", - "rust": "i16", "schema": "tiny_integer_uniq", "col_type": "TinyIntegerUniq" }, { "name": "small_int", - "rust": "Option", "schema": "small_integer_null", "col_type": "SmallIntegerNull" }, { "name": "small_int!", - "rust": "i16", "schema": "small_integer", "col_type": "SmallInteger" }, { "name": "small_int^", - "rust": "i16", "schema": "small_integer_uniq", "col_type": "SmallIntegerUniq" }, { "name": "int", - "rust": "Option", "schema": "integer_null", "col_type": "IntegerNull" }, { "name": "int!", - "rust": "i32", "schema": "integer", "col_type": "Integer" }, { "name": "int^", - "rust": "i32", "schema": "integer_uniq", "col_type": "IntegerUniq" }, { "name": "big_int", - "rust": "Option", "schema": "big_integer_null", "col_type": "BigIntegerNull" }, { "name": "big_int!", - "rust": "i64", "schema": "big_integer", "col_type": "BigInteger" }, { "name": "big_int^", - "rust": "i64", "schema": "big_integer_uniq", "col_type": "BigIntegerUniq" }, { "name": "float", - "rust": "Option", "schema": "float_null", "col_type": "FloatNull" }, { "name": "float!", - "rust": "f32", "schema": "float", "col_type": "Float" }, { "name": "double", - "rust": "Option", "schema": "double_null", "col_type": "DoubleNull" }, { "name": "double!", - "rust": "f64", "schema": "double", "col_type": "Double" }, { "name": "decimal", - "rust": "Option", "schema": "decimal_null", "col_type": "DecimalNull" }, { "name": "decimal!", - "rust": "Decimal", "schema": "decimal", "col_type": "Decimal" }, { "name": "decimal_len", - "rust": "Option", "schema": "decimal_len_null", "col_type": "DecimalLenNull", - "arity":2 + "arity": 2 }, { "name": "decimal_len!", - "rust": "Decimal", "schema": "decimal_len", "col_type": "DecimalLen", - "arity":2 + "arity": 2 }, { "name": "bool", - "rust": "Option", "schema": "boolean_null", "col_type": "BooleanNull" }, { "name": "bool!", - "rust": "bool", "schema": "boolean", "col_type": "Boolean" }, { "name": "tstz", - "rust": "Option", "schema": "timestamp_with_time_zone_null", "col_type": "TimestampWithTimeZoneNull" }, { "name": "tstz!", - "rust": "DateTimeWithTimeZone", "schema": "timestamp_with_time_zone", "col_type": "TimestampWithTimeZone" }, { "name": "date", - "rust": "Option", "schema": "date_null", "col_type": "DateNull" }, { "name": "date!", - "rust": "Date", "schema": "date", "col_type": "Date" }, + { + "name": "date^", + "schema": "date_uniq", + "col_type": "DateUniq" + }, + { + "name": "date_time", + "schema": "date_time_null", + "col_type": "DateTimeNull" + }, + { + "name": "date_time!", + "schema": "date_time", + "col_type": "DateTime" + }, + { + "name": "date_time^", + "schema": "date_time_uniq", + "col_type": "DateTimeUniq" + }, { "name": "ts", - "rust": "Option", "schema": "timestamp_null", "col_type": "TimestampNull" }, { "name": "ts!", - "rust": "DateTime", "schema": "timestamp", "col_type": "Timestamp" }, @@ -226,45 +255,130 @@ }, { "name": "json!", - "rust": "serde_json::Value", "schema": "json", "col_type": "Json" }, { "name": "jsonb", - "rust": "Option", "schema": "json_binary_null", "col_type": "JsonBinaryNull" }, { "name": "jsonb!", - "rust": "serde_json::Value", "schema": "json_binary", "col_type": "JsonBinary" }, { "name": "blob", - "rust": "Option>", "schema": "blob_null", "col_type": "BlobNull" }, { "name": "blob!", - "rust": "Vec", "schema": "blob", "col_type": "Blob" }, { "name": "money", - "rust": "Option", "schema": "money_null", "col_type": "MoneyNull" }, { "name": "money!", - "rust": "Decimal", "schema": "money", "col_type": "Money" + }, + { + "name": "money^", + "schema": "money_uniq", + "col_type": "MoneyUniq" + }, + { + "name": "unsigned!", + "schema": "unsigned", + "col_type": "Unsigned" + }, + { + "name": "unsigned", + "schema": "unsigned_null", + "col_type": "UnsignedNull" + }, + { + "name": "unsigned^", + "schema": "unsigned_uniq", + "col_type": "UnsignedUniq" + }, + { + "name": "binary_len!", + "schema": "binary_len", + "col_type": "BinaryLen", + "arity": 1 + }, + { + "name": "binary_len", + "schema": "binary_len_null", + "col_type": "BinaryLenNull", + "arity": 1 + }, + { + "name": "binary_len^", + "schema": "binary_len_uniq", + "col_type": "BinaryLenUniq", + "arity": 1 + }, + { + "name": "var_binary!", + "schema": "var_binary", + "col_type": "VarBinary", + "arity": 1 + }, + { + "name": "var_binary", + "schema": "var_binary_null", + "col_type": "VarBinaryNull", + "arity": 1 + }, + { + "name": "var_binary^", + "schema": "var_binary_uniq", + "col_type": "VarBinaryUniq", + "arity": 1 + }, + { + "name": "varbit_len!", + "schema": "varbit", + "col_type": "VarBitLen", + "arity": 1 + }, + { + "name": "varbit_len", + "schema": "varbit_null", + "col_type": "VarBitLenNull", + "arity": 1 + }, + { + "name": "varbit_len^", + "schema": "varbit_uniq", + "col_type": "VarBitLenUniq", + "arity": 1 + }, + { + "name": "array", + "schema": "array", + "col_type": "array_null", + "arity": 1 + }, + { + "name": "array!", + "schema": "array", + "col_type": "array", + "arity": 1 + }, + { + "name": "array^", + "schema": "array", + "col_type": "array_uniq", + "arity": 1 } ] -} +} \ No newline at end of file diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index ec812c8da..3f38c0c7b 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, env::current_dir, path::Path}; use chrono::Utc; use duct::cmd; +use heck::ToUpperCamelCase; use rrgen::RRgen; use serde_json::json; @@ -71,10 +72,30 @@ pub fn get_columns_and_references( ))); } - columns.push(( - fname.to_string(), - format!("{}({})", col_type, params.join(",")), - )); + let col = match ftype.as_ref() { + "array" | "array^" | "array!" => { + let array_kind = match params.as_slice() { + [array_kind] => Ok(array_kind), + _ => Err(Error::Message(format!( + "type: `{ftype}` requires exactly {arity} parameter{}, but {} were given (`{}`).", + if arity == 1 { "" } else { "s" }, + params.len(), + params.join(",") + ))), + }?; + + format!( + r#"{}(ArrayColType::{})"#, + col_type, + array_kind.to_upper_camel_case() + ) + } + &_ => { + format!("{}({})", col_type, params.join(",")) + } + }; + + columns.push((fname.to_string(), col)); } } } @@ -125,3 +146,110 @@ pub fn generate( Ok(gen_result) } + +#[cfg(test)] +mod tests { + use super::*; + + fn to_field(name: &str, field_type: &str) -> (String, String) { + (name.to_string(), field_type.to_string()) + } + + #[test] + fn test_get_columns_with_field_types() { + let fields = [ + to_field("expect_string_null", "string"), + to_field("expect_string", "string!"), + to_field("expect_unique", "string^"), + ]; + let res = get_columns_and_references(&fields).expect("Failed to parse fields"); + + let expected_columns = vec![ + to_field("expect_string_null", "StringNull"), + to_field("expect_string", "String"), + to_field("expect_unique", "StringUniq"), + ]; + let expected_references: Vec<(String, String)> = vec![]; + + assert_eq!(res, (expected_columns, expected_references)); + } + #[test] + fn test_get_columns_with_array_types() { + let fields = [ + to_field("expect_array_null", "array:string"), + to_field("expect_array", "array!:string"), + to_field("expect_array_uniq", "array^:string"), + ]; + let res = get_columns_and_references(&fields).expect("Failed to parse fields"); + + let expected_columns = vec![ + to_field("expect_array_null", "array_null(ArrayColType::String)"), + to_field("expect_array", "array(ArrayColType::String)"), + to_field("expect_array_uniq", "array_uniq(ArrayColType::String)"), + ]; + let expected_references: Vec<(String, String)> = vec![]; + + assert_eq!(res, (expected_columns, expected_references)); + } + + #[test] + fn test_get_references_from_fields() { + let fields = [ + to_field("user", "references"), + to_field("post", "references"), + ]; + let res = get_columns_and_references(&fields).expect("Failed to parse fields"); + + let expected_columns: Vec<(String, String)> = vec![]; + let expected_references = vec![to_field("user", ""), to_field("post", "")]; + + assert_eq!(res, (expected_columns, expected_references)); + } + + #[test] + fn test_ignore_fields_are_filtered_out() { + let mut fields = vec![to_field("name", "string")]; + + for ignore_field in IGNORE_FIELDS { + fields.push(to_field(ignore_field, "string")); + } + + let res = get_columns_and_references(&fields).expect("Failed to parse fields"); + + let expected_columns = vec![to_field("name", "StringNull")]; + let expected_references: Vec<(String, String)> = vec![]; + + assert_eq!(res, (expected_columns, expected_references)); + } + + #[test] + fn validate_arity() { + // field not expected arity, but given 2 + let fields = vec![to_field("name", "string:2")]; + let res = get_columns_and_references(&fields); + if let Err(err) = res { + assert_eq!( + err.to_string(), + "type: `string` requires specifying 0 parameters, but only 1 were given (`2`)." + ); + } else { + panic!("Expected Err, but got Ok: {res:?}"); + } + + // references not expected arity, but given 2 + let references = vec![to_field("post:2", "")]; + let res = get_columns_and_references(&references); + if let Err(err) = res { + let mappings = get_mappings(); + assert_eq!( + err.to_string(), + format!( + "type: `` not found. try any of: {:?}", + mappings.schema_fields() + ) + ); + } else { + panic!("Expected Err, but got Ok: {res:?}"); + } + } +} diff --git a/src/schema.rs b/src/schema.rs index 96ac2132f..809e4e6f9 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,7 +4,7 @@ use sea_orm::{ Alias, ColumnDef, Expr, Index, IntoIden, PgInterval, Table, TableAlterStatement, TableCreateStatement, TableForeignKey, }, - DbErr, ForeignKeyAction, + ColumnType, DbErr, ForeignKeyAction, }; pub use sea_orm_migration::schema::*; use sea_orm_migration::{prelude::Iden, sea_query, SchemaManager}; @@ -79,6 +79,18 @@ pub enum ColType { Integer, IntegerNull, IntegerUniq, + Unsigned, + UnsignedNull, + UnsignedUniq, + TinyUnsigned, + TinyUnsignedNull, + TinyUnsignedUniq, + SmallUnsigned, + SmallUnsignedNull, + SmallUnsignedUniq, + BigUnsigned, + BigUnsignedNull, + BigUnsignedUniq, TinyInteger, TinyIntegerNull, TinyIntegerUniq, @@ -108,6 +120,9 @@ pub enum ColType { Date, DateNull, DateUniq, + DateTime, + DateTimeNull, + DateTimeUniq, Time, TimeNull, TimeUniq, @@ -117,6 +132,12 @@ pub enum ColType { Binary, BinaryNull, BinaryUniq, + BinaryLen(u32), + BinaryLenNull(u32), + BinaryLenUniq(u32), + VarBinary(u32), + VarBinaryNull(u32), + VarBinaryUniq(u32), // Added variants based on the JSON TimestampWithTimeZone, TimestampWithTimeZoneNull, @@ -135,9 +156,72 @@ pub enum ColType { Uuid, UuidNull, UuidUniq, + VarBitLen(u32), + VarBitLenNull(u32), + VarBitLenUniq(u32), + Array(ColumnType), + ArrayNull(ColumnType), + ArrayUniq(ColumnType), +} + +pub enum ArrayColType { + String, + Text, + Char, + Float, + Int, + TinyInt, + SmallInt, + BigInt, + TinyUnsigned, + SmallUnsigned, + Unsigned, + BigUnsigned, + Double, + Boolean, +} + +impl ColType { + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn array(kind: ArrayColType) -> Self { + Self::Array(Self::array_col_type(&kind)) + } + + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn array_uniq(kind: ArrayColType) -> Self { + Self::ArrayUniq(Self::array_col_type(&kind)) + } + + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn array_null(kind: ArrayColType) -> Self { + Self::ArrayNull(Self::array_col_type(&kind)) + } + + fn array_col_type(kind: &ArrayColType) -> ColumnType { + match kind { + ArrayColType::String => ColumnType::string(None), + ArrayColType::Text => ColumnType::Text, + ArrayColType::Char => ColumnType::Char(None), + ArrayColType::Float => ColumnType::Float, + ArrayColType::Int => ColumnType::Integer, + ArrayColType::TinyInt => ColumnType::TinyInteger, + ArrayColType::SmallInt => ColumnType::SmallInteger, + ArrayColType::BigInt => ColumnType::BigInteger, + ArrayColType::TinyUnsigned => ColumnType::TinyUnsigned, + ArrayColType::SmallUnsigned => ColumnType::SmallUnsigned, + ArrayColType::Unsigned => ColumnType::Unsigned, + ArrayColType::BigUnsigned => ColumnType::BigUnsigned, + ArrayColType::Double => ColumnType::Double, + ArrayColType::Boolean => ColumnType::Boolean, + } + } } impl ColType { + #[allow(clippy::too_many_lines)] fn to_def(&self, name: impl IntoIden) -> ColumnDef { match self { Self::PkAuto => pk_auto(name), @@ -163,6 +247,18 @@ impl ColType { Self::TinyInteger => tiny_integer(name), Self::TinyIntegerNull => tiny_integer_null(name), Self::TinyIntegerUniq => tiny_integer_uniq(name), + Self::Unsigned => unsigned(name), + Self::UnsignedNull => unsigned_null(name), + Self::UnsignedUniq => unsigned_uniq(name), + Self::TinyUnsigned => tiny_unsigned(name), + Self::TinyUnsignedNull => tiny_unsigned_null(name), + Self::TinyUnsignedUniq => tiny_unsigned_uniq(name), + Self::SmallUnsigned => small_unsigned(name), + Self::SmallUnsignedNull => small_unsigned_null(name), + Self::SmallUnsignedUniq => small_unsigned_uniq(name), + Self::BigUnsigned => big_unsigned(name), + Self::BigUnsignedNull => big_unsigned_null(name), + Self::BigUnsignedUniq => big_unsigned_uniq(name), Self::SmallInteger => small_integer(name), Self::SmallIntegerNull => small_integer_null(name), Self::SmallIntegerUniq => small_integer_uniq(name), @@ -189,6 +285,9 @@ impl ColType { Self::Date => date(name), Self::DateNull => date_null(name), Self::DateUniq => date_uniq(name), + Self::DateTime => date_time(name), + Self::DateTimeNull => date_time_null(name), + Self::DateTimeUniq => date_time_uniq(name), Self::Time => time(name), Self::TimeNull => time_null(name), Self::TimeUniq => time_uniq(name), @@ -198,6 +297,12 @@ impl ColType { Self::Binary => binary(name), Self::BinaryNull => binary_null(name), Self::BinaryUniq => binary_uniq(name), + Self::BinaryLen(len) => binary_len(name, *len), + Self::BinaryLenNull(len) => binary_len_null(name, *len), + Self::BinaryLenUniq(len) => binary_len_uniq(name, *len), + Self::VarBinary(len) => var_binary(name, *len), + Self::VarBinaryNull(len) => var_binary_null(name, *len), + Self::VarBinaryUniq(len) => var_binary_uniq(name, *len), Self::TimestampWithTimeZone => timestamptz(name), Self::TimestampWithTimeZoneNull => timestamptz_null(name), Self::Json => json(name), @@ -215,6 +320,12 @@ impl ColType { Self::Uuid => uuid(name), Self::UuidNull => uuid_null(name), Self::UuidUniq => uuid_uniq(name), + Self::VarBitLen(len) => varbit(name, *len), + Self::VarBitLenNull(len) => varbit_null(name, *len), + Self::VarBitLenUniq(len) => varbit_uniq(name, *len), + Self::Array(kind) => array(name, kind.clone()), + Self::ArrayNull(kind) => array_null(name, kind.clone()), + Self::ArrayUniq(kind) => array_uniq(name, kind.clone()), } } }