From 049f500bde9a42943dbf34359eca4950cb263a7d Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Fri, 10 Jan 2025 16:29:26 +0200 Subject: [PATCH 01/11] 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()), } } } From df2579f695dd0e4e4bd68fb200919d7fac64a071 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Fri, 10 Jan 2025 16:36:19 +0200 Subject: [PATCH 02/11] clippy --- loco-gen/src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index 3f38c0c7b..317c33ad5 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -85,7 +85,7 @@ pub fn get_columns_and_references( }?; format!( - r#"{}(ArrayColType::{})"#, + r"{}(ArrayColType::{})", col_type, array_kind.to_upper_camel_case() ) From d2a9b0815556c2c671d3b2efdeb68bfaa35e4607 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Fri, 10 Jan 2025 22:19:06 +0200 Subject: [PATCH 03/11] revert rust field --- loco-gen/src/mappings.json | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/loco-gen/src/mappings.json b/loco-gen/src/mappings.json index 88b7c4a08..469dd06df 100644 --- a/loco-gen/src/mappings.json +++ b/loco-gen/src/mappings.json @@ -2,248 +2,297 @@ "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", + "rust": "Option", "schema": "tiny_unsigned_null", "col_type": "TinyUnsignedNull" }, { "name": "tiny_unsigned!", + "rust": "i16", "schema": "tiny_unsigned", "col_type": "TinyUnsigned" }, { "name": "tiny_unsigned^", + "rust": "i16", "schema": "tiny_unsigned_uniq", "col_type": "TinyUnsignedUniq" }, { "name": "small_unsigned", + "rust": "Option", "schema": "small_unsigned_null", "col_type": "SmallUnsignedNull" }, { "name": "small_unsigned!", + "rust": "i16", "schema": "small_unsigned", "col_type": "SmallUnsigned" }, { "name": "small_unsigned^", + "rust": "i16", "schema": "small_unsigned_uniq", "col_type": "SmallUnsignedUniq" }, { "name": "big_unsigned", + "rust": "Option", "schema": "big_unsigned_null", "col_type": "BigUnsignedNull" }, { "name": "big_unsigned!", + "rust": "i64", "schema": "big_unsigned", "col_type": "BigUnsigned" }, { "name": "big_unsigned^", + "rust": "i64", "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 }, { "name": "decimal_len!", + "rust": "Decimal", "schema": "decimal_len", "col_type": "DecimalLen", "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^", + "rust": "Date", "schema": "date_uniq", "col_type": "DateUniq" }, { "name": "date_time", + "rust": "Option", "schema": "date_time_null", "col_type": "DateTimeNull" }, { "name": "date_time!", + "rust": "DateTime", "schema": "date_time", "col_type": "DateTime" }, { "name": "date_time^", + "rust": "DateTime", "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" }, @@ -255,127 +304,150 @@ }, { "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^", + "rust": "Decimal", "schema": "money_uniq", "col_type": "MoneyUniq" }, { "name": "unsigned!", + "rust": "i32", "schema": "unsigned", "col_type": "Unsigned" }, { "name": "unsigned", + "rust": "Option", "schema": "unsigned_null", "col_type": "UnsignedNull" }, { "name": "unsigned^", + "rust": "i32", "schema": "unsigned_uniq", "col_type": "UnsignedUniq" }, { "name": "binary_len!", + "rust": "Vec", "schema": "binary_len", "col_type": "BinaryLen", "arity": 1 }, { "name": "binary_len", + "rust": "Option>", "schema": "binary_len_null", "col_type": "BinaryLenNull", "arity": 1 }, { "name": "binary_len^", + "rust": "Vec", "schema": "binary_len_uniq", "col_type": "BinaryLenUniq", "arity": 1 }, { "name": "var_binary!", + "rust": "Vec", "schema": "var_binary", "col_type": "VarBinary", "arity": 1 }, { "name": "var_binary", + "rust": "Option>", "schema": "var_binary_null", "col_type": "VarBinaryNull", "arity": 1 }, { "name": "var_binary^", + "rust": "Vec", "schema": "var_binary_uniq", "col_type": "VarBinaryUniq", "arity": 1 }, { "name": "varbit_len!", + "rust": "Vec", "schema": "varbit", "col_type": "VarBitLen", "arity": 1 }, { "name": "varbit_len", + "rust": "Option>", "schema": "varbit_null", "col_type": "VarBitLenNull", "arity": 1 }, { "name": "varbit_len^", + "rust": "Vec", "schema": "varbit_uniq", "col_type": "VarBitLenUniq", "arity": 1 }, { "name": "array", + "rust": "Option>", "schema": "array", "col_type": "array_null", "arity": 1 }, { "name": "array!", + "rust": "Option>", "schema": "array", "col_type": "array", "arity": 1 }, { "name": "array^", + "rust": "Option>", "schema": "array", "col_type": "array_uniq", "arity": 1 From 331a005b313e4cab38795de11b1b407dad874027 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 17:45:07 +0200 Subject: [PATCH 04/11] adding rust types for array --- loco-gen/src/lib.rs | 268 ++++++++++++++++++++++++++++++++++--- loco-gen/src/mappings.json | 44 +++++- loco-gen/src/model.rs | 16 +-- loco-gen/src/scaffold.rs | 16 +-- src/schema.rs | 10 -- 5 files changed, 294 insertions(+), 60 deletions(-) diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index 0dc912430..bb3f3fe44 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -8,6 +8,7 @@ use serde_json::{json, Value}; mod controller; use colored::Colorize; use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, str::FromStr, @@ -64,48 +65,105 @@ pub type Result = std::result::Result; #[derive(Serialize, Deserialize, Debug)] struct FieldType { name: String, - rust: Option, - schema: Option, - col_type: Option, + rust: RustType, + schema: String, + col_type: String, #[serde(default)] arity: usize, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +enum RustType { + String(String), + Map(HashMap), +} + #[derive(Serialize, Deserialize, Debug)] struct Mappings { field_types: Vec, } impl Mappings { - pub fn rust_field(&self, field: &str) -> Option<&String> { + fn error_unrecognized_default_field(&self, field: &str) -> Error { + Self::error_unrecognized(field, self.schema_fields()) + } + fn error_unrecognized(field: &str, allow_fields: Vec<&String>) -> Error { + Error::Message(format!( + "type: `{}` not found. try any of: `{}`", + field, + allow_fields + .iter() + .map(|&s| s.to_string()) + .collect::>() + .join(",") + )) + } + pub fn rust_field_with_params(&self, field: &str, params: &Vec) -> Result<&str> { + match field { + "array" | "array^" | "array!" => { + if let RustType::Map(ref map) = self.rust_field_kind(field)? { + if let [single] = params.as_slice() { + Ok(map + .get(single) + .ok_or_else(|| Self::error_unrecognized(field, map.keys().collect()))?) + } else { + Err(self.error_unrecognized_default_field(field)) + } + } else { + panic!("array field should configured as array") + } + } + + _ => self.rust_field(field), + } + } + + pub fn rust_field_kind(&self, field: &str) -> Result<&RustType> { self.field_types .iter() .find(|f| f.name == field) - .and_then(|f| f.rust.as_ref()) + .map(|f| &f.rust) + .ok_or_else(|| self.error_unrecognized_default_field(field)) } - pub fn schema_field(&self, field: &str) -> Option<&String> { + + pub fn rust_field(&self, field: &str) -> Result<&str> { self.field_types .iter() .find(|f| f.name == field) - .and_then(|f| f.schema.as_ref()) + .map(|f| &f.rust) + .ok_or_else(|| self.error_unrecognized_default_field(field)) + .and_then(|rust_type| match rust_type { + RustType::String(s) => Ok(s), + RustType::Map(_) => Err(Error::Message(format!( + "type `{field}` need params to get the rust field type" + ))), + }) + .map(std::string::String::as_str) } - pub fn col_type_field(&self, field: &str) -> Option<&String> { + + pub fn schema_field(&self, field: &str) -> Result<&str> { self.field_types .iter() .find(|f| f.name == field) - .and_then(|f| f.col_type.as_ref()) + .map(|f| f.schema.as_str()) + .ok_or_else(|| self.error_unrecognized_default_field(field)) } - pub fn col_type_arity(&self, field: &str) -> Option { + pub fn col_type_field(&self, field: &str) -> Result<&str> { self.field_types .iter() .find(|f| f.name == field) - .map(|f| f.arity) + .map(|f| f.col_type.as_str()) + .ok_or_else(|| self.error_unrecognized_default_field(field)) } - pub fn schema_fields(&self) -> Vec<&String> { + pub fn col_type_arity(&self, field: &str) -> Result { self.field_types .iter() - .filter(|f| f.schema.is_some()) - .map(|f| &f.name) - .collect::>() + .find(|f| f.name == field) + .map(|f| f.arity) + .ok_or_else(|| self.error_unrecognized_default_field(field)) + } + pub fn schema_fields(&self) -> Vec<&String> { + self.field_types.iter().map(|f| &f.name).collect::>() } } @@ -475,4 +533,184 @@ mod tests { ); } } + + fn test_mapping() -> Mappings { + Mappings { + field_types: vec![ + FieldType { + name: "array".to_string(), + rust: RustType::Map(HashMap::from([ + ("string".to_string(), "Vec".to_string()), + ("chat".to_string(), "Vec".to_string()), + ("int".to_string(), "Vec".to_string()), + ])), + schema: "array".to_string(), + col_type: "array_null".to_string(), + arity: 1, + }, + FieldType { + name: "string^".to_string(), + rust: RustType::String("String".to_string()), + schema: "string_uniq".to_string(), + col_type: "StringUniq".to_string(), + arity: 0, + }, + ], + } + } + + #[test] + fn can_get_schema_fields_from_mapping() { + let mapping = test_mapping(); + assert_eq!( + mapping.schema_fields(), + Vec::from([&"array".to_string(), &"string^".to_string()]) + ); + } + + #[test] + fn can_get_col_type_arity_from_mapping() { + let mapping = test_mapping(); + + assert_eq!(mapping.col_type_arity("array").expect("Get array arity"), 1); + assert_eq!( + mapping + .col_type_arity("string^") + .expect("Get string^ arity"), + 0 + ); + + assert_eq!( + mapping + .col_type_arity("unknown") + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } + + #[test] + fn can_get_col_type_field_from_mapping() { + let mapping = test_mapping(); + + assert_eq!( + mapping.col_type_field("array").expect("Get array field"), + "array_null" + ); + + assert_eq!( + mapping + .col_type_field("unknown") + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } + + #[test] + fn can_get_schema_field_from_mapping() { + let mapping = test_mapping(); + + assert_eq!( + mapping.schema_field("string^").expect("Get string^ schema"), + "string_uniq" + ); + + assert_eq!( + mapping + .schema_field("unknown") + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } + + #[test] + fn can_get_rust_field_from_mapping() { + let mapping = test_mapping(); + + assert_eq!( + mapping + .rust_field("string^") + .expect("Get string^ rust field"), + "String" + ); + + assert_eq!( + mapping + .rust_field("array") + .expect_err("expect error") + .to_string(), + "type `array` need params to get the rust field type".to_string() + ); + + assert_eq!( + mapping + .rust_field("unknown") + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } + + #[test] + fn can_get_rust_field_kind_from_mapping() { + let mapping = test_mapping(); + + assert!(mapping.rust_field_kind("string^").is_ok()); + + assert_eq!( + mapping + .rust_field_kind("unknown") + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } + + #[test] + fn can_get_rust_field_with_params_from_mapping() { + let mapping = test_mapping(); + + assert_eq!( + mapping + .rust_field_with_params("string^", &vec!["string".to_string()]) + .expect("Get string^ rust field"), + "String" + ); + + assert_eq!( + mapping + .rust_field_with_params("array", &vec!["string".to_string()]) + .expect("Get string^ rust field"), + "Vec" + ); + assert_eq!( + mapping + .rust_field_with_params("array", &vec!["unknown".to_string()]) + .expect_err("expect error") + .to_string(), + "type: `array` not found. try any of: `int,string,chat`" + ); + + assert_eq!( + mapping + .rust_field_with_params("unknown", &vec![]) + .expect_err("expect error") + .to_string(), + mapping + .error_unrecognized_default_field("unknown") + .to_string() + ); + } } diff --git a/loco-gen/src/mappings.json b/loco-gen/src/mappings.json index 469dd06df..97888ad83 100644 --- a/loco-gen/src/mappings.json +++ b/loco-gen/src/mappings.json @@ -432,22 +432,52 @@ "arity": 1 }, { - "name": "array", - "rust": "Option>", + "name": "array!", + "rust": { + "string": "Vec", + "char": "Vec", + "text": "Vec", + "float": "Vec", + "int": "Vec", + "small_int": "Vec", + "big_int": "Vec", + "double": "Vec", + "bool": "Vec" + }, "schema": "array", - "col_type": "array_null", + "col_type": "array", "arity": 1 }, { - "name": "array!", - "rust": "Option>", + "name": "array", + "rust": { + "string": "Option>", + "char": "Option>", + "text": "Option>", + "float": "Option>", + "int": "Option>", + "small_int": "Option>", + "big_int": "Option>", + "double": "Option>", + "bool": "Option>" + }, "schema": "array", - "col_type": "array", + "col_type": "array_null", "arity": 1 }, { "name": "array^", - "rust": "Option>", + "rust": { + "string": "Vec", + "char": "Vec", + "text": "Vec", + "float": "Vec", + "int": "Vec", + "small_int": "Vec", + "big_int": "Vec", + "double": "Vec", + "bool": "Vec" + }, "schema": "array", "col_type": "array_uniq", "arity": 1 diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index 317c33ad5..4c813226a 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -44,24 +44,12 @@ pub fn get_columns_and_references( } crate::infer::FieldType::Type(ftype) => { let mappings = get_mappings(); - let col_type = mappings.col_type_field(ftype.as_str()).ok_or_else(|| { - Error::Message(format!( - "type: `{}` not found. try any of: {:?}", - ftype, - mappings.schema_fields() - )) - })?; + let col_type = mappings.col_type_field(ftype.as_str())?; columns.push((fname.to_string(), col_type.to_string())); } crate::infer::FieldType::TypeWithParameters(ftype, params) => { let mappings = get_mappings(); - let col_type = mappings.col_type_field(ftype.as_str()).ok_or_else(|| { - Error::Message(format!( - "type: `{}` not found. try any of: {:?}", - ftype, - mappings.schema_fields() - )) - })?; + let col_type = mappings.col_type_field(ftype.as_str())?; let arity = mappings.col_type_arity(ftype.as_str()).unwrap_or_default(); if params.len() != arity { return Err(Error::Message(format!( diff --git a/loco-gen/src/scaffold.rs b/loco-gen/src/scaffold.rs index 4e03e18cc..40f4d5d3e 100644 --- a/loco-gen/src/scaffold.rs +++ b/loco-gen/src/scaffold.rs @@ -41,24 +41,12 @@ pub fn generate( } crate::infer::FieldType::Type(ftype) => { let mappings = get_mappings(); - let rust_type = mappings.rust_field(ftype.as_str()).ok_or_else(|| { - Error::Message(format!( - "type: `{}` not found. try any of: {:?}", - ftype, - mappings.schema_fields() - )) - })?; + let rust_type = mappings.rust_field(ftype.as_str())?; columns.push((fname.to_string(), rust_type.to_string(), ftype)); } crate::infer::FieldType::TypeWithParameters(ftype, params) => { let mappings = get_mappings(); - let rust_type = mappings.rust_field(ftype.as_str()).ok_or_else(|| { - Error::Message(format!( - "type: `{}` not found. try any of: {:?}", - ftype, - mappings.schema_fields() - )) - })?; + let rust_type = mappings.rust_field_with_params(ftype.as_str(), ¶ms)?; let arity = mappings.col_type_arity(ftype.as_str()).unwrap_or_default(); if params.len() != arity { return Err(Error::Message(format!( diff --git a/src/schema.rs b/src/schema.rs index 809e4e6f9..cf572ffc4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -170,13 +170,8 @@ pub enum ArrayColType { Char, Float, Int, - TinyInt, SmallInt, BigInt, - TinyUnsigned, - SmallUnsigned, - Unsigned, - BigUnsigned, Double, Boolean, } @@ -207,13 +202,8 @@ impl ColType { 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, } From 6fc81338e58e886fa614bd17378f7883ed48a718 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 19:04:33 +0200 Subject: [PATCH 05/11] scaffold ui --- .../src/templates/scaffold/html/view_create.t | 30 +++++++++++- .../src/templates/scaffold/html/view_edit.t | 31 +++++++++++- .../src/templates/scaffold/htmx/view_create.t | 45 +++++++++++++++-- .../src/templates/scaffold/htmx/view_edit.t | 48 ++++++++++++++++++- 4 files changed, 146 insertions(+), 8 deletions(-) diff --git a/loco-gen/src/templates/scaffold/html/view_create.t b/loco-gen/src/templates/scaffold/html/view_create.t index 5095be362..c9bb56b9e 100644 --- a/loco-gen/src/templates/scaffold/html/view_create.t +++ b/loco-gen/src/templates/scaffold/html/view_create.t @@ -43,6 +43,16 @@ Create {{file_name}} {% elif column.2 == "json!" or column.2 == "jsonb!" -%} + {% elif column.2 == "array!" or column.2 == "array^" -%} +
+ +
+ + {% elif column.2 == "array" -%} +
+ +
+ {% endif -%} {% endfor -%} @@ -54,4 +64,22 @@ Create {{file_name}}
Back to {{name | plural}} -{% raw %}{% endblock content %}{% endraw %} \ No newline at end of file +{% raw %}{% endblock content %}{% endraw %} + +{% raw %}{% block js %}{% endraw %} + +{% raw %}{% endblock js %}{% endraw %} \ No newline at end of file diff --git a/loco-gen/src/templates/scaffold/html/view_edit.t b/loco-gen/src/templates/scaffold/html/view_edit.t index 20145738d..dbd7725fd 100644 --- a/loco-gen/src/templates/scaffold/html/view_edit.t +++ b/loco-gen/src/templates/scaffold/html/view_edit.t @@ -43,8 +43,23 @@ Edit {{name}}: {% raw %}{{ item.id }}{% endraw %} {% elif column.2 == "json!" or column.2 == "jsonb!" -%} + {% elif column.2 == "array!" or column.2 == "array^" -%} +
+ {% raw %}{%{% endraw %} for data in item.{{column.0}} {% raw %}-%}{% endraw %} + + {% raw %}{% endfor -%}{% endraw %} +
+ + {% elif column.2 == "array" -%} +
+ {% raw %}{%{% endraw %} for data in item.{{column.0}} {% raw %}-%}{% endraw %} + + {% raw %}{% endfor -%}{% endraw %} +
+ + {% endif -%} - {% endif -%} + {% endfor -%}
@@ -74,5 +89,19 @@ function confirmDelete(event) { xhr.send(); } } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% raw %}{% endblock js %}{% endraw %} \ No newline at end of file diff --git a/loco-gen/src/templates/scaffold/htmx/view_create.t b/loco-gen/src/templates/scaffold/htmx/view_create.t index 7e638a116..70a95cf93 100644 --- a/loco-gen/src/templates/scaffold/htmx/view_create.t +++ b/loco-gen/src/templates/scaffold/htmx/view_create.t @@ -43,6 +43,16 @@ Create {{file_name}} {% elif column.2 == "json!" or column.2 == "jsonb!" -%} + {% elif column.2 == "array!" or column.2 == "array^" -%} +
+ +
+ + {% elif column.2 == "array" -%} +
+ +
+ {% endif -%}
{% endfor -%} @@ -59,23 +69,50 @@ Create {{file_name}} htmx.defineExtension('submitjson', { onEvent: function (name, evt) { if (name === "htmx:configRequest") { - evt.detail.headers['Content-Type'] = "application/json" + evt.detail.headers['Content-Type'] = "application/json"; } }, encodeParameters: function (xhr, parameters, elt) { const json = {}; + // Handle individual field inputs for (const [key, value] of Object.entries(parameters)) { - const inputType = elt.querySelector(`[name=${key}]`).type; + const inputType = elt.querySelector(`[name="${key}"]`).type; if (inputType === 'number') { json[key] = parseFloat(value); } else if (inputType === 'checkbox') { - json[key] = elt.querySelector(`[name=${key}]`).checked; + json[key] = elt.querySelector(`[name="${key}"]`).checked; } else { json[key] = value; } } + + elt.querySelectorAll('[name]').forEach(input => { + if (input.name.endsWith('[]')) { + const group = input.name.split('[')[0]; // Extract group name + + if (!json[group]) { + json[group] = []; + } + json[group].push(input.value); + } + }); + return JSON.stringify(json); } - }) + }); + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% raw %}{% endblock js %}{% endraw %} \ No newline at end of file diff --git a/loco-gen/src/templates/scaffold/htmx/view_edit.t b/loco-gen/src/templates/scaffold/htmx/view_edit.t index aa202ee3d..58f97ef35 100644 --- a/loco-gen/src/templates/scaffold/htmx/view_edit.t +++ b/loco-gen/src/templates/scaffold/htmx/view_edit.t @@ -43,6 +43,22 @@ Edit {{name}}: {% raw %}{{ item.id }}{% endraw %} {% elif column.2 == "json!" or column.2 == "jsonb!" -%} + {% elif column.2 == "array!" or column.2 == "array^" -%} +
+ {% raw %}{%{% endraw %} for data in item.{{column.0}} {% raw %}-%}{% endraw %} + + {% raw %}{% endfor -%}{% endraw %} +
+ + {% elif column.2 == "array" -%} +
+ {% raw %}{%{% endraw %} for data in item.{{column.0}} {% raw %}-%}{% endraw %} + + {% raw %}{% endfor -%}{% endraw %} +
+ {% endif -%} {% endfor -%} @@ -71,16 +87,30 @@ Edit {{name}}: {% raw %}{{ item.id }}{% endraw %} }, encodeParameters: function (xhr, parameters, elt) { const json = {}; + // Handle individual field inputs for (const [key, value] of Object.entries(parameters)) { - const inputType = elt.querySelector(`[name=${key}]`).type; + const inputType = elt.querySelector(`[name="${key}"]`).type; if (inputType === 'number') { json[key] = parseFloat(value); } else if (inputType === 'checkbox') { - json[key] = elt.querySelector(`[name=${key}]`).checked; + json[key] = elt.querySelector(`[name="${key}"]`).checked; } else { json[key] = value; } } + + // Handle array inputs dynamically based on the name + elt.querySelectorAll('[name]').forEach(input => { + if (input.name.endsWith('[]')) { + const group = input.name.split('[')[0]; // Extract group name + + if (!json[group]) { + json[group] = []; + } + json[group].push(input.value); + } + }); + return JSON.stringify(json); } }) @@ -97,5 +127,19 @@ Edit {{name}}: {% raw %}{{ item.id }}{% endraw %} xhr.send(); } } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% raw %}{% endblock js %}{% endraw %} \ No newline at end of file From a21450ee977c04ab8afeb1803858da33fd834842 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 19:28:14 +0200 Subject: [PATCH 06/11] clippy fixes --- loco-gen/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index bb3f3fe44..3842df3a9 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -85,9 +85,9 @@ struct Mappings { } impl Mappings { fn error_unrecognized_default_field(&self, field: &str) -> Error { - Self::error_unrecognized(field, self.schema_fields()) + Self::error_unrecognized(field, &self.schema_fields()) } - fn error_unrecognized(field: &str, allow_fields: Vec<&String>) -> Error { + fn error_unrecognized(field: &str, allow_fields: &[&String]) -> Error { Error::Message(format!( "type: `{}` not found. try any of: `{}`", field, @@ -103,9 +103,10 @@ impl Mappings { "array" | "array^" | "array!" => { if let RustType::Map(ref map) = self.rust_field_kind(field)? { if let [single] = params.as_slice() { + let keys: Vec<&String> = map.keys().collect(); Ok(map .get(single) - .ok_or_else(|| Self::error_unrecognized(field, map.keys().collect()))?) + .ok_or_else(|| Self::error_unrecognized(field, &keys))?) } else { Err(self.error_unrecognized_default_field(field)) } From 00441725e3bd24d512e6849c232ee3dd6f0970d9 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 19:49:29 +0200 Subject: [PATCH 07/11] test --- loco-gen/src/model.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index 4c813226a..5b18b9a66 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -231,10 +231,7 @@ mod tests { let mappings = get_mappings(); assert_eq!( err.to_string(), - format!( - "type: `` not found. try any of: {:?}", - mappings.schema_fields() - ) + mappings.error_unrecognized_default_field("").to_string() ); } else { panic!("Expected Err, but got Ok: {res:?}"); From 58bec24c97c9b0c3ab2b1d54647ad8b5707f90dd Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 20:12:32 +0200 Subject: [PATCH 08/11] test --- loco-gen/src/lib.rs | 60 +++++---------------------------------------- 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index 3842df3a9..2a6a907ed 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -581,15 +581,7 @@ mod tests { 0 ); - assert_eq!( - mapping - .col_type_arity("unknown") - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.col_type_arity("unknown").is_err()); } #[test] @@ -601,15 +593,7 @@ mod tests { "array_null" ); - assert_eq!( - mapping - .col_type_field("unknown") - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.col_type_field("unknown").is_err()); } #[test] @@ -621,15 +605,7 @@ mod tests { "string_uniq" ); - assert_eq!( - mapping - .schema_field("unknown") - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.schema_field("unknown").is_err()); } #[test] @@ -651,15 +627,7 @@ mod tests { "type `array` need params to get the rust field type".to_string() ); - assert_eq!( - mapping - .rust_field("unknown") - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.rust_field("unknown").is_err(),); } #[test] @@ -668,15 +636,7 @@ mod tests { assert!(mapping.rust_field_kind("string^").is_ok()); - assert_eq!( - mapping - .rust_field_kind("unknown") - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.rust_field_kind("unknown").is_err(),); } #[test] @@ -704,14 +664,6 @@ mod tests { "type: `array` not found. try any of: `int,string,chat`" ); - assert_eq!( - mapping - .rust_field_with_params("unknown", &vec![]) - .expect_err("expect error") - .to_string(), - mapping - .error_unrecognized_default_field("unknown") - .to_string() - ); + assert!(mapping.rust_field_with_params("unknown", &vec![]).is_err()); } } From 201b17b52ce71480601db5a728178668bd9b95d2 Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Mon, 13 Jan 2025 20:20:31 +0200 Subject: [PATCH 09/11] test --- loco-gen/src/lib.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index 2a6a907ed..cf7f9fac0 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -619,13 +619,7 @@ mod tests { "String" ); - assert_eq!( - mapping - .rust_field("array") - .expect_err("expect error") - .to_string(), - "type `array` need params to get the rust field type".to_string() - ); + assert!(mapping.rust_field("array").is_err()); assert!(mapping.rust_field("unknown").is_err(),); } @@ -656,13 +650,9 @@ mod tests { .expect("Get string^ rust field"), "Vec" ); - assert_eq!( - mapping - .rust_field_with_params("array", &vec!["unknown".to_string()]) - .expect_err("expect error") - .to_string(), - "type: `array` not found. try any of: `int,string,chat`" - ); + assert!(mapping + .rust_field_with_params("array", &vec!["unknown".to_string()]) + .is_err()); assert!(mapping.rust_field_with_params("unknown", &vec![]).is_err()); } From fe89071ea6851bc24a8edbc53f9d270b9821c11c Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Tue, 14 Jan 2025 08:42:35 +0200 Subject: [PATCH 10/11] test --- .../src/templates/scaffold/htmx/view_create.t | 5 ++- ...enerate[views_[create]]@Html_scaffold.snap | 18 +++++++++++ ...enerate[views_[create]]@Htmx_scaffold.snap | 30 +++++++++++++++-- .../generate[views_[edit]]@Html_scaffold.snap | 16 ++++++++++ .../generate[views_[edit]]@Htmx_scaffold.snap | 32 +++++++++++++++++-- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/loco-gen/src/templates/scaffold/htmx/view_create.t b/loco-gen/src/templates/scaffold/htmx/view_create.t index 70a95cf93..3fbdb1978 100644 --- a/loco-gen/src/templates/scaffold/htmx/view_create.t +++ b/loco-gen/src/templates/scaffold/htmx/view_create.t @@ -74,13 +74,12 @@ Create {{file_name}} }, encodeParameters: function (xhr, parameters, elt) { const json = {}; - // Handle individual field inputs for (const [key, value] of Object.entries(parameters)) { - const inputType = elt.querySelector(`[name="${key}"]`).type; + const inputType = elt.querySelector(`[name=${key}]`).type; if (inputType === 'number') { json[key] = parseFloat(value); } else if (inputType === 'checkbox') { - json[key] = elt.querySelector(`[name="${key}"]`).checked; + json[key] = elt.querySelector(`[name=${key}]`).checked; } else { json[key] = value; } diff --git a/loco-gen/tests/templates/snapshots/generate[views_[create]]@Html_scaffold.snap b/loco-gen/tests/templates/snapshots/generate[views_[create]]@Html_scaffold.snap index 2a9fd91b7..cb61380dd 100644 --- a/loco-gen/tests/templates/snapshots/generate[views_[create]]@Html_scaffold.snap +++ b/loco-gen/tests/templates/snapshots/generate[views_[create]]@Html_scaffold.snap @@ -27,3 +27,21 @@ Create movie Back to movies {% endblock content %} + +{% block js %} + +{% endblock js %} diff --git a/loco-gen/tests/templates/snapshots/generate[views_[create]]@Htmx_scaffold.snap b/loco-gen/tests/templates/snapshots/generate[views_[create]]@Htmx_scaffold.snap index 162098eac..384b96012 100644 --- a/loco-gen/tests/templates/snapshots/generate[views_[create]]@Htmx_scaffold.snap +++ b/loco-gen/tests/templates/snapshots/generate[views_[create]]@Htmx_scaffold.snap @@ -31,7 +31,7 @@ Create movie htmx.defineExtension('submitjson', { onEvent: function (name, evt) { if (name === "htmx:configRequest") { - evt.detail.headers['Content-Type'] = "application/json" + evt.detail.headers['Content-Type'] = "application/json"; } }, encodeParameters: function (xhr, parameters, elt) { @@ -46,8 +46,34 @@ Create movie json[key] = value; } } + + elt.querySelectorAll('[name]').forEach(input => { + if (input.name.endsWith('[]')) { + const group = input.name.split('[')[0]; // Extract group name + + if (!json[group]) { + json[group] = []; + } + json[group].push(input.value); + } + }); + return JSON.stringify(json); } - }) + }); + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% endblock js %} diff --git a/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Html_scaffold.snap b/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Html_scaffold.snap index 404bad6af..86f992247 100644 --- a/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Html_scaffold.snap +++ b/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Html_scaffold.snap @@ -18,6 +18,8 @@ Edit movie: {{ item.id }}
+ +
@@ -45,5 +47,19 @@ function confirmDelete(event) { xhr.send(); } } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% endblock js %} diff --git a/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Htmx_scaffold.snap b/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Htmx_scaffold.snap index 2c097aa50..9a65856ff 100644 --- a/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Htmx_scaffold.snap +++ b/loco-gen/tests/templates/snapshots/generate[views_[edit]]@Htmx_scaffold.snap @@ -43,16 +43,30 @@ Edit movie: {{ item.id }} }, encodeParameters: function (xhr, parameters, elt) { const json = {}; + // Handle individual field inputs for (const [key, value] of Object.entries(parameters)) { - const inputType = elt.querySelector(`[name=${key}]`).type; + const inputType = elt.querySelector(`[name="${key}"]`).type; if (inputType === 'number') { json[key] = parseFloat(value); } else if (inputType === 'checkbox') { - json[key] = elt.querySelector(`[name=${key}]`).checked; + json[key] = elt.querySelector(`[name="${key}"]`).checked; } else { json[key] = value; } } + + // Handle array inputs dynamically based on the name + elt.querySelectorAll('[name]').forEach(input => { + if (input.name.endsWith('[]')) { + const group = input.name.split('[')[0]; // Extract group name + + if (!json[group]) { + json[group] = []; + } + json[group].push(input.value); + } + }); + return JSON.stringify(json); } }) @@ -69,5 +83,19 @@ Edit movie: {{ item.id }} xhr.send(); } } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.add-more').forEach(button => { + button.addEventListener('click', function () { + const group = this.getAttribute('data-group'); + const container = document.getElementById(`${group}-inputs`); + const newInput = document.createElement('input'); + newInput.type = 'text'; + newInput.name = `${group}[]`; + newInput.placeholder = `Enter another ${group} value`; + container.appendChild(newInput); + }); + }); + }); {% endblock js %} From 9dad0975506bcce35b4f46d6128ae10a12c970ac Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Sun, 19 Jan 2025 09:19:36 +0200 Subject: [PATCH 11/11] remove char and text --- loco-gen/src/mappings.json | 6 ------ src/schema.rs | 4 ---- 2 files changed, 10 deletions(-) diff --git a/loco-gen/src/mappings.json b/loco-gen/src/mappings.json index 97888ad83..f1bd05d53 100644 --- a/loco-gen/src/mappings.json +++ b/loco-gen/src/mappings.json @@ -435,8 +435,6 @@ "name": "array!", "rust": { "string": "Vec", - "char": "Vec", - "text": "Vec", "float": "Vec", "int": "Vec", "small_int": "Vec", @@ -452,8 +450,6 @@ "name": "array", "rust": { "string": "Option>", - "char": "Option>", - "text": "Option>", "float": "Option>", "int": "Option>", "small_int": "Option>", @@ -469,8 +465,6 @@ "name": "array^", "rust": { "string": "Vec", - "char": "Vec", - "text": "Vec", "float": "Vec", "int": "Vec", "small_int": "Vec", diff --git a/src/schema.rs b/src/schema.rs index cf572ffc4..26d8a8d5b 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -166,8 +166,6 @@ pub enum ColType { pub enum ArrayColType { String, - Text, - Char, Float, Int, SmallInt, @@ -198,8 +196,6 @@ impl ColType { 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::SmallInt => ColumnType::SmallInteger,