239 lines
9.0 KiB
Rust
239 lines
9.0 KiB
Rust
use std::{collections::HashMap, env::current_dir, path::Path};
|
|
|
|
use chrono::Utc;
|
|
use duct::cmd;
|
|
use rrgen::RRgen;
|
|
use serde_json::json;
|
|
|
|
use crate::{
|
|
column::{self, ColumnKind},
|
|
render_template, AppInfo, Error, GenerateResults, Result,
|
|
};
|
|
|
|
/// skipping some fields from the generated models.
|
|
/// For example, the `created_at` and `updated_at` fields are automatically
|
|
/// generated by the Loco app and should be given
|
|
pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"];
|
|
|
|
/// columns are <name>, <dbtype>: ("content", "string")
|
|
/// references are <to table, id col in from table>: ("user", `user_id`)
|
|
/// parsed from e.g.: model article content:string user:references
|
|
/// puts a `user_id` in articles, then fk to users
|
|
///
|
|
/// Parses every field through [`column::parse_column`] (via
|
|
/// [`column::columns_from_fields`]), the single source of truth for column
|
|
/// type information, then partitions the result: references never appear in
|
|
/// the returned `columns` list -- they are always emitted as a `BigInteger`
|
|
/// (i64) foreign key by the `create_table`/schema helper's own fk path, which
|
|
/// reads the `references` tuples returned here.
|
|
#[allow(clippy::type_complexity)]
|
|
pub fn get_columns_and_references(
|
|
fields: &[(String, String)],
|
|
) -> Result<(Vec<(String, String)>, Vec<(String, String)>)> {
|
|
let mut columns = Vec::new();
|
|
let mut references = Vec::new();
|
|
for col in column::columns_from_fields(fields)? {
|
|
match &col.kind {
|
|
ColumnKind::Reference { target, fk_field } => {
|
|
// A trailing `?` on the reference name is how the
|
|
// `create_table` schema helper is told the FK is nullable;
|
|
// see `column::parse_column`'s doc comment for the DSL.
|
|
let ref_name = if col.nullable {
|
|
format!("{target}?")
|
|
} else {
|
|
target.clone()
|
|
};
|
|
references.push((ref_name, fk_field.clone().unwrap_or_default()));
|
|
}
|
|
_ => {
|
|
columns.push((col.name.clone(), col.col_type()));
|
|
}
|
|
}
|
|
}
|
|
Ok((columns, references))
|
|
}
|
|
|
|
pub fn generate(
|
|
rrgen: &RRgen,
|
|
name: &str,
|
|
with_tz: bool,
|
|
fields: &[(String, String)],
|
|
appinfo: &AppInfo,
|
|
) -> Result<GenerateResults> {
|
|
let pkg_name: &str = &appinfo.app_name;
|
|
let ts = Utc::now();
|
|
|
|
let (columns, references) = get_columns_and_references(fields)?;
|
|
|
|
let vars = json!({"name": name, "ts": ts, "with_tz": with_tz,"pkg_name": pkg_name, "columns": columns, "references": references});
|
|
let gen_result = render_template(rrgen, Path::new("model"), &vars)?;
|
|
|
|
if std::env::var("SKIP_MIGRATION").is_err() {
|
|
// generate the model files by migrating and re-running seaorm
|
|
let cwd = current_dir()?;
|
|
let env_map: HashMap<_, _> = std::env::vars().collect();
|
|
|
|
let _ = cmd!("cargo", "loco-tool", "db", "migrate",)
|
|
.stderr_to_stdout()
|
|
.dir(cwd.as_path())
|
|
.full_env(&env_map)
|
|
.run()
|
|
.map_err(|err| {
|
|
Error::Message(format!(
|
|
"failed to run loco db migration. error details: `{err}`",
|
|
))
|
|
})?;
|
|
let _ = cmd!("cargo", "loco-tool", "db", "entities",)
|
|
.stderr_to_stdout()
|
|
.dir(cwd.as_path())
|
|
.full_env(&env_map)
|
|
.run()
|
|
.map_err(|err| {
|
|
Error::Message(format!(
|
|
"failed to run loco db entities. error details: `{err}`",
|
|
))
|
|
})?;
|
|
}
|
|
|
|
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() {
|
|
// Note: the `!`/`^` suffix now trails the *whole* spec (matching
|
|
// every other type's grammar in `column::parse_column`), not the
|
|
// `array` keyword -- i.e. `array:string!`, not the old `array!:string`.
|
|
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 unknown_or_malformed_types_are_rejected() {
|
|
// `string` takes no parameters; `column::parse_column` doesn't
|
|
// recognize the extra `:2` segment and rejects it as an unknown type
|
|
// rather than an arity mismatch (there is no longer a generic
|
|
// per-type arity table -- `parse_column`'s grammar is matched
|
|
// exhaustively per type).
|
|
let fields = vec![to_field("name", "string:2")];
|
|
assert!(get_columns_and_references(&fields).is_err());
|
|
|
|
// an empty spec is not a recognized base type name either.
|
|
let fields = vec![to_field("post", "")];
|
|
assert!(get_columns_and_references(&fields).is_err());
|
|
}
|
|
|
|
// ---- 1.0 canonical-value fixes -----------------------------------------
|
|
|
|
#[test]
|
|
fn test_int_is_64_bit_big_integer() {
|
|
// `int` maps to a 64-bit `BigInteger`. An earlier 1.0 iteration made it
|
|
// a 32-bit `Integer`, but that broke on SQLite (32-bit DTO vs the i64
|
|
// entity sea-orm generates → a non-compiling scaffold), so `int` was
|
|
// settled as i64, equal to `big_int`. Only `small_int` is 16-bit.
|
|
// Documented in the CHANGELOG ("the `int`/`unsigned` field types
|
|
// generate 64-bit columns").
|
|
let fields = [to_field("hits", "int!")];
|
|
let res = get_columns_and_references(&fields).expect("Failed to parse fields");
|
|
assert_eq!(res, (vec![to_field("hits", "BigInteger")], vec![]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_big_int_is_64_bit_big_integer() {
|
|
let fields = [to_field("views", "big_int!")];
|
|
let res = get_columns_and_references(&fields).expect("Failed to parse fields");
|
|
assert_eq!(res, (vec![to_field("views", "BigInteger")], vec![]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reference_is_never_a_column_and_fk_is_64_bit() {
|
|
let fields = [to_field("user", "references")];
|
|
let res = get_columns_and_references(&fields).expect("Failed to parse fields");
|
|
// references never appear in the columns list -- the schema helper's
|
|
// fk path (fed by the `references` tuples) always emits the fk
|
|
// column itself, as a 64-bit `BigInteger`.
|
|
assert_eq!(res, (vec![], vec![to_field("user", "")]));
|
|
assert_eq!(
|
|
crate::column::parse_column("user", "references")
|
|
.expect("failed to parse")
|
|
.col_type(),
|
|
"BigInteger"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decimal_required() {
|
|
let fields = [to_field("price", "decimal!")];
|
|
let res = get_columns_and_references(&fields).expect("Failed to parse fields");
|
|
assert_eq!(res, (vec![to_field("price", "Decimal")], vec![]));
|
|
}
|
|
}
|