Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
use duct::cmd;
use insta::assert_snapshot;
use rstest::rstest;
use serial_test::serial;
use std::{collections::HashMap, env::current_dir, fs::read_to_string};
#[rstest]
#[serial]
fn test_migrations_flow(#[values("postgres", "sqlite")] db_kind: &str) {
if db_kind == "postgres" && std::env::var("DATABASE_URL").is_err() {
return;
}
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.create()
.expect("Should create temp folder");
let loco_dev_path = current_dir().unwrap();
let loco_dev_path = loco_dev_path.parent().unwrap();
// 1. install most recent dev cli: cd loco-new; cargo install --path . --force
// 2. when running locally set LOCO_DEV_MODE_PATH=<to local loco path>
// LOCO_DEV_MODE_PATH=../../ cargo run -- new
let mut env_map: HashMap<String, String> = std::env::vars().collect();
env_map.insert(
"LOCO_DEV_MODE_PATH".into(),
loco_dev_path.to_str().unwrap().to_string(),
);
if db_kind == "sqlite" {
env_map.remove("DATABASE_URL");
}
cmd!(
"loco",
"new",
"-n",
"myapp",
"--db",
db_kind,
"--bg",
"async",
"--assets",
"serverside",
"-a"
)
.full_env(&env_map)
.dir(&tree_fs.root)
.run()
.expect("new");
// build a mega long all-types "title:string ..." pairs for all types from
// `column::scalar_from_base_name`'s recognized base names (the successor to
// the now-removed `mappings.json`); name of column is name of type
// adjusted with unique, or nonnull, etc. arity arguments get manual
// treatment. `bool`/`tstz` have no `^` (unique) variant -- see
// `column::parse_column`'s rejection of `bool^`/`tstz^`.
let base_names = [
"string",
"text",
"uuid",
"bool",
"date",
"time",
"date_time",
"tstz",
"json",
"jsonb",
"blob",
"money",
"decimal",
"float",
"double",
"small_int",
"small_unsigned",
"unsigned",
"int",
"big_int",
];
let mut type_names = Vec::new();
for base in base_names {
type_names.push(format!("{base}:{base}"));
type_names.push(format!("{base}_nonull:{base}!"));
// bool/tstz have no unique column type; `json` has no btree operator
// class on Postgres (use `jsonb`). See `column::parse_column`'s rejects.
if base != "bool" && base != "tstz" && base != "json" {
type_names.push(format!("{base}_uniq:{base}^"));
}
}
// push arity arguments manually
type_names.push("age:decimal_len:8:24".to_string());
type_names.push("age_nonull:decimal_len!:8:24".to_string());
if db_kind == "postgres" {
type_names.push("array_string:array:string".to_string());
type_names.push("array_float:array:float".to_string());
type_names.push("array_int:array:int".to_string());
type_names.push("array_double:array:double".to_string());
type_names.push("array_bool:array:bool".to_string());
}
let types_line = type_names.join(" ");
let script = [
"loco db reset",
"loco g model user name:string",
"loco g model user_without_tz name:string --without-tz",
&format!("loco g scaffold playlists {types_line}"),
&format!("loco g scaffold playlists_without_tz {types_line} --without-tz"),
&format!("loco g model movies {types_line} playlist:references user:references?"),
"loco g migration AddContentToMovies content:string",
"loco g migration CreateActors foobar:string",
// TBD this errors under sqlite because they don`t support alter and uniq
// &format!("loco g migration AddAllToActors {types_line}"),
"loco g migration CreateJoinTableActorsAndMovies minutes:int",
"loco g migration CreateJoinTableUser_without_tzAndMovies minutes:int --without-tz",
"loco g migration CreateAwards name:string actor:references",
"loco g migration RemoveContentFromMovies content:string",
"loco g migration AddRatingToMovies rating:int",
"loco db migrate",
"loco db entities",
"loco db schema",
];
for line in script {
cmd("cargo", line.split(' '))
.full_env(&env_map)
.dir(tree_fs.root.join("myapp"))
.run()
.unwrap_or_else(|_| panic!("command {line} should run successfully"));
}
// cargo loco build
assert_snapshot!(
format!("migrations_flow_{db_kind}"),
read_to_string(tree_fs.root.join("myapp").join("schema_dump.json")).unwrap()
);
}
+1
View File
@@ -0,0 +1 @@
mod templates;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
---
source: loco-gen/tests/db.rs
assertion_line: 133
expression: "read_to_string(tree_fs.root.join(\"myapp\").join(\"schema_dump.json\")).unwrap()"
---
[
{
"sql": "CREATE TABLE \"actor_movies\" ( \"minutes\" integer NULL, \"actor_id\" integer NOT NULL, \"movie_id\" integer NOT NULL, CONSTRAINT \"idx-actor_movies-refs-pk\" PRIMARY KEY (\"actor_id\", \"movie_id\"), FOREIGN KEY (\"actor_id\") REFERENCES \"actors\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\"movie_id\") REFERENCES \"movies\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "actor_movies"
},
{
"sql": "CREATE TABLE \"actors\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"foobar\" varchar NULL )",
"table": "actors"
},
{
"sql": "CREATE TABLE \"awards\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" varchar NULL, \"actor_id\" integer NOT NULL, FOREIGN KEY (\"actor_id\") REFERENCES \"actors\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "awards"
},
{
"sql": "CREATE TABLE \"movies\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"string\" varchar NULL, \"string_nonull\" varchar NOT NULL, \"string_uniq\" varchar NOT NULL UNIQUE, \"text\" text NULL, \"text_nonull\" text NOT NULL, \"text_uniq\" text NOT NULL UNIQUE, \"uuid\" uuid_text NULL, \"uuid_nonull\" uuid_text NOT NULL, \"uuid_uniq\" uuid_text NOT NULL UNIQUE, \"bool\" boolean NULL, \"bool_nonull\" boolean NOT NULL, \"date\" date_text NULL, \"date_nonull\" date_text NOT NULL, \"date_uniq\" date_text NOT NULL UNIQUE, \"time\" time_text NULL, \"time_nonull\" time_text NOT NULL, \"time_uniq\" time_text NOT NULL UNIQUE, \"date_time\" datetime_text NULL, \"date_time_nonull\" datetime_text NOT NULL, \"date_time_uniq\" datetime_text NOT NULL UNIQUE, \"tstz\" timestamp_with_timezone_text NULL, \"tstz_nonull\" timestamp_with_timezone_text NOT NULL, \"json\" json_text NULL, \"json_nonull\" json_text NOT NULL, \"jsonb\" jsonb_text NULL, \"jsonb_nonull\" jsonb_text NOT NULL, \"jsonb_uniq\" jsonb_text NOT NULL UNIQUE, \"blob\" blob NULL, \"blob_nonull\" blob NOT NULL, \"blob_uniq\" blob NOT NULL UNIQUE, \"money\" real_money NULL, \"money_nonull\" real_money NOT NULL, \"money_uniq\" real_money NOT NULL UNIQUE, \"decimal\" real_decimal NULL, \"decimal_nonull\" real_decimal NOT NULL, \"decimal_uniq\" real_decimal NOT NULL UNIQUE, \"float\" float NULL, \"float_nonull\" float NOT NULL, \"float_uniq\" float NOT NULL UNIQUE, \"double\" double NULL, \"double_nonull\" double NOT NULL, \"double_uniq\" double NOT NULL UNIQUE, \"small_int\" smallint NULL, \"small_int_nonull\" smallint NOT NULL, \"small_int_uniq\" smallint NOT NULL UNIQUE, \"small_unsigned\" smallint NULL, \"small_unsigned_nonull\" smallint NOT NULL, \"small_unsigned_uniq\" smallint NOT NULL UNIQUE, \"unsigned\" integer NULL, \"unsigned_nonull\" integer NOT NULL, \"unsigned_uniq\" integer NOT NULL UNIQUE, \"int\" integer NULL, \"int_nonull\" integer NOT NULL, \"int_uniq\" integer NOT NULL UNIQUE, \"big_int\" integer NULL, \"big_int_nonull\" integer NOT NULL, \"big_int_uniq\" integer NOT NULL UNIQUE, \"age\" real(8, 24) NULL, \"age_nonull\" real(8, 24) NOT NULL, \"playlist_id\" integer NOT NULL, \"user_id\" integer NULL, \"rating\" integer NULL, FOREIGN KEY (\"playlist_id\") REFERENCES \"playlists\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION )",
"table": "movies"
},
{
"sql": "CREATE TABLE \"playlists\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"string\" varchar NULL, \"string_nonull\" varchar NOT NULL, \"string_uniq\" varchar NOT NULL UNIQUE, \"text\" text NULL, \"text_nonull\" text NOT NULL, \"text_uniq\" text NOT NULL UNIQUE, \"uuid\" uuid_text NULL, \"uuid_nonull\" uuid_text NOT NULL, \"uuid_uniq\" uuid_text NOT NULL UNIQUE, \"bool\" boolean NULL, \"bool_nonull\" boolean NOT NULL, \"date\" date_text NULL, \"date_nonull\" date_text NOT NULL, \"date_uniq\" date_text NOT NULL UNIQUE, \"time\" time_text NULL, \"time_nonull\" time_text NOT NULL, \"time_uniq\" time_text NOT NULL UNIQUE, \"date_time\" datetime_text NULL, \"date_time_nonull\" datetime_text NOT NULL, \"date_time_uniq\" datetime_text NOT NULL UNIQUE, \"tstz\" timestamp_with_timezone_text NULL, \"tstz_nonull\" timestamp_with_timezone_text NOT NULL, \"json\" json_text NULL, \"json_nonull\" json_text NOT NULL, \"jsonb\" jsonb_text NULL, \"jsonb_nonull\" jsonb_text NOT NULL, \"jsonb_uniq\" jsonb_text NOT NULL UNIQUE, \"blob\" blob NULL, \"blob_nonull\" blob NOT NULL, \"blob_uniq\" blob NOT NULL UNIQUE, \"money\" real_money NULL, \"money_nonull\" real_money NOT NULL, \"money_uniq\" real_money NOT NULL UNIQUE, \"decimal\" real_decimal NULL, \"decimal_nonull\" real_decimal NOT NULL, \"decimal_uniq\" real_decimal NOT NULL UNIQUE, \"float\" float NULL, \"float_nonull\" float NOT NULL, \"float_uniq\" float NOT NULL UNIQUE, \"double\" double NULL, \"double_nonull\" double NOT NULL, \"double_uniq\" double NOT NULL UNIQUE, \"small_int\" smallint NULL, \"small_int_nonull\" smallint NOT NULL, \"small_int_uniq\" smallint NOT NULL UNIQUE, \"small_unsigned\" smallint NULL, \"small_unsigned_nonull\" smallint NOT NULL, \"small_unsigned_uniq\" smallint NOT NULL UNIQUE, \"unsigned\" integer NULL, \"unsigned_nonull\" integer NOT NULL, \"unsigned_uniq\" integer NOT NULL UNIQUE, \"int\" integer NULL, \"int_nonull\" integer NOT NULL, \"int_uniq\" integer NOT NULL UNIQUE, \"big_int\" integer NULL, \"big_int_nonull\" integer NOT NULL, \"big_int_uniq\" integer NOT NULL UNIQUE, \"age\" real(8, 24) NULL, \"age_nonull\" real(8, 24) NOT NULL )",
"table": "playlists"
},
{
"sql": "CREATE TABLE \"playlists_without_tzs\" ( \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"string\" varchar NULL, \"string_nonull\" varchar NOT NULL, \"string_uniq\" varchar NOT NULL UNIQUE, \"text\" text NULL, \"text_nonull\" text NOT NULL, \"text_uniq\" text NOT NULL UNIQUE, \"uuid\" uuid_text NULL, \"uuid_nonull\" uuid_text NOT NULL, \"uuid_uniq\" uuid_text NOT NULL UNIQUE, \"bool\" boolean NULL, \"bool_nonull\" boolean NOT NULL, \"date\" date_text NULL, \"date_nonull\" date_text NOT NULL, \"date_uniq\" date_text NOT NULL UNIQUE, \"time\" time_text NULL, \"time_nonull\" time_text NOT NULL, \"time_uniq\" time_text NOT NULL UNIQUE, \"date_time\" datetime_text NULL, \"date_time_nonull\" datetime_text NOT NULL, \"date_time_uniq\" datetime_text NOT NULL UNIQUE, \"tstz\" timestamp_with_timezone_text NULL, \"tstz_nonull\" timestamp_with_timezone_text NOT NULL, \"json\" json_text NULL, \"json_nonull\" json_text NOT NULL, \"jsonb\" jsonb_text NULL, \"jsonb_nonull\" jsonb_text NOT NULL, \"jsonb_uniq\" jsonb_text NOT NULL UNIQUE, \"blob\" blob NULL, \"blob_nonull\" blob NOT NULL, \"blob_uniq\" blob NOT NULL UNIQUE, \"money\" real_money NULL, \"money_nonull\" real_money NOT NULL, \"money_uniq\" real_money NOT NULL UNIQUE, \"decimal\" real_decimal NULL, \"decimal_nonull\" real_decimal NOT NULL, \"decimal_uniq\" real_decimal NOT NULL UNIQUE, \"float\" float NULL, \"float_nonull\" float NOT NULL, \"float_uniq\" float NOT NULL UNIQUE, \"double\" double NULL, \"double_nonull\" double NOT NULL, \"double_uniq\" double NOT NULL UNIQUE, \"small_int\" smallint NULL, \"small_int_nonull\" smallint NOT NULL, \"small_int_uniq\" smallint NOT NULL UNIQUE, \"small_unsigned\" smallint NULL, \"small_unsigned_nonull\" smallint NOT NULL, \"small_unsigned_uniq\" smallint NOT NULL UNIQUE, \"unsigned\" integer NULL, \"unsigned_nonull\" integer NOT NULL, \"unsigned_uniq\" integer NOT NULL UNIQUE, \"int\" integer NULL, \"int_nonull\" integer NOT NULL, \"int_uniq\" integer NOT NULL UNIQUE, \"big_int\" integer NULL, \"big_int_nonull\" integer NOT NULL, \"big_int_uniq\" integer NOT NULL UNIQUE, \"age\" real(8, 24) NULL, \"age_nonull\" real(8, 24) NOT NULL )",
"table": "playlists_without_tzs"
},
{
"sql": "CREATE TABLE \"seaql_migrations\" ( \"version\" varchar NOT NULL PRIMARY KEY, \"applied_at\" integer NOT NULL )",
"table": "seaql_migrations"
},
{
"sql": "CREATE TABLE \"user_without_tz_movies\" ( \"minutes\" integer NULL, \"user_without_tz_id\" integer NOT NULL, \"movie_id\" integer NOT NULL, CONSTRAINT \"idx-user_without_tz_movies-refs-pk\" PRIMARY KEY (\"user_without_tz_id\", \"movie_id\"), FOREIGN KEY (\"user_without_tz_id\") REFERENCES \"user_without_tzs\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\"movie_id\") REFERENCES \"movies\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "user_without_tz_movies"
},
{
"sql": "CREATE TABLE \"user_without_tzs\" ( \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" varchar NULL )",
"table": "user_without_tzs"
},
{
"sql": "CREATE TABLE \"users\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"pid\" uuid_text NOT NULL, \"email\" varchar NOT NULL UNIQUE, \"password\" varchar NOT NULL, \"api_key\" varchar NOT NULL UNIQUE, \"name\" varchar NOT NULL, \"reset_token\" varchar NULL, \"reset_sent_at\" timestamp_with_timezone_text NULL, \"email_verification_token\" varchar NULL, \"email_verification_sent_at\" timestamp_with_timezone_text NULL, \"email_verified_at\" timestamp_with_timezone_text NULL, \"magic_link_token\" varchar NULL, \"magic_link_expiration\" timestamp_with_timezone_text NULL )",
"table": "users"
}
]
@@ -0,0 +1,836 @@
---
source: loco-gen/tests/db.rs
expression: "read_to_string(tree_fs.root.join(\"myapp\").join(\"schema_dump.json\")).unwrap()"
---
[
{
"column": "created_at",
"table": "actor_movies",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "actor_movies",
"type": "timestamp with time zone"
},
{
"column": "actor_id",
"table": "actor_movies",
"type": "integer"
},
{
"column": "movie_id",
"table": "actor_movies",
"type": "integer"
},
{
"column": "created_at",
"table": "actors",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "actors",
"type": "timestamp with time zone"
},
{
"column": "id",
"table": "actors",
"type": "integer"
},
{
"column": "foobar",
"table": "actors",
"type": "character varying"
},
{
"column": "created_at",
"table": "awards",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "awards",
"type": "timestamp with time zone"
},
{
"column": "id",
"table": "awards",
"type": "integer"
},
{
"column": "name",
"table": "awards",
"type": "character varying"
},
{
"column": "actor_id",
"table": "awards",
"type": "integer"
},
{
"column": "created_at",
"table": "movies",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "movies",
"type": "timestamp with time zone"
},
{
"column": "id",
"table": "movies",
"type": "integer"
},
{
"column": "uuid_uniq",
"table": "movies",
"type": "uuid"
},
{
"column": "uuid",
"table": "movies",
"type": "uuid"
},
{
"column": "uuid_nonull",
"table": "movies",
"type": "uuid"
},
{
"column": "string",
"table": "movies",
"type": "character varying"
},
{
"column": "string_nonull",
"table": "movies",
"type": "character varying"
},
{
"column": "string_uniq",
"table": "movies",
"type": "character varying"
},
{
"column": "text",
"table": "movies",
"type": "text"
},
{
"column": "text_nonull",
"table": "movies",
"type": "text"
},
{
"column": "text_uniq",
"table": "movies",
"type": "text"
},
{
"column": "small_unsigned",
"table": "movies",
"type": "smallint"
},
{
"column": "small_unsigned_nonull",
"table": "movies",
"type": "smallint"
},
{
"column": "small_unsigned_uniq",
"table": "movies",
"type": "smallint"
},
{
"column": "big_unsigned",
"table": "movies",
"type": "bigint"
},
{
"column": "big_unsigned_nonull",
"table": "movies",
"type": "bigint"
},
{
"column": "big_unsigned_uniq",
"table": "movies",
"type": "bigint"
},
{
"column": "small_int",
"table": "movies",
"type": "smallint"
},
{
"column": "small_int_nonull",
"table": "movies",
"type": "smallint"
},
{
"column": "small_int_uniq",
"table": "movies",
"type": "smallint"
},
{
"column": "int",
"table": "movies",
"type": "integer"
},
{
"column": "int_nonull",
"table": "movies",
"type": "integer"
},
{
"column": "int_uniq",
"table": "movies",
"type": "integer"
},
{
"column": "big_int",
"table": "movies",
"type": "bigint"
},
{
"column": "big_int_nonull",
"table": "movies",
"type": "bigint"
},
{
"column": "big_int_uniq",
"table": "movies",
"type": "bigint"
},
{
"column": "float",
"table": "movies",
"type": "real"
},
{
"column": "float_nonull",
"table": "movies",
"type": "real"
},
{
"column": "float_uniq",
"table": "movies",
"type": "real"
},
{
"column": "double",
"table": "movies",
"type": "double precision"
},
{
"column": "double_nonull",
"table": "movies",
"type": "double precision"
},
{
"column": "double_uniq",
"table": "movies",
"type": "double precision"
},
{
"column": "decimal",
"table": "movies",
"type": "numeric"
},
{
"column": "decimal_nonull",
"table": "movies",
"type": "numeric"
},
{
"column": "decimal_uniq",
"table": "movies",
"type": "numeric"
},
{
"column": "bool",
"table": "movies",
"type": "boolean"
},
{
"column": "bool_nonull",
"table": "movies",
"type": "boolean"
},
{
"column": "tstz",
"table": "movies",
"type": "timestamp with time zone"
},
{
"column": "tstz_nonull",
"table": "movies",
"type": "timestamp with time zone"
},
{
"column": "date",
"table": "movies",
"type": "date"
},
{
"column": "date_nonull",
"table": "movies",
"type": "date"
},
{
"column": "date_uniq",
"table": "movies",
"type": "date"
},
{
"column": "date_time",
"table": "movies",
"type": "timestamp without time zone"
},
{
"column": "date_time_nonull",
"table": "movies",
"type": "timestamp without time zone"
},
{
"column": "date_time_uniq",
"table": "movies",
"type": "timestamp without time zone"
},
{
"column": "json",
"table": "movies",
"type": "json"
},
{
"column": "json_nonull",
"table": "movies",
"type": "json"
},
{
"column": "jsonb",
"table": "movies",
"type": "jsonb"
},
{
"column": "jsonb_nonull",
"table": "movies",
"type": "jsonb"
},
{
"column": "jsonb_uniq",
"table": "movies",
"type": "jsonb"
},
{
"column": "blob",
"table": "movies",
"type": "bytea"
},
{
"column": "blob_nonull",
"table": "movies",
"type": "bytea"
},
{
"column": "blob_uniq",
"table": "movies",
"type": "bytea"
},
{
"column": "money",
"table": "movies",
"type": "money"
},
{
"column": "money_nonull",
"table": "movies",
"type": "money"
},
{
"column": "money_uniq",
"table": "movies",
"type": "money"
},
{
"column": "unsigned_nonull",
"table": "movies",
"type": "integer"
},
{
"column": "unsigned",
"table": "movies",
"type": "integer"
},
{
"column": "unsigned_uniq",
"table": "movies",
"type": "integer"
},
{
"column": "age",
"table": "movies",
"type": "numeric"
},
{
"column": "age_nonull",
"table": "movies",
"type": "numeric"
},
{
"column": "array_string",
"table": "movies",
"type": "ARRAY"
},
{
"column": "array_float",
"table": "movies",
"type": "ARRAY"
},
{
"column": "array_int",
"table": "movies",
"type": "ARRAY"
},
{
"column": "array_double",
"table": "movies",
"type": "ARRAY"
},
{
"column": "array_bool",
"table": "movies",
"type": "ARRAY"
},
{
"column": "playlist_id",
"table": "movies",
"type": "integer"
},
{
"column": "rating",
"table": "movies",
"type": "integer"
},
{
"column": "created_at",
"table": "playlists",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "playlists",
"type": "timestamp with time zone"
},
{
"column": "id",
"table": "playlists",
"type": "integer"
},
{
"column": "uuid_uniq",
"table": "playlists",
"type": "uuid"
},
{
"column": "uuid",
"table": "playlists",
"type": "uuid"
},
{
"column": "uuid_nonull",
"table": "playlists",
"type": "uuid"
},
{
"column": "string",
"table": "playlists",
"type": "character varying"
},
{
"column": "string_nonull",
"table": "playlists",
"type": "character varying"
},
{
"column": "string_uniq",
"table": "playlists",
"type": "character varying"
},
{
"column": "text",
"table": "playlists",
"type": "text"
},
{
"column": "text_nonull",
"table": "playlists",
"type": "text"
},
{
"column": "text_uniq",
"table": "playlists",
"type": "text"
},
{
"column": "small_unsigned",
"table": "playlists",
"type": "smallint"
},
{
"column": "small_unsigned_nonull",
"table": "playlists",
"type": "smallint"
},
{
"column": "small_unsigned_uniq",
"table": "playlists",
"type": "smallint"
},
{
"column": "big_unsigned",
"table": "playlists",
"type": "bigint"
},
{
"column": "big_unsigned_nonull",
"table": "playlists",
"type": "bigint"
},
{
"column": "big_unsigned_uniq",
"table": "playlists",
"type": "bigint"
},
{
"column": "small_int",
"table": "playlists",
"type": "smallint"
},
{
"column": "small_int_nonull",
"table": "playlists",
"type": "smallint"
},
{
"column": "small_int_uniq",
"table": "playlists",
"type": "smallint"
},
{
"column": "int",
"table": "playlists",
"type": "integer"
},
{
"column": "int_nonull",
"table": "playlists",
"type": "integer"
},
{
"column": "int_uniq",
"table": "playlists",
"type": "integer"
},
{
"column": "big_int",
"table": "playlists",
"type": "bigint"
},
{
"column": "big_int_nonull",
"table": "playlists",
"type": "bigint"
},
{
"column": "big_int_uniq",
"table": "playlists",
"type": "bigint"
},
{
"column": "float",
"table": "playlists",
"type": "real"
},
{
"column": "float_nonull",
"table": "playlists",
"type": "real"
},
{
"column": "float_uniq",
"table": "playlists",
"type": "real"
},
{
"column": "double",
"table": "playlists",
"type": "double precision"
},
{
"column": "double_nonull",
"table": "playlists",
"type": "double precision"
},
{
"column": "double_uniq",
"table": "playlists",
"type": "double precision"
},
{
"column": "decimal",
"table": "playlists",
"type": "numeric"
},
{
"column": "decimal_nonull",
"table": "playlists",
"type": "numeric"
},
{
"column": "decimal_uniq",
"table": "playlists",
"type": "numeric"
},
{
"column": "bool",
"table": "playlists",
"type": "boolean"
},
{
"column": "bool_nonull",
"table": "playlists",
"type": "boolean"
},
{
"column": "tstz",
"table": "playlists",
"type": "timestamp with time zone"
},
{
"column": "tstz_nonull",
"table": "playlists",
"type": "timestamp with time zone"
},
{
"column": "date",
"table": "playlists",
"type": "date"
},
{
"column": "date_nonull",
"table": "playlists",
"type": "date"
},
{
"column": "date_uniq",
"table": "playlists",
"type": "date"
},
{
"column": "date_time",
"table": "playlists",
"type": "timestamp without time zone"
},
{
"column": "date_time_nonull",
"table": "playlists",
"type": "timestamp without time zone"
},
{
"column": "date_time_uniq",
"table": "playlists",
"type": "timestamp without time zone"
},
{
"column": "json",
"table": "playlists",
"type": "json"
},
{
"column": "json_nonull",
"table": "playlists",
"type": "json"
},
{
"column": "jsonb",
"table": "playlists",
"type": "jsonb"
},
{
"column": "jsonb_nonull",
"table": "playlists",
"type": "jsonb"
},
{
"column": "jsonb_uniq",
"table": "playlists",
"type": "jsonb"
},
{
"column": "blob",
"table": "playlists",
"type": "bytea"
},
{
"column": "blob_nonull",
"table": "playlists",
"type": "bytea"
},
{
"column": "blob_uniq",
"table": "playlists",
"type": "bytea"
},
{
"column": "money",
"table": "playlists",
"type": "money"
},
{
"column": "money_nonull",
"table": "playlists",
"type": "money"
},
{
"column": "money_uniq",
"table": "playlists",
"type": "money"
},
{
"column": "unsigned_nonull",
"table": "playlists",
"type": "integer"
},
{
"column": "unsigned",
"table": "playlists",
"type": "integer"
},
{
"column": "unsigned_uniq",
"table": "playlists",
"type": "integer"
},
{
"column": "age",
"table": "playlists",
"type": "numeric"
},
{
"column": "age_nonull",
"table": "playlists",
"type": "numeric"
},
{
"column": "array_string",
"table": "playlists",
"type": "ARRAY"
},
{
"column": "array_float",
"table": "playlists",
"type": "ARRAY"
},
{
"column": "array_int",
"table": "playlists",
"type": "ARRAY"
},
{
"column": "array_double",
"table": "playlists",
"type": "ARRAY"
},
{
"column": "array_bool",
"table": "playlists",
"type": "ARRAY"
},
{
"column": "version",
"table": "seaql_migrations",
"type": "character varying"
},
{
"column": "applied_at",
"table": "seaql_migrations",
"type": "bigint"
},
{
"column": "created_at",
"table": "users",
"type": "timestamp with time zone"
},
{
"column": "updated_at",
"table": "users",
"type": "timestamp with time zone"
},
{
"column": "id",
"table": "users",
"type": "integer"
},
{
"column": "pid",
"table": "users",
"type": "uuid"
},
{
"column": "email",
"table": "users",
"type": "character varying"
},
{
"column": "password",
"table": "users",
"type": "character varying"
},
{
"column": "api_key",
"table": "users",
"type": "character varying"
},
{
"column": "name",
"table": "users",
"type": "character varying"
},
{
"column": "reset_token",
"table": "users",
"type": "character varying"
},
{
"column": "reset_sent_at",
"table": "users",
"type": "timestamp with time zone"
},
{
"column": "email_verification_token",
"table": "users",
"type": "character varying"
},
{
"column": "email_verification_sent_at",
"table": "users",
"type": "timestamp with time zone"
},
{
"column": "email_verified_at",
"table": "users",
"type": "timestamp with time zone"
},
{
"column": "magic_link_token",
"table": "users",
"type": "character varying"
},
{
"column": "magic_link_expiration",
"table": "users",
"type": "timestamp with time zone"
}
]
@@ -0,0 +1,34 @@
---
source: loco-gen/tests/db.rs
expression: "read_to_string(tree_fs.root.join(\"myapp\").join(\"schema_dump.json\")).unwrap()"
---
[
{
"sql": "CREATE TABLE \"actor_movies\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"actor_id\" integer NOT NULL, \"movie_id\" integer NOT NULL, CONSTRAINT \"idx-actor_movies-refs-pk\" PRIMARY KEY (\"actor_id\", \"movie_id\"), FOREIGN KEY (\"actor_id\") REFERENCES \"actors\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\"movie_id\") REFERENCES \"movies\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "actor_movies"
},
{
"sql": "CREATE TABLE \"actors\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"foobar\" varchar NULL )",
"table": "actors"
},
{
"sql": "CREATE TABLE \"awards\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" varchar NULL, \"actor_id\" integer NOT NULL, FOREIGN KEY (\"actor_id\") REFERENCES \"actors\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "awards"
},
{
"sql": "CREATE TABLE \"movies\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"uuid_uniq\" uuid_text NOT NULL UNIQUE, \"uuid\" uuid_text NULL, \"uuid_nonull\" uuid_text NOT NULL, \"string\" varchar NULL, \"string_nonull\" varchar NOT NULL, \"string_uniq\" varchar NOT NULL UNIQUE, \"text\" text NULL, \"text_nonull\" text NOT NULL, \"text_uniq\" text NOT NULL UNIQUE, \"small_unsigned\" smallint NULL, \"small_unsigned_nonull\" smallint NOT NULL, \"small_unsigned_uniq\" smallint NOT NULL UNIQUE, \"big_unsigned\" bigint NULL, \"big_unsigned_nonull\" bigint NOT NULL, \"big_unsigned_uniq\" bigint NOT NULL UNIQUE, \"small_int\" smallint NULL, \"small_int_nonull\" smallint NOT NULL, \"small_int_uniq\" smallint NOT NULL UNIQUE, \"int\" integer NULL, \"int_nonull\" integer NOT NULL, \"int_uniq\" integer NOT NULL UNIQUE, \"big_int\" bigint NULL, \"big_int_nonull\" bigint NOT NULL, \"big_int_uniq\" bigint NOT NULL UNIQUE, \"float\" float NULL, \"float_nonull\" float NOT NULL, \"float_uniq\" float NOT NULL UNIQUE, \"double\" double NULL, \"double_nonull\" double NOT NULL, \"double_uniq\" double NOT NULL UNIQUE, \"decimal\" real NULL, \"decimal_nonull\" real NOT NULL, \"decimal_uniq\" real NOT NULL UNIQUE, \"bool\" boolean NULL, \"bool_nonull\" boolean NOT NULL, \"tstz\" timestamp_with_timezone_text, \"tstz_nonull\" timestamp_with_timezone_text NOT NULL, \"date\" date_text NULL, \"date_nonull\" date_text NOT NULL, \"date_uniq\" date_text NOT NULL UNIQUE, \"date_time\" datetime_text NULL, \"date_time_nonull\" datetime_text NOT NULL, \"date_time_uniq\" datetime_text NOT NULL UNIQUE, \"json\" json_text NULL, \"json_nonull\" json_text NOT NULL, \"jsonb\" jsonb_text NULL, \"jsonb_nonull\" jsonb_text NOT NULL, \"jsonb_uniq\" jsonb_text NOT NULL UNIQUE, \"blob\" blob NULL, \"blob_nonull\" blob NOT NULL, \"blob_uniq\" blob NOT NULL UNIQUE, \"money\" real_money NULL, \"money_nonull\" real_money NOT NULL, \"money_uniq\" real_money NOT NULL UNIQUE, \"unsigned_nonull\" integer NOT NULL, \"unsigned\" integer NULL, \"unsigned_uniq\" integer NOT NULL UNIQUE, \"age\" real(8, 24) NULL, \"age_nonull\" real(8, 24) NOT NULL, \"playlist_id\" integer NOT NULL, \"rating\" integer NULL, FOREIGN KEY (\"playlist_id\") REFERENCES \"playlists\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE )",
"table": "movies"
},
{
"sql": "CREATE TABLE \"playlists\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"uuid_uniq\" uuid_text NOT NULL UNIQUE, \"uuid\" uuid_text NULL, \"uuid_nonull\" uuid_text NOT NULL, \"string\" varchar NULL, \"string_nonull\" varchar NOT NULL, \"string_uniq\" varchar NOT NULL UNIQUE, \"text\" text NULL, \"text_nonull\" text NOT NULL, \"text_uniq\" text NOT NULL UNIQUE, \"small_unsigned\" smallint NULL, \"small_unsigned_nonull\" smallint NOT NULL, \"small_unsigned_uniq\" smallint NOT NULL UNIQUE, \"big_unsigned\" bigint NULL, \"big_unsigned_nonull\" bigint NOT NULL, \"big_unsigned_uniq\" bigint NOT NULL UNIQUE, \"small_int\" smallint NULL, \"small_int_nonull\" smallint NOT NULL, \"small_int_uniq\" smallint NOT NULL UNIQUE, \"int\" integer NULL, \"int_nonull\" integer NOT NULL, \"int_uniq\" integer NOT NULL UNIQUE, \"big_int\" bigint NULL, \"big_int_nonull\" bigint NOT NULL, \"big_int_uniq\" bigint NOT NULL UNIQUE, \"float\" float NULL, \"float_nonull\" float NOT NULL, \"float_uniq\" float NOT NULL UNIQUE, \"double\" double NULL, \"double_nonull\" double NOT NULL, \"double_uniq\" double NOT NULL UNIQUE, \"decimal\" real NULL, \"decimal_nonull\" real NOT NULL, \"decimal_uniq\" real NOT NULL UNIQUE, \"bool\" boolean NULL, \"bool_nonull\" boolean NOT NULL, \"tstz\" timestamp_with_timezone_text, \"tstz_nonull\" timestamp_with_timezone_text NOT NULL, \"date\" date_text NULL, \"date_nonull\" date_text NOT NULL, \"date_uniq\" date_text NOT NULL UNIQUE, \"date_time\" datetime_text NULL, \"date_time_nonull\" datetime_text NOT NULL, \"date_time_uniq\" datetime_text NOT NULL UNIQUE, \"json\" json_text NULL, \"json_nonull\" json_text NOT NULL, \"jsonb\" jsonb_text NULL, \"jsonb_nonull\" jsonb_text NOT NULL, \"jsonb_uniq\" jsonb_text NOT NULL UNIQUE, \"blob\" blob NULL, \"blob_nonull\" blob NOT NULL, \"blob_uniq\" blob NOT NULL UNIQUE, \"money\" real_money NULL, \"money_nonull\" real_money NOT NULL, \"money_uniq\" real_money NOT NULL UNIQUE, \"unsigned_nonull\" integer NOT NULL, \"unsigned\" integer NULL, \"unsigned_uniq\" integer NOT NULL UNIQUE, \"age\" real(8, 24) NULL, \"age_nonull\" real(8, 24) NOT NULL )",
"table": "playlists"
},
{
"sql": "CREATE TABLE \"seaql_migrations\" ( \"version\" varchar NOT NULL PRIMARY KEY, \"applied_at\" bigint NOT NULL )",
"table": "seaql_migrations"
},
{
"sql": "CREATE TABLE \"users\" ( \"created_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \"pid\" uuid_text NOT NULL, \"email\" varchar NOT NULL UNIQUE, \"password\" varchar NOT NULL, \"api_key\" varchar NOT NULL UNIQUE, \"name\" varchar NOT NULL, \"reset_token\" varchar NULL, \"reset_sent_at\" timestamp_with_timezone_text NULL, \"email_verification_token\" varchar NULL, \"email_verification_sent_at\" timestamp_with_timezone_text NULL, \"email_verified_at\" timestamp_with_timezone_text NULL, \"magic_link_token\" varchar NULL, \"magic_link_expiration\" timestamp_with_timezone_text NULL )",
"table": "users"
}
]
@@ -0,0 +1,65 @@
use super::utils::APP_ROUTS;
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
#[test]
fn can_generate() {
let actions = vec!["GET".to_string(), "POST".to_string()];
let component = Component::Controller {
name: "movie".to_string(),
actions: actions.clone(),
};
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("Api_controller");
let _guard = settings.bind_to_scope();
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/controllers/mod.rs")
.add_empty("tests/requests/mod.rs")
.add("src/app.rs", APP_ROUTS)
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_snapshot!("generate_results", collect_messages(&gen_result));
let controllers_path = tree_fs.root.join("src").join("controllers");
assert_snapshot!(
"generate[controller_file]",
fs::read_to_string(controllers_path.join("movie.rs")).expect("controller file missing")
);
assert_snapshot!(
"inject[controller_mod_rs]",
fs::read_to_string(controllers_path.join("mod.rs")).expect("mod.rs injection failed")
);
assert_snapshot!(
"inject[app_rs]",
fs::read_to_string(tree_fs.root.join("src").join("app.rs"))
.expect("app.rs injection failed")
);
let test_controllers_path = tree_fs.root.join("tests").join("requests");
assert_snapshot!(
"generate[tests_controller_mod_rs]",
fs::read_to_string(test_controllers_path.join("movie.rs")).expect("test file missing")
);
assert_snapshot!(
"inject[tests_controller_mod_rs]",
fs::read_to_string(test_controllers_path.join("mod.rs")).expect("test mod.rs missing")
);
}
@@ -0,0 +1,104 @@
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component, DeploymentKind};
use rrgen::RRgen;
use std::{fs, path::PathBuf};
#[rstest::rstest]
fn can_generate_docker(
#[values(vec![], vec![std::path::PathBuf::from("404.html"), PathBuf::from("asset")])]
copy_paths: Vec<PathBuf>,
#[values(true, false)] is_client_side_rendering: bool,
) {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("deployment");
let _guard = settings.bind_to_scope();
let component = Component::Deployment {
kind: DeploymentKind::Docker {
copy_paths: copy_paths.clone(),
is_client_side_rendering,
},
};
let tree_fs = tree_fs::TreeBuilder::default().drop(true).create().unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_eq!(
collect_messages(&gen_result),
r"* Dockerfile generated successfully.
* Dockerignore generated successfully.
"
);
insta::with_settings!({
filters => vec![
(r"FROM rust:\d+\.\d+\.\d+-slim", "FROM rust:[version]-slim"),
]
}, {
assert_snapshot!(
format!(
"generate[docker_file_[{}]_[{}]]",
copy_paths.len(),
is_client_side_rendering
),
fs::read_to_string(tree_fs.root.join("Dockerfile")).expect("Dockerfile missing")
);
});
assert_eq!(
fs::read_to_string(tree_fs.root.join(".dockerignore")).expect(".dockerignore missing"),
r"target
Dockerfile
.dockerignore
.git
.gitignore
"
);
}
#[test]
fn can_generate_nginx() {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("deployment");
let _guard = settings.bind_to_scope();
let component = Component::Deployment {
kind: DeploymentKind::Nginx {
host: "localhost".to_string(),
port: 8080,
},
};
let tree_fs = tree_fs::TreeBuilder::default().drop(true).create().unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_eq!(
collect_messages(&gen_result),
r"* Nginx generated successfully.
"
);
assert_snapshot!(
"generate[nginx]",
fs::read_to_string(tree_fs.root.join("nginx").join("default.conf"))
.expect("nginx config missing")
);
}
+87
View File
@@ -0,0 +1,87 @@
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
#[test]
fn can_generate() {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("mailer");
let _guard = settings.bind_to_scope();
let component = Component::Mailer {
name: "reset_password".to_string(),
};
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/mailers/mod.rs")
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_eq!(
collect_messages(&gen_result),
r"* A mailer `ResetPassword` was added successfully.
"
);
let mailer_path = tree_fs.root.join("src").join("mailers");
for (name, path) in [
(
"generate[mailer_mod_rs]",
mailer_path.join("reset_password.rs"),
),
("inject[mailer_mod_rs]", mailer_path.join("mod.rs")),
(
"generate[subject_t_file]",
mailer_path
.join("reset_password")
.join("welcome")
.join("subject.t"),
),
(
"generate[text_t_file]",
mailer_path
.join("reset_password")
.join("welcome")
.join("text.t"),
),
(
"generate[html_t_file]",
mailer_path
.join("reset_password")
.join("welcome")
.join("html.t"),
),
(
"generate[shared_base_t_file]",
mailer_path.join("shared").join("base.t"),
),
(
"generate[shared_subject_t_file]",
mailer_path.join("shared").join("subject.t"),
),
(
"generate[shared_text_t_file]",
mailer_path.join("shared").join("text.t"),
),
] {
assert_snapshot!(
name,
fs::read_to_string(path).unwrap_or_else(|_| panic!("{name} missing"))
);
}
}
@@ -0,0 +1,191 @@
use super::utils::{guess_file_by_time, MIGRATION_SRC_LIB};
use insta::{assert_snapshot, with_settings};
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use rstest::rstest;
use std::fs;
#[rstest]
#[case("create_table", Component::Migration {
name: "CreateMovies".to_string(),
with_tz: true,
fields: vec![
("title".to_string(), "string".to_string()),
("user".to_string(), "references".to_string()),
],
}, "movies.rs")]
#[case("create_table_without_tz", Component::Migration {
name: "CreateMovies".to_string(),
with_tz: false,
fields: vec![
("title".to_string(), "string".to_string()),
("user".to_string(), "references".to_string()),
],
}, "movies.rs")]
#[case("add_column", Component::Migration {
name: "AddNameAndAgeToUsers".to_string(),
with_tz: true,
fields: vec![
("name".to_string(), "string".to_string()),
("age".to_string(), "int".to_string()),
],
}, "add_name_and_age_to_users.rs")]
#[case("remove_columns", Component::Migration {
name: "RemoveNameAndAgeFromUsers".to_string(),
with_tz: true,
fields: vec![
("name".to_string(), "string".to_string()),
("age".to_string(), "int".to_string()),
],
}, "remove_name_and_age_from_users.rs")]
#[case("add_reference", Component::Migration {
name: "AddUserRefToPosts".to_string(),
with_tz: true,
fields: vec![
("user".to_string(), "references".to_string()),
],
}, "add_user_ref_to_posts.rs")]
#[case("create_join_table_without_tz", Component::Migration {
name: "CreateJoinTableUsersAndGroups".to_string(),
with_tz: false,
fields: vec![
("count".to_string(), "int".to_string()),
],
}, "create_join_table_users_and_groups.rs")]
#[case("create_join_table", Component::Migration {
name: "CreateJoinTableUsersAndGroups".to_string(),
with_tz: true,
fields: vec![
("count".to_string(), "int".to_string()),
],
}, "create_join_table_users_and_groups.rs")]
#[case("empty", Component::Migration {
name: "FixUsersTable".to_string(),
with_tz: true,
fields: vec![
("count".to_string(), "int".to_string()),
],
}, "fix_users_table.rs")]
#[test]
fn can_generate(
#[case] test_name: &str,
#[case] component: Component,
#[case] suffix_generate_file: &str,
) {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix(format!("{test_name}_migration"));
let _guard = settings.bind_to_scope();
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_snapshot!("generate_result", collect_messages(&gen_result));
let migration_path = tree_fs.root.join("migration").join("src");
let migration_file = guess_file_by_time(
&migration_path,
&format!("m{{TIME}}_{suffix_generate_file}"),
3,
)
.expect("Failed to find the generated migration file");
assert_snapshot!(
"generate[migration_file]",
fs::read_to_string(&migration_file).expect("Failed to read the migration file")
);
with_settings!({
filters => vec![(r"\d{8}_\d{6}", "[TIME]")]
}, {
assert_snapshot!(
"inject[migration_lib]",
fs::read_to_string(migration_path.join("lib.rs")).expect("Failed to read lib.rs")
);
});
}
#[rstest]
#[case(Component::Migration {
name: "CreateMovies".to_string(),
with_tz: true,
fields: vec![
("title".to_string(), "string".to_string()),
("user".to_string(), "references".to_string()),
],
})]
#[case(Component::Migration {
name: "AddNameAndAgeToUsers".to_string(),
with_tz: true,
fields: vec![
("name".to_string(), "string".to_string()),
("age".to_string(), "int".to_string()),
],
})]
#[case(Component::Migration {
name: "RemoveNameAndAgeFromUsers".to_string(),
with_tz: true,
fields: vec![
("name".to_string(), "string".to_string()),
("age".to_string(), "int".to_string()),
],
})]
#[case(Component::Migration {
name: "AddUserRefToPosts".to_string(),
with_tz: true,
fields: vec![
("user".to_string(), "references".to_string()),
],
})]
#[case(Component::Migration {
name: "CreateJoinTableUsersAndGroups".to_string(),
with_tz: true,
fields: vec![
("count".to_string(), "int".to_string()),
],
})]
#[case(Component::Migration {
name: "FixUsersTable".to_string(),
with_tz: true,
fields: vec![
("count".to_string(), "int".to_string()),
],
})]
#[test]
fn fail_when_migration_lib_not_exists(#[case] component: Component) {
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("tests/models/mod.rs")
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let err = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect_err("Expected error when migration lib doesn't exist");
assert_eq!(
err.to_string(),
"cannot inject into migration/src/lib.rs: file does not exist"
);
}
+13
View File
@@ -0,0 +1,13 @@
mod controller;
mod deployment;
mod mailer;
#[cfg(feature = "with-db")]
mod migration;
#[cfg(feature = "with-db")]
mod model;
#[cfg(feature = "with-db")]
mod scaffold;
mod scheduler;
mod task;
mod utils;
mod worker;
+183
View File
@@ -0,0 +1,183 @@
use super::utils::{guess_file_by_time, MIGRATION_SRC_LIB};
use insta::{assert_snapshot, with_settings};
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
macro_rules! configure_insta {
() => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("model");
let _guard = settings.bind_to_scope();
};
}
#[test]
fn can_generate() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
configure_insta!();
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.add_empty("tests/models/mod.rs")
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let component = Component::Model {
name: "movies".to_string(),
with_tz: true,
fields: vec![("title".to_string(), "string".to_string())],
};
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_eq!(
collect_messages(&gen_result),
r"* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
* A test for model `Movies` was added. Run with `cargo test`.
"
);
let migration_path = tree_fs.root.join("migration/src");
let migration_file = guess_file_by_time(&migration_path, "m{TIME}_movies.rs", 3)
.expect("Failed to find the generated migration file");
assert_snapshot!(
"generate[migration_file]",
fs::read_to_string(&migration_file).expect("Failed to read the migration file")
);
with_settings!({
filters => vec![(r"\d{8}_\d{6}", "[TIME]")]
}, {
assert_snapshot!(
"inject[migration_lib]",
fs::read_to_string(migration_path.join("lib.rs")).expect("Failed to read lib.rs")
);
});
let tests_path = tree_fs.root.join("tests/models");
assert_snapshot!(
"generate[test_model]",
fs::read_to_string(tests_path.join("movies.rs")).expect("Failed to read movies.rs")
);
assert_snapshot!(
"inject[test_mod]",
fs::read_to_string(tests_path.join("mod.rs")).expect("Failed to read mod.rs")
);
}
// Regression for #1755: a model generated with no fields must still get its
// `id` primary-key column, otherwise sea-orm produces an entity with no primary
// key that fails to compile ("Entity must have a primary key column").
#[test]
fn generate_without_fields_still_emits_primary_key() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.add_empty("tests/models/mod.rs")
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let component = Component::Model {
name: "posts".to_string(),
with_tz: true,
fields: vec![],
};
generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
let migration_path = tree_fs.root.join("migration/src");
let migration_file = guess_file_by_time(&migration_path, "m{TIME}_posts.rs", 3)
.expect("Failed to find the generated migration file");
let migration = fs::read_to_string(&migration_file).expect("read migration");
assert!(
migration.contains("(\"id\", ColType::PkAuto)"),
"a field-less model must still emit its id primary key, got:\n{migration}"
);
}
#[test]
fn fail_when_migration_lib_not_exists() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("tests/models/mod.rs")
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let component = Component::Model {
name: "movies".to_string(),
with_tz: true,
fields: vec![("title".to_string(), "string".to_string())],
};
let err = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect_err("Expected error when model lib doesn't exist");
assert_eq!(
err.to_string(),
"cannot inject into migration/src/lib.rs: file does not exist"
);
}
#[test]
fn fail_when_test_models_mod_not_exists() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let component = Component::Model {
name: "movies".to_string(),
with_tz: true,
fields: vec![("title".to_string(), "string".to_string())],
};
let err = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect_err("Expected error when migration src doesn't exist");
assert_eq!(
err.to_string(),
"cannot inject into tests/models/mod.rs: file does not exist"
);
}
@@ -0,0 +1,198 @@
use super::utils::{guess_file_by_time, APP_ROUTS, MIGRATION_SRC_LIB, ROUTES_TSX_FIXTURE};
use insta::{assert_snapshot, with_settings};
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
/// Loco 1.0 ships a single scaffold flavor: DTO + controller + React-SPA
/// frontend, built straight from `column::Column`. The field set here is the
/// reference target from `examples/reference_spa`'s `post` resource
/// (`title:string! content:text! status:enum:draft,published,archived!
/// price:decimal! published_at:tstz`), so this snapshot doubles as the
/// golden-output check for the DTO + controller generator.
#[test]
fn can_generate() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("Api_scaffold");
let _guard = settings.bind_to_scope();
let component = Component::Scaffold {
name: "post".to_string(),
with_tz: true,
fields: vec![
("title".to_string(), "string!".to_string()),
("content".to_string(), "text!".to_string()),
(
"status".to_string(),
"enum:draft,published,archived!".to_string(),
),
("price".to_string(), "decimal!".to_string()),
("published_at".to_string(), "tstz".to_string()),
],
frontend: true,
};
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/controllers/mod.rs")
.add_empty("src/dtos/mod.rs")
.add_empty("tests/models/mod.rs")
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.add("src/app.rs", APP_ROUTS)
.add("frontend/src/routes.tsx", ROUTES_TSX_FIXTURE)
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
assert_snapshot!("generate_results", collect_messages(&gen_result));
// MIGRATION
let migration_path = tree_fs.root.join("migration/src");
let migration_file = guess_file_by_time(&migration_path, "m{TIME}_posts.rs", 3)
.expect("Failed to find the generated migration file");
assert_snapshot!(
"generate[migration_file]",
fs::read_to_string(&migration_file).expect("Failed to read the migration file")
);
with_settings!({
filters => vec![(r"\d{8}_\d{6}", "[TIME]")]
}, {
assert_snapshot!(
"inject[migration_lib]",
fs::read_to_string(migration_path.join("lib.rs")).expect("Failed to read lib.rs")
);
});
// MODEL TEST
let tests_path = tree_fs.root.join("tests/models");
assert_snapshot!(
"generate[test_model]",
fs::read_to_string(tests_path.join("posts.rs")).expect("Failed to read posts.rs")
);
assert_snapshot!(
"inject[test_mod]",
fs::read_to_string(tests_path.join("mod.rs")).expect("Failed to read mod.rs")
);
// DTO
let dtos_path = tree_fs.root.join("src").join("dtos");
assert_snapshot!(
"generate[dto_file]",
fs::read_to_string(dtos_path.join("posts.rs")).expect("dto file missing")
);
assert_snapshot!(
"inject[dtos_mod_rs]",
fs::read_to_string(dtos_path.join("mod.rs")).expect("mod.rs injection failed")
);
// CONTROLLER
let controllers_path = tree_fs.root.join("src").join("controllers");
assert_snapshot!(
"generate[controller_file]",
fs::read_to_string(controllers_path.join("posts.rs")).expect("controller file missing")
);
assert_snapshot!(
"inject[controller_mod_rs]",
fs::read_to_string(controllers_path.join("mod.rs")).expect("mod.rs injection failed")
);
assert_snapshot!(
"inject[app_rs]",
fs::read_to_string(tree_fs.root.join("src").join("app.rs"))
.expect("app.rs injection failed")
);
// FRONTEND
let frontend_path = tree_fs.root.join("frontend").join("src");
assert_snapshot!(
"generate[frontend_api]",
fs::read_to_string(frontend_path.join("api").join("posts.ts"))
.expect("frontend api file missing")
);
let pages_path = frontend_path.join("pages").join("posts");
for page in ["List", "New", "Edit", "Show"] {
assert_snapshot!(
format!("generate[frontend_page_{page}]"),
fs::read_to_string(pages_path.join(format!("{page}.tsx")))
.unwrap_or_else(|_| panic!("frontend {page}.tsx file missing"))
);
}
assert_snapshot!(
"inject[routes_tsx]",
fs::read_to_string(frontend_path.join("routes.tsx")).expect("routes.tsx injection failed")
);
// no `tests/requests/<plural>.rs` is generated for the API scaffold: a
// non-compiling generated controller test is worse than none (see
// `api/test.t`'s removal).
assert!(!tree_fs.root.join("tests/requests").exists());
}
/// Adaptive scaffold, headless path: with `frontend: false` (a non-clientside
/// app, no `frontend/`), only the typed backend (DTO + controller) is emitted
/// and NO frontend files/dir are created.
#[test]
fn can_generate_backend_only_without_frontend() {
// SAFETY: test-local env setup; no other thread reads the environment during this test.
unsafe { std::env::set_var("SKIP_MIGRATION", "") };
let component = Component::Scaffold {
name: "post".to_string(),
with_tz: true,
fields: vec![
("title".to_string(), "string!".to_string()),
("content".to_string(), "text!".to_string()),
(
"status".to_string(),
"enum:draft,published,archived!".to_string(),
),
("price".to_string(), "decimal!".to_string()),
("published_at".to_string(), "tstz".to_string()),
],
frontend: false,
};
// note: NO `frontend/src/routes.tsx` fixture -- this is a headless app.
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/controllers/mod.rs")
.add_empty("src/dtos/mod.rs")
.add_empty("tests/models/mod.rs")
.add("migration/src/lib.rs", MIGRATION_SRC_LIB)
.add("src/app.rs", APP_ROUTS)
.create()
.unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Generation failed");
// backend emitted
assert!(tree_fs.root.join("src/dtos/posts.rs").exists());
assert!(tree_fs.root.join("src/controllers/posts.rs").exists());
// frontend NOT emitted -- no orphan files or `frontend/` dir
assert!(!tree_fs.root.join("frontend").exists());
}
@@ -0,0 +1,43 @@
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
macro_rules! configure_insta {
($($expr:expr_2021),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("scheduler");
let _guard = settings.bind_to_scope();
};
}
#[test]
fn can_generate() {
configure_insta!();
let component = Component::Scheduler {};
let tree_fs: tree_fs::Tree = tree_fs::TreeBuilder::default().drop(true).create().unwrap();
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Failed to generated scheduler file");
assert_eq!(
collect_messages(&gen_result),
r"* A Scheduler job configuration was added successfully. Run with `cargo loco scheduler --list`.
"
);
assert_snapshot!(
"generate[controller_file]",
fs::read_to_string(tree_fs.root.join("config").join("scheduler.yaml"))
.expect("Failed to read the scheduler.yaml")
);
}
@@ -0,0 +1,31 @@
---
source: loco-gen/tests/templates/controller.rs
expression: "fs::read_to_string(controllers_path.join(\"movie.rs\")).expect(\"controller file missing\")"
---
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
#[debug_handler]
pub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {
format::empty()
}
#[debug_handler]
pub async fn GET(State(_ctx): State<AppContext>) -> Result<Response> {
format::empty()
}
#[debug_handler]
pub async fn POST(State(_ctx): State<AppContext>) -> Result<Response> {
format::empty()
}
pub fn routes() -> Routes {
Routes::new()
.prefix("api/movies/")
.add("/", get(index))
.add("GET", get(GET))
.add("POST", get(POST))
}
@@ -0,0 +1,144 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(controllers_path.join(\"posts.rs\")).expect(\"controller file missing\")"
---
#![allow(clippy::unused_async)]
use axum::http::StatusCode;
use loco_rs::prelude::*;
use sea_orm::{PaginatorTrait, QueryOrder};
use serde::Deserialize;
use crate::{
dtos::{
common::{ApiError, Page},
posts::{CreatePost, PostDto, UpdatePost},
},
models::_entities::posts::{ActiveModel, Column, Entity},
};
const fn default_page() -> u64 {
1
}
const fn default_per_page() -> u64 {
25
}
#[derive(Debug, Deserialize)]
pub struct ListParams {
#[serde(default = "default_page")]
pub page: u64,
#[serde(default = "default_per_page")]
pub per_page: u64,
}
/// Build a 404 response shaped as the [`ApiError`] envelope.
fn not_found(message: &str) -> Response {
let error = ApiError {
code: "not_found".to_string(),
message: message.to_string(),
details: None,
};
(StatusCode::NOT_FOUND, Json(error)).into_response()
}
#[debug_handler]
async fn list(
_auth: auth::JWT,
State(ctx): State<AppContext>,
Query(params): Query<ListParams>,
) -> Result<Json<Page<PostDto>>> {
let page = params.page.max(1);
let per_page = params.per_page.max(1);
let query = Entity::find().order_by_asc(Column::Id);
let paginator = query.paginate(&ctx.db, per_page);
let total = paginator.num_items_and_pages().await?.number_of_items;
let items = paginator.fetch_page(page - 1).await?;
Ok(Json(Page {
items: items.into_iter().map(PostDto::from).collect(),
total: i64::try_from(total).unwrap_or(i64::MAX),
page: i64::try_from(page).unwrap_or(i64::MAX),
per_page: i64::try_from(per_page).unwrap_or(i64::MAX),
}))
}
#[debug_handler]
async fn get_one(
_auth: auth::JWT,
State(ctx): State<AppContext>,
Path(id): Path<i64>,
) -> Result<Response> {
let Some(model) = Entity::find_by_id(id).one(&ctx.db).await? else {
return Ok(not_found("post not found"));
};
Ok(Json(PostDto::from(model)).into_response())
}
#[debug_handler]
async fn create(
_auth: auth::JWT,
State(ctx): State<AppContext>,
Json(params): Json<CreatePost>,
) -> Result<Response> {
let item = ActiveModel {
title: Set(params.title),
content: Set(params.content),
status: Set(params.status.as_str().to_string()),
price: Set(params.price),
published_at: Set(params.published_at),
..Default::default()
};
let item = item.insert(&ctx.db).await?;
Ok((StatusCode::CREATED, Json(PostDto::from(item))).into_response())
}
#[debug_handler]
async fn update(
_auth: auth::JWT,
State(ctx): State<AppContext>,
Path(id): Path<i64>,
Json(params): Json<UpdatePost>,
) -> Result<Response> {
let Some(model) = Entity::find_by_id(id).one(&ctx.db).await? else {
return Ok(not_found("post not found"));
};
let mut item = model.into_active_model();
item.title = Set(params.title);
item.content = Set(params.content);
item.status = Set(params.status.as_str().to_string());
item.price = Set(params.price);
item.published_at = Set(params.published_at);
let item = item.update(&ctx.db).await?;
Ok(Json(PostDto::from(item)).into_response())
}
#[debug_handler]
async fn remove(
_auth: auth::JWT,
State(ctx): State<AppContext>,
Path(id): Path<i64>,
) -> Result<Response> {
let Some(model) = Entity::find_by_id(id).one(&ctx.db).await? else {
return Ok(not_found("post not found"));
};
model.into_active_model().delete(&ctx.db).await?;
Ok(StatusCode::NO_CONTENT.into_response())
}
pub fn routes() -> Routes {
Routes::new()
.prefix("/api/posts")
.add("/", get(list))
.add("/", post(create))
.add("/{id}", get(get_one))
.add("/{id}", put(update))
.add("/{id}", delete(remove))
}
@@ -0,0 +1,17 @@
---
source: loco-gen/tests/templates/scheduler.rs
expression: "fs::read_to_string(tree_fs.root.join(\"config\").join(\"scheduler.yaml\")).expect(\"Failed to read the scheduler.yaml\")"
---
output: stdout
jobs:
write_content:
shell: true
run: "echo loco >> ./scheduler.txt"
schedule: run every 1 second
# schedule: "* * * * * * *"
output: silent
tags: ['base', 'infra']
# run_task:
# run: "foo"
# schedule: "at 10:00 am"
@@ -0,0 +1,20 @@
---
source: loco-gen/tests/templates/task.rs
expression: "fs::read_to_string(task_path.join(\"cleanup.rs\")).expect(\"Failed to read generated task file: cleanup.rs\")"
---
use loco_rs::prelude::*;
pub struct Cleanup;
#[async_trait]
impl Task for Cleanup {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "cleanup".to_string(),
detail: "Task generator".to_string(),
}
}
async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {
println!("Task Cleanup generated");
Ok(())
}
}
@@ -0,0 +1,57 @@
---
source: loco-gen/tests/templates/worker.rs
expression: "fs::read_to_string(worker_path.join(\"register_email.rs\")).expect(\"Failed to read generated worker file: register_email.rs\")"
snapshot_kind: text
---
use serde::{Deserialize, Serialize};
use loco_rs::prelude::*;
pub struct Worker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct WorkerArgs {
}
#[async_trait]
impl BackgroundWorker<WorkerArgs> for Worker {
/// Creates a new instance of the Worker with the given application context.
///
/// This function is called when registering the worker with the queue system.
///
/// # Parameters
/// * `ctx` - The application context containing shared resources
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
/// Returns the class name of the worker.
///
/// This name is used when enqueueing jobs and identifying the worker in logs.
/// The implementation returns the struct name as a string.
fn class_name() -> String {
"RegisterEmail".to_string()
}
/// Returns tags associated with this worker.
///
/// Tags can be used to filter which workers run during startup.
/// The default implementation returns an empty vector (no tags).
fn tags() -> Vec<String> {
Vec::new()
}
/// Performs the actual work when a job is processed.
///
/// This is the main function that contains the worker's logic.
/// It gets executed when a job is dequeued from the job queue.
///
/// # Returns
/// * `Result<()>` - Ok if the job completed successfully, Err otherwise
async fn perform(&self, _args: WorkerArgs) -> Result<()> {
println!("=================RegisterEmail=======================");
// TODO: Some actual work goes here...
Ok(())
}
}
@@ -0,0 +1,21 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\"Dockerfile\")).expect(\"Dockerfile missing\")"
snapshot_kind: text
---
FROM rust:[version]-slim AS builder
WORKDIR /usr/src/
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /usr/app
COPY --from=builder /usr/src/config config
COPY --from=builder /usr/src/target/release/tester-cli tester-cli
ENTRYPOINT ["/usr/app/tester-cli"]
@@ -0,0 +1,29 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\"Dockerfile\")).expect(\"Dockerfile missing\")"
snapshot_kind: text
---
FROM rust:[version]-slim AS builder
WORKDIR /usr/src/
COPY . .
RUN apt-get update && apt-get install -y curl ca-certificates
# Install Node.js using the latest available version from NodeSource.
# In production, replace "setup_current.x" with a specific version
# to avoid unexpected breaking changes in future releases.
RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \
apt-get install -y nodejs
RUN cd frontend && npm install && npm run build
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /usr/app
COPY --from=builder /usr/src/config config
COPY --from=builder /usr/src/target/release/tester-cli tester-cli
ENTRYPOINT ["/usr/app/tester-cli"]
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\"Dockerfile\")).expect(\"Dockerfile missing\")"
snapshot_kind: text
---
FROM rust:[version]-slim AS builder
WORKDIR /usr/src/
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /usr/app
COPY --from=builder /usr/src/404.html 404.html
COPY --from=builder /usr/src/asset asset
COPY --from=builder /usr/src/config config
COPY --from=builder /usr/src/target/release/tester-cli tester-cli
ENTRYPOINT ["/usr/app/tester-cli"]
@@ -0,0 +1,31 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\"Dockerfile\")).expect(\"Dockerfile missing\")"
snapshot_kind: text
---
FROM rust:[version]-slim AS builder
WORKDIR /usr/src/
COPY . .
RUN apt-get update && apt-get install -y curl ca-certificates
# Install Node.js using the latest available version from NodeSource.
# In production, replace "setup_current.x" with a specific version
# to avoid unexpected breaking changes in future releases.
RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \
apt-get install -y nodejs
RUN cd frontend && npm install && npm run build
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /usr/app
COPY --from=builder /usr/src/404.html 404.html
COPY --from=builder /usr/src/asset asset
COPY --from=builder /usr/src/config config
COPY --from=builder /usr/src/target/release/tester-cli tester-cli
ENTRYPOINT ["/usr/app/tester-cli"]
@@ -0,0 +1,93 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(dtos_path.join(\"posts.rs\")).expect(\"dto file missing\")"
---
use sea_orm::prelude::{DateTimeWithTimeZone, Decimal};
use ts_rs::TS;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/src/bindings/")]
#[serde(rename_all = "snake_case")]
pub enum PostStatus {
Draft,
Published,
Archived,
}
impl From<String> for PostStatus {
fn from(value: String) -> Self {
match value.as_str() {
"published" => Self::Published,
"archived" => Self::Archived,
_ => Self::Draft,
}
}
}
impl PostStatus {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Draft => "draft",
Self::Published => "published",
Self::Archived => "archived",
}
}
}
#[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/src/bindings/")]
pub struct PostDto {
#[ts(type = "number")]
pub id: i64,
pub title: String,
pub content: String,
pub status: PostStatus,
#[ts(type = "string")]
pub price: Decimal,
#[ts(type = "string | null")]
pub published_at: Option<DateTimeWithTimeZone>,
#[ts(type = "string")]
pub created_at: DateTimeWithTimeZone,
#[ts(type = "string")]
pub updated_at: DateTimeWithTimeZone,
}
impl From<crate::models::_entities::posts::Model> for PostDto {
fn from(m: crate::models::_entities::posts::Model) -> Self {
Self {
id: m.id,
title: m.title,
content: m.content,
status: PostStatus::from(m.status),
price: m.price,
published_at: m.published_at,
created_at: m.created_at,
updated_at: m.updated_at,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/src/bindings/")]
pub struct CreatePost {
pub title: String,
pub content: String,
pub status: PostStatus,
#[ts(type = "string")]
pub price: Decimal,
#[ts(type = "string | null")]
pub published_at: Option<DateTimeWithTimeZone>,
}
#[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/src/bindings/")]
pub struct UpdatePost {
pub title: String,
pub content: String,
pub status: PostStatus,
#[ts(type = "string")]
pub price: Decimal,
#[ts(type = "string | null")]
pub published_at: Option<DateTimeWithTimeZone>,
}
@@ -0,0 +1,111 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(frontend_path.join(\"api\").join(\"posts.ts\")).expect(\"frontend api file missing\")"
---
import {
useMutation,
useQuery,
useQueryClient,
type UseMutationResult,
type UseQueryResult,
} from "@tanstack/react-query";
import type { CreatePost } from "../bindings/CreatePost";
import type { Page } from "../bindings/Page";
import type { PostDto } from "../bindings/PostDto";
import type { UpdatePost } from "../bindings/UpdatePost";
import { ApiClientError, del, get, post, put } from "./client";
export interface ListPostsParams {
page?: number;
per_page?: number;
}
export const postKeys = {
all: ["posts"] as const,
list: (params?: ListPostsParams) =>
["posts", "list", params ?? {}] as const,
detail: (id: number) => ["posts", "detail", id] as const,
};
function buildQueryString(params?: ListPostsParams): string {
if (!params) {
return "";
}
const search = new URLSearchParams();
if (params.page !== undefined) {
search.set("page", String(params.page));
}
if (params.per_page !== undefined) {
search.set("per_page", String(params.per_page));
}
const qs = search.toString();
return qs ? `?${qs}` : "";
}
export function useListPosts(
params?: ListPostsParams,
): UseQueryResult<Page<PostDto>, ApiClientError> {
return useQuery({
queryKey: postKeys.list(params),
queryFn: () => get<Page<PostDto>>(`/api/posts${buildQueryString(params)}`),
});
}
export function usePost(id: number): UseQueryResult<PostDto, ApiClientError> {
return useQuery({
queryKey: postKeys.detail(id),
queryFn: () => get<PostDto>(`/api/posts/${id}`),
enabled: Number.isFinite(id) && id > 0,
});
}
export function useCreatePost(): UseMutationResult<
PostDto,
ApiClientError,
CreatePost
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreatePost) => post<PostDto>("/api/posts", data),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: postKeys.all });
},
});
}
export interface UpdatePostVariables {
id: number;
data: UpdatePost;
}
export function useUpdatePost(): UseMutationResult<
PostDto,
ApiClientError,
UpdatePostVariables
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: UpdatePostVariables) =>
put<PostDto>(`/api/posts/${id}`, data),
onSuccess: (_data, variables) => {
void queryClient.invalidateQueries({ queryKey: postKeys.all });
void queryClient.invalidateQueries({
queryKey: postKeys.detail(variables.id),
});
},
});
}
export function useRemovePost(): UseMutationResult<
void,
ApiClientError,
number
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => del(`/api/posts/${id}`),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: postKeys.all });
},
});
}
@@ -0,0 +1,133 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(pages_path.join(format!(\"{page}.tsx\"))).unwrap_or_else(|_|\npanic!(\"frontend {page}.tsx file missing\"))"
---
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { usePost, useUpdatePost } from "../../api/posts";
import type { PostStatus } from "../../bindings/PostStatus";
import type { UpdatePost } from "../../bindings/UpdatePost";
const STATUS_OPTIONS: PostStatus[] = ["draft", "published", "archived"];
export function Edit() {
const navigate = useNavigate();
const { id: idParam } = useParams<{ id: string }>();
const id = Number(idParam);
const isValidId = Number.isFinite(id) && id > 0;
const { data, isLoading, isError, error } = usePost(isValidId ? id : NaN);
const updatePost = useUpdatePost();
const [form, setForm] = useState<UpdatePost | null>(null);
useEffect(() => {
if (data) {
setForm({
title: data.title,
content: data.content,
status: data.status,
price: data.price,
published_at: data.published_at,
});
}
}, [data]);
if (!isValidId) {
return <p role="alert">Invalid post id.</p>;
}
if (isLoading || !form) {
return <p>Loading…</p>;
}
if (isError) {
return <p role="alert">{error.message}</p>;
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!form) {
return;
}
updatePost.mutate(
{ id, data: form },
{
onSuccess: () => {
navigate(`/posts/${id}`);
},
},
);
}
return (
<div>
<h1>Edit Post</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="content">Content</label>
<textarea
id="content"
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="status">Status</label>
<select
id="status"
value={form.status}
onChange={(e) =>
setForm({ ...form, status: e.target.value as PostStatus })
}
>
{STATUS_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<div>
<label htmlFor="price">Price</label>
<input
id="price"
type="text"
value={form.price}
onChange={(e) => setForm({ ...form, price: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="published_at">Published at</label>
<input
id="published_at"
type="datetime-local"
value={form.published_at ?? ""}
onChange={(e) =>
setForm({
...form,
published_at: e.target.value || null,
})
}
/>
</div>
<button type="submit" disabled={updatePost.isPending}>
{updatePost.isPending ? "Saving…" : "Save"}
</button>
{updatePost.error && <p role="alert">{updatePost.error.message}</p>}
</form>
</div>
);
}
@@ -0,0 +1,64 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(pages_path.join(format!(\"{page}.tsx\"))).unwrap_or_else(|_|\npanic!(\"frontend {page}.tsx file missing\"))"
---
import { Link } from "react-router";
import { useListPosts, useRemovePost } from "../../api/posts";
export function List() {
const { data, isLoading, isError, error } = useListPosts();
const removePost = useRemovePost();
if (isLoading) {
return <p>Loading…</p>;
}
if (isError) {
return <p role="alert">{error.message}</p>;
}
return (
<div>
<h1>Posts</h1>
<p>
<Link to="/posts/new">New Post</Link>
</p>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>Status</th>
<th>Price</th>
<th>Published at</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{data?.items.map((post) => (
<tr key={post.id}>
<td>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</td>
<td>{post.content}</td>
<td>{post.status}</td>
<td>{post.price}</td>
<td>{post.published_at}</td>
<td>
<Link to={`/posts/${post.id}/edit`}>Edit</Link>{" "}
<button
type="button"
disabled={removePost.isPending}
onClick={() => removePost.mutate(post.id)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
{removePost.isError && <p role="alert">{removePost.error.message}</p>}
</div>
);
}
@@ -0,0 +1,104 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(pages_path.join(format!(\"{page}.tsx\"))).unwrap_or_else(|_|\npanic!(\"frontend {page}.tsx file missing\"))"
---
import { useState } from "react";
import { useNavigate } from "react-router";
import { useCreatePost } from "../../api/posts";
import type { CreatePost } from "../../bindings/CreatePost";
import type { PostStatus } from "../../bindings/PostStatus";
const STATUS_OPTIONS: PostStatus[] = ["draft", "published", "archived"];
export function New() {
const navigate = useNavigate();
const createPost = useCreatePost();
const [form, setForm] = useState<CreatePost>({
title: "",
content: "",
status: "draft",
price: "",
published_at: null,
});
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
createPost.mutate(form, {
onSuccess: (created) => {
navigate(`/posts/${created.id}`);
},
});
}
return (
<div>
<h1>New Post</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="content">Content</label>
<textarea
id="content"
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="status">Status</label>
<select
id="status"
value={form.status}
onChange={(e) =>
setForm({ ...form, status: e.target.value as PostStatus })
}
>
{STATUS_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<div>
<label htmlFor="price">Price</label>
<input
id="price"
type="text"
value={form.price}
onChange={(e) => setForm({ ...form, price: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="published_at">Published at</label>
<input
id="published_at"
type="datetime-local"
value={form.published_at ?? ""}
onChange={(e) =>
setForm({
...form,
published_at: e.target.value || null,
})
}
/>
</div>
<button type="submit" disabled={createPost.isPending}>
{createPost.isPending ? "Creating…" : "Create"}
</button>
{createPost.error && <p role="alert">{createPost.error.message}</p>}
</form>
</div>
);
}
@@ -0,0 +1,54 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(pages_path.join(format!(\"{page}.tsx\"))).unwrap_or_else(|_|\npanic!(\"frontend {page}.tsx file missing\"))"
---
import { Link, useParams } from "react-router";
import { usePost } from "../../api/posts";
export function Show() {
const { id: idParam } = useParams<{ id: string }>();
const id = Number(idParam);
const isValidId = Number.isFinite(id) && id > 0;
const { data, isLoading, isError, error } = usePost(isValidId ? id : NaN);
if (!isValidId) {
return <p role="alert">Invalid post id.</p>;
}
if (isLoading) {
return <p>Loading…</p>;
}
if (isError) {
return <p role="alert">{error.message}</p>;
}
if (!data) {
return <p>Post not found.</p>;
}
return (
<div>
<h1>{data.title}</h1>
<dl>
<dt>Content</dt>
<dd>{data.content}</dd>
<dt>Status</dt>
<dd>{data.status}</dd>
<dt>Price</dt>
<dd>{data.price}</dd>
<dt>Published at</dt>
<dd>{data.published_at ?? "—"}</dd>
<dt>Created at</dt>
<dd>{data.created_at}</dd>
<dt>Updated at</dt>
<dd>{data.updated_at}</dd>
</dl>
<p>
<Link to={`/posts/${data.id}/edit`}>Edit</Link>{" "}
<Link to="/posts">Back to list</Link>
</p>
</div>
);
}
@@ -0,0 +1,10 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
{% extends "base.t" %}
{% block title %}Welcome!{% endblock %}
{% block body %}
<h1>Welcome!</h1>
<p>welcome to <em>acmeworld!</em></p>
{% endblock %}
@@ -0,0 +1,39 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
#![allow(non_upper_case_globals)]
use loco_rs::prelude::*;
use serde_json::json;
static shared: Dir<'_> = include_dir!("src/mailers/shared");
static welcome: Dir<'_> = include_dir!("src/mailers/reset_password/welcome");
#[allow(clippy::module_name_repetitions)]
pub struct ResetPassword {}
impl Mailer for ResetPassword {}
impl ResetPassword {
/// Send an email
///
/// # Errors
/// When email sending is failed
pub async fn send_welcome(ctx: &AppContext, to: &str, msg: &str) -> Result<()> {
Self::mail_template_with_shared(
ctx,
&welcome,
&[&shared],
mailer::Args {
to: to.to_string(),
locals: json!({
"message": msg,
"domain": ctx.config.server.full_url()
}),
..Default::default()
},
)
.await?;
Ok(())
}
}
@@ -0,0 +1,31 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "posts",
&[
("id", ColType::PkAuto),
("title", ColType::String),
("content", ColType::Text),
("status", ColType::String),
("price", ColType::Decimal),
("published_at", ColType::TimestampWithTimeZoneNull),
],
&[
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "posts").await
}
}
@@ -0,0 +1,24 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_column(m, "users", "name", ColType::StringNull).await?;
add_column(m, "users", "age", ColType::BigIntegerNull).await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
remove_column(m, "users", "name").await?;
remove_column(m, "users", "age").await?;
Ok(())
}
}
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
snapshot_kind: text
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_reference(m, "posts", "user", "").await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
remove_reference(m, "posts", "user", "").await?;
Ok(())
}
}
@@ -0,0 +1,28 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_join_table_without_timestamps(m, "user_groups",
&[
("count", ColType::BigIntegerNull),
],
&[
("user", ""),
("group", ""),
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "user_groups").await
}
}
@@ -0,0 +1,28 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_join_table_without_timestamps(m, "user_groups",
&[
("count", ColType::BigIntegerNull),
],
&[
("user", ""),
("group", ""),
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "user_groups").await
}
}
@@ -0,0 +1,28 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "movies",
&[
("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
("user", ""),
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "movies").await
}
}
@@ -0,0 +1,28 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table_without_timestamps(m, "movies",
&[
("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
("user", ""),
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "movies").await
}
}
@@ -0,0 +1,20 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
snapshot_kind: text
---
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
todo!()
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}
@@ -0,0 +1,27 @@
---
source: loco-gen/tests/templates/model.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_table(m, "movies",
&[
("id", ColType::PkAuto),
("title", ColType::StringNull),
],
&[
]
).await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "movies").await
}
}
@@ -0,0 +1,24 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")"
---
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
remove_column(m, "users", "name").await?;
remove_column(m, "users", "age").await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_column(m, "users", "name", ColType::StringNull).await?;
add_column(m, "users", "age", ColType::BigIntegerNull).await?;
Ok(())
}
}
@@ -0,0 +1,25 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\"nginx\").join(\"default.conf\")).expect(\"nginx config missing\")"
---
server {
listen 80;
server_name ~^(?<subdomain>\w*)\.localhost$;
location / {
if ($http_x_subdomain = "") {
set $http_x_subdomain $subdomain;
}
proxy_set_header X-Subdomain $http_x_subdomain;
proxy_pass http://localhost:8080/;
}
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8080/;
}
}
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Email{% endblock %}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
<div class="footer">
{% block footer %}{% endblock %}
</div>
</div>
</body>
</html>
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
{% block subject %}Email{% endblock %}
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
{% block text %}{% endblock %}
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
guess what? welcome!
@@ -0,0 +1,35 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(tests_path.join(\"posts.rs\")).expect(\"Failed to read posts.rs\")"
---
use tester::app::App;
use loco_rs::testing::prelude::*;
use serial_test::serial;
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
};
}
#[tokio::test]
#[serial]
async fn test_model() {
configure_insta!();
let boot = boot_test::<App>().await.unwrap();
seed::<App>(&boot.app_context).await.unwrap();
// query your model, e.g.:
//
// let item = models::posts::Model::find_by_pid(
// &boot.app_context.db,
// "11111111-1111-1111-1111-111111111111",
// )
// .await;
// snapshot the result:
// assert_debug_snapshot!(item);
}
@@ -0,0 +1,35 @@
---
source: loco-gen/tests/templates/model.rs
expression: "fs::read_to_string(tests_path.join(\"movies.rs\")).expect(\"Failed to read movies.rs\")"
---
use tester::app::App;
use loco_rs::testing::prelude::*;
use serial_test::serial;
macro_rules! configure_insta {
($($expr:expr),*) => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
};
}
#[tokio::test]
#[serial]
async fn test_model() {
configure_insta!();
let boot = boot_test::<App>().await.unwrap();
seed::<App>(&boot.app_context).await.unwrap();
// query your model, e.g.:
//
// let item = models::posts::Model::find_by_pid(
// &boot.app_context.db,
// "11111111-1111-1111-1111-111111111111",
// )
// .await;
// snapshot the result:
// assert_debug_snapshot!(item);
}
@@ -0,0 +1,40 @@
---
source: loco-gen/tests/templates/controller.rs
expression: "fs::read_to_string(test_controllers_path.join(\"movie.rs\")).expect(\"test file missing\")"
---
use tester::app::App;
use loco_rs::testing::prelude::*;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn can_get_movies() {
request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/api/movies").await;
assert_eq!(res.status_code(), 200);
// you can assert content like this:
// assert_eq!(res.text(), "content");
})
.await;
}
#[tokio::test]
#[serial]
async fn can_get_GET() {
request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/api/movies/GET").await;
assert_eq!(res.status_code(), 200);
})
.await;
}
#[tokio::test]
#[serial]
async fn can_get_POST() {
request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/api/movies/POST").await;
assert_eq!(res.status_code(), 200);
})
.await;
}
@@ -0,0 +1,21 @@
---
source: loco-gen/tests/templates/task.rs
expression: "fs::read_to_string(tests_task_path.join(\"cleanup.rs\")).expect(\"Failed to read generated tests task file: cleanup.rs\")"
---
use tester::app::App;
use loco_rs::{task, testing::prelude::*};
use loco_rs::boot::run_task;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_can_run_cleanup() {
let boot = boot_test::<App>().await.unwrap();
assert!(
run_task::<App>(&boot.app_context, Some(&"cleanup".to_string()), &task::Vars::default())
.await
.is_ok()
);
}
@@ -0,0 +1,24 @@
---
source: loco-gen/tests/templates/worker.rs
expression: "fs::read_to_string(tests_worker_path.join(\"register_email.rs\")).expect(\"Failed to read generated tests worker file: register_email.rs\")"
---
use loco_rs::{bgworker::BackgroundWorker, testing::prelude::*};
use tester::{
app::App,
workers::register_email::{Worker, WorkerArgs},
};
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_run_register_email_worker() {
let boot = boot_test::<App>().await.unwrap();
// Execute the worker ensuring that it operates in 'ForegroundBlocking' mode, which prevents the addition of your worker to the background
assert!(
Worker::perform_later(&boot.app_context,WorkerArgs {})
.await
.is_ok()
);
// Include additional assert validations after the execution of the worker
}
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
welcome to acmeworld!
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration `add_name_and_age_to_users` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration `add_user_ref_to_posts` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration for `CreateJoinTableUsersAndGroups` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration for `CreateJoinTableUsersAndGroups` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration for `FixUsersTable` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/migration.rs
expression: collect_messages(&gen_result)
snapshot_kind: text
---
* Migration `remove_name_and_age_from_users` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/controller.rs
expression: collect_messages(&gen_result)
---
* Controller `Movie` was added successfully.
* Tests for controller `Movie` was added successfully. Run `cargo test`.
@@ -0,0 +1,13 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: collect_messages(&gen_result)
---
* Migration for `post` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.
* A test for model `Posts` was added. Run with `cargo test`.
* Controller `Post` was added successfully.
* DTO `PostDto` was added successfully.
* Frontend API hooks `posts.ts` were added successfully.
* Frontend `Edit` page for `Post` was added successfully.
* Frontend `List` page for `Post` was added successfully.
* Frontend `New` page for `Post` was added successfully.
* Frontend `Show` page for `Post` was added successfully.
@@ -0,0 +1,9 @@
---
source: loco-gen/tests/templates/deployment.rs
expression: "fs::read_to_string(tree_fs.root.join(\".cargo\").join(\"config.toml\")).expect(\".cargo/config.toml not exists\")"
---
[alias]
loco = "run --"
loco-tool = "run --"
playground = "run --example playground"
@@ -0,0 +1,11 @@
---
source: loco-gen/tests/templates/controller.rs
expression: "fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\")).expect(\"app.rs injection failed\")"
---
impl Hooks for App {
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() // controller routes below
.add_route(controllers::movie::routes())
.add_route(controllers::auth::routes())
}
}
@@ -0,0 +1,12 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\")).expect(\"app.rs injection failed\")"
---
impl Hooks for App {
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() // controller routes below
.add_route(controllers::posts::routes())
.add_route(controllers::auth::routes())
}
}
@@ -0,0 +1,10 @@
---
source: loco-gen/tests/templates/task.rs
expression: "fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\")).expect(\"Failed to read updated app file: app.rs\")"
---
impl Hooks for App {
#[allow(unused_variables)]
fn register_tasks(tasks: &mut Tasks) {
tasks.register(tasks::cleanup::Cleanup);
// tasks-inject (do not remove)
}
@@ -0,0 +1,9 @@
---
source: loco-gen/tests/templates/worker.rs
expression: "fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\")).expect(\"Failed to read updated app file: app.rs\")"
---
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
queue.register(crate::workers::register_email::Worker::build(ctx)).await?;
queue.register(DownloadWorker::build(ctx)).await?;
Ok(())
}
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/controller.rs
expression: "fs::read_to_string(controllers_path.join(\"mod.rs\")).expect(\"mod.rs injection failed\")"
---
pub mod movie;
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(controllers_path.join(\"mod.rs\")).expect(\"mod.rs injection failed\")"
---
pub mod posts;
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(dtos_path.join(\"mod.rs\")).expect(\"mod.rs injection failed\")"
---
pub mod posts;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/mailer.rs
expression: "fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))"
---
pub mod reset_password;
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_posts;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_posts::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_add_name_and_age_to_users;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_add_name_and_age_to_users::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_add_user_ref_to_posts;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_add_user_ref_to_posts::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_create_join_table_users_and_groups;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_create_join_table_users_and_groups::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
snapshot_kind: text
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_create_join_table_users_and_groups;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_create_join_table_users_and_groups::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_movies;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_movies::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,23 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
snapshot_kind: text
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_movies;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_movies::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_fix_users_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_fix_users_table::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/model.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_movies;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_movies::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,22 @@
---
source: loco-gen/tests/templates/migration.rs
expression: "fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")"
---
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m[TIME]_users;
mod m[TIME]_remove_name_and_age_from_users;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m[TIME]_users::Migration),
Box::new(m[TIME]_remove_name_and_age_from_users::Migration),
// inject-above (do not remove this comment)
]
}
}
@@ -0,0 +1,35 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(frontend_path.join(\"routes.tsx\")).expect(\"routes.tsx injection failed\")"
---
import { createBrowserRouter } from 'react-router'
import { App } from './App'
import { Login } from './auth/Login'
import { RequireAuth } from './auth/RequireAuth'
import { Home } from './pages/Home'
// scaffold:imports
import { Edit } from './pages/posts/Edit'
import { List } from './pages/posts/List'
import { New } from './pages/posts/New'
import { Show } from './pages/posts/Show'
export const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <Home /> },
{ path: 'login', element: <Login /> },
{
element: <RequireAuth />,
children: [
// scaffold:routes
{ path: 'posts', element: <List /> },
{ path: 'posts/new', element: <New /> },
{ path: 'posts/:id', element: <Show /> },
{ path: 'posts/:id/edit', element: <Edit /> },
],
},
],
},
])
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/task.rs
expression: "fs::read_to_string(task_path.join(\"mod.rs\")).expect(\"Failed to read updated task mod file: mod.rs\")"
---
pub mod cleanup;
@@ -0,0 +1,6 @@
---
source: loco-gen/tests/templates/scaffold.rs
expression: "fs::read_to_string(tests_path.join(\"mod.rs\")).expect(\"Failed to read mod.rs\")"
---
mod posts;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/model.rs
expression: "fs::read_to_string(tests_path.join(\"mod.rs\")).expect(\"Failed to read mod.rs\")"
---
mod movies;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/controller.rs
expression: "fs::read_to_string(test_controllers_path.join(\"mod.rs\")).expect(\"test mod.rs missing\")"
---
pub mod movie;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/task.rs
expression: "fs::read_to_string(tests_task_path.join(\"mod.rs\")).expect(\"Failed to read updated tests task mod file: mod.rs\")"
---
pub mod cleanup;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/worker.rs
expression: "fs::read_to_string(tests_worker_path.join(\"mod.rs\")).expect(\"Failed to read updated tests worker mod file: mod.rs\")"
---
pub mod register_email;
@@ -0,0 +1,5 @@
---
source: loco-gen/tests/templates/worker.rs
expression: "fs::read_to_string(worker_path.join(\"mod.rs\")).expect(\"Failed to read updated worker mod file: mod.rs\")"
---
pub mod register_email;
+80
View File
@@ -0,0 +1,80 @@
use super::utils::APP_TASK;
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
macro_rules! configure_insta {
() => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("task");
let _guard = settings.bind_to_scope();
};
}
#[test]
fn can_generate() {
configure_insta!();
let component = Component::Task {
name: "cleanup".to_string(),
};
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/tasks/mod.rs")
.add_empty("tests/requests/mod.rs")
.add_empty("tests/tasks/mod.rs")
.add("src/app.rs", APP_TASK)
.create()
.expect("Failed to create tree_fs structure");
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Failed to generate components");
assert_eq!(
collect_messages(&gen_result),
r"* A Task `Cleanup` was added successfully. Run with `cargo loco task cleanup`.
* Tests for task `Cleanup` was added successfully. Run `cargo test`.
"
);
let task_path = tree_fs.root.join("src").join("tasks");
assert_snapshot!(
"generate[controller_file]",
fs::read_to_string(task_path.join("cleanup.rs"))
.expect("Failed to read generated task file: cleanup.rs")
);
assert_snapshot!(
"inject[task_mod_rs]",
fs::read_to_string(task_path.join("mod.rs"))
.expect("Failed to read updated task mod file: mod.rs")
);
assert_snapshot!(
"inject[app_rs]",
fs::read_to_string(tree_fs.root.join("src").join("app.rs"))
.expect("Failed to read updated app file: app.rs")
);
// Assertions for test files
let tests_task_path = tree_fs.root.join("tests").join("tasks");
assert_snapshot!(
"generate[tests_task_file]",
fs::read_to_string(tests_task_path.join("cleanup.rs"))
.expect("Failed to read generated tests task file: cleanup.rs")
);
assert_snapshot!(
"inject[tests_task_mod]",
fs::read_to_string(tests_task_path.join("mod.rs"))
.expect("Failed to read updated tests task mod file: mod.rs")
);
}
+92
View File
@@ -0,0 +1,92 @@
use chrono::{Duration, Utc};
use std::path::{Path, PathBuf};
pub const MIGRATION_SRC_LIB: &str = r"
#![allow(elided_lifetimes_in_paths)]
#![allow(clippy::wildcard_imports)]
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_users;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_users::Migration),
// inject-above (do not remove this comment)
]
}
}
";
pub const APP_ROUTS: &str = r"
impl Hooks for App {
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() // controller routes below
.add_route(controllers::auth::routes())
}
}
";
pub const APP_TASK: &str = r"
impl Hooks for App {
#[allow(unused_variables)]
fn register_tasks(tasks: &mut Tasks) {
// tasks-inject (do not remove)
}
";
pub const APP_WORKER: &str = r"
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
queue.register(DownloadWorker::build(ctx)).await?;
Ok(())
}
";
/// A minimal `frontend/src/routes.tsx` fixture mirroring the shape of
/// `examples/reference_spa/frontend/src/routes.tsx`, but carrying the
/// `// scaffold:imports` / `// scaffold:routes` anchor comments the Api
/// scaffold's `frontend_list.t` injects into. The once-per-app base
/// `routes.tsx` (workstream 2c) must ship these two markers for real apps.
pub const ROUTES_TSX_FIXTURE: &str = r"import { createBrowserRouter } from 'react-router'
import { App } from './App'
import { Login } from './auth/Login'
import { RequireAuth } from './auth/RequireAuth'
import { Home } from './pages/Home'
// scaffold:imports
export const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <Home /> },
{ path: 'login', element: <Login /> },
{
element: <RequireAuth />,
children: [
// scaffold:routes
],
},
],
},
])
";
pub fn guess_file_by_time(path: &Path, file_format: &str, max_attempts: u32) -> Option<PathBuf> {
let now = Utc::now();
for seconds_to_subtract in 0..=max_attempts {
let guessed_time = now - Duration::seconds(i64::from(seconds_to_subtract));
let formatted_time = guessed_time.format("%Y%m%d_%H%M%S").to_string();
let file_name = file_format.replace("{TIME}", &formatted_time);
let file_path = path.join(file_name);
if file_path.exists() {
return Some(file_path);
}
}
None
}
+80
View File
@@ -0,0 +1,80 @@
use super::utils::APP_WORKER;
use insta::assert_snapshot;
use loco_gen::{collect_messages, generate, AppInfo, Component};
use rrgen::RRgen;
use std::fs;
macro_rules! configure_insta {
() => {
let mut settings = insta::Settings::clone_current();
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_suffix("worker");
let _guard = settings.bind_to_scope();
};
}
#[test]
fn can_generate() {
configure_insta!();
let component = Component::Worker {
name: "register_email".to_string(),
};
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.add_empty("src/workers/mod.rs")
.add_empty("tests/workers/mod.rs")
.add("src/app.rs", APP_WORKER)
.create()
.expect("Failed to create tree_fs structure");
let rrgen = RRgen::with_working_dir(&tree_fs.root);
let gen_result = generate(
&rrgen,
component,
&AppInfo {
app_name: "tester".to_string(),
},
)
.expect("Failed to generate components");
assert_eq!(
collect_messages(&gen_result),
r"* Test for worker `RegisterEmail` was added successfully. Run `cargo test`.
* A worker `RegisterEmail` was added successfully. Run with `cargo loco start --worker`.
"
);
// Assertions for generated files
let worker_path = tree_fs.root.join("src").join("workers");
assert_snapshot!(
"generate[controller_file]",
fs::read_to_string(worker_path.join("register_email.rs"))
.expect("Failed to read generated worker file: register_email.rs")
);
assert_snapshot!(
"inject[worker_mod_rs]",
fs::read_to_string(worker_path.join("mod.rs"))
.expect("Failed to read updated worker mod file: mod.rs")
);
assert_snapshot!(
"inject[app_rs]",
fs::read_to_string(tree_fs.root.join("src").join("app.rs"))
.expect("Failed to read updated app file: app.rs")
);
// Assertions for test files
let tests_worker_path = tree_fs.root.join("tests").join("workers");
assert_snapshot!(
"generate[tests_worker_file]",
fs::read_to_string(tests_worker_path.join("register_email.rs"))
.expect("Failed to read generated tests worker file: register_email.rs")
);
assert_snapshot!(
"inject[tests_worker_mod]",
fs::read_to_string(tests_worker_path.join("mod.rs"))
.expect("Failed to read updated tests worker mod file: mod.rs")
);
}