use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Component, Path};
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid migration name `{name}`: {reason}")]
pub struct InvalidMigrationName {
name: String,
reason: &'static str,
}
pub fn validate_migration_name(name: &str) -> Result<(), InvalidMigrationName> {
let invalid = |reason| InvalidMigrationName {
name: name.to_string(),
reason,
};
if name.trim().is_empty() {
return Err(invalid("name cannot be empty"));
}
if name.contains('/') || name.contains('\\') {
return Err(invalid("path separators are not allowed"));
}
if name.chars().any(char::is_control) {
return Err(invalid("control characters are not allowed"));
}
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(invalid("name must be one normal path component"));
}
Ok(())
}
pub const ADJECTIVES: &[&str] = &[
"abandoned",
"aberrant",
"abnormal",
"absent",
"absurd",
"acoustic",
"adorable",
"amazing",
"ambiguous",
"ambitious",
"amused",
"amusing",
"ancient",
"aromatic",
"aspiring",
"awesome",
"bent",
"big",
"bitter",
"bizarre",
"black",
"blue",
"blushing",
"bored",
"boring",
"bouncy",
"brainy",
"brave",
"breezy",
"brief",
"bright",
"broad",
"broken",
"brown",
"bumpy",
"burly",
"busy",
"calm",
"careful",
"careless",
"certain",
"charming",
"cheerful",
"chemical",
"chief",
"chilly",
"chubby",
"chunky",
"clammy",
"classy",
"clean",
"clear",
"clever",
"cloudy",
"closed",
"clumsy",
"cold",
"colorful",
"colossal",
"common",
"complete",
"complex",
"concerned",
"condemned",
"confused",
"conscious",
"cooing",
"cool",
"crazy",
"cuddly",
"cultured",
"curious",
"curly",
"curved",
"curvy",
"cute",
"cynical",
"daffy",
"daily",
"damp",
"dapper",
"dark",
"dashing",
"dazzling",
"dear",
"deep",
"demonic",
"dizzy",
"dry",
"dusty",
"eager",
"early",
"easy",
"elite",
"eminent",
"empty",
"equal",
"even",
"exotic",
"fair",
"faithful",
"familiar",
"famous",
"fancy",
"fantastic",
"far",
"fast",
"fat",
"faulty",
"fearless",
"fine",
"first",
"fixed",
"flaky",
"flashy",
"flat",
"flawless",
"flimsy",
"flippant",
"flowery",
"fluffy",
"foamy",
"free",
"freezing",
"fresh",
"friendly",
"funny",
"furry",
"futuristic",
"fuzzy",
"giant",
"gifted",
"gigantic",
"glamorous",
"glorious",
"glossy",
"good",
"goofy",
"gorgeous",
"graceful",
"gray",
"great",
"greedy",
"green",
"grey",
"groovy",
"handy",
"happy",
"hard",
"harsh",
"heavy",
"hesitant",
"high",
"hot",
"huge",
"icy",
"illegal",
"jazzy",
"jittery",
"keen",
"kind",
"known",
"lame",
"large",
"last",
"late",
"lazy",
"lean",
"left",
"legal",
"lethal",
"light",
"little",
"lively",
"living",
"lonely",
"long",
"loose",
"loud",
"lovely",
"loving",
"low",
"lowly",
"lucky",
"lumpy",
"lush",
"luxuriant",
"lying",
"lyrical",
"magenta",
"magical",
"majestic",
"many",
"massive",
"married",
"marvelous",
"material",
"mature",
"mean",
"medical",
"melodic",
"melted",
"messy",
"mighty",
"military",
"milky",
"minor",
"misty",
"mixed",
"moaning",
"modern",
"motionless",
"mushy",
"mute",
"mysterious",
"naive",
"nappy",
"narrow",
"nasty",
"natural",
"neat",
"nebulous",
"needy",
"nervous",
"new",
"next",
"nice",
"nifty",
"noisy",
"normal",
"nostalgic",
"nosy",
"numerous",
"odd",
"old",
"omniscient",
"open",
"opposite",
"optimal",
"orange",
"ordinary",
"organic",
"outgoing",
"outstanding",
"oval",
"overconfident",
"overjoyed",
"overrated",
"pale",
"panoramic",
"parallel",
"parched",
"past",
"peaceful",
"perfect",
"perpetual",
"petite",
"pink",
"plain",
"polite",
"powerful",
"premium",
"pretty",
"previous",
"productive",
"public",
"purple",
"puzzling",
"quick",
"quiet",
"rainy",
"rapid",
"rare",
"real",
"red",
"redundant",
"reflective",
"regular",
"remarkable",
"rich",
"right",
"robust",
"romantic",
"round",
"sad",
"safe",
"salty",
"same",
"secret",
"serious",
"shallow",
"sharp",
"shiny",
"shocking",
"short",
"silent",
"silky",
"silly",
"simple",
"skinny",
"sleepy",
"slim",
"slimy",
"slippery",
"sloppy",
"slow",
"small",
"smart",
"smiling",
"smooth",
"soft",
"solid",
"sour",
"sparkling",
"special",
"spicy",
"spooky",
"spotty",
"square",
"stale",
"steady",
"steep",
"sticky",
"stiff",
"stormy",
"strange",
"striped",
"strong",
"sturdy",
"sudden",
"superb",
"supreme",
"sweet",
"swift",
"talented",
"tan",
"tearful",
"tense",
"thankful",
"thick",
"thin",
"third",
"tidy",
"tiny",
"tired",
"tiresome",
"tough",
"tranquil",
"tricky",
"true",
"typical",
"uneven",
"unique",
"unknown",
"unusual",
"useful",
"vengeful",
"violet",
"volatile",
"wakeful",
"wandering",
"warm",
"watery",
"wealthy",
"wet",
"white",
"whole",
"wide",
"wild",
"windy",
"wise",
"wonderful",
"wooden",
"woozy",
"workable",
"worried",
"worthless",
"yellow",
"yielding",
"young",
"youthful",
"yummy",
"zippy",
];
pub const HEROES: &[&str] = &[
"aaron_stack",
"abomination",
"absorbing_man",
"adam_destine",
"adam_warlock",
"agent_brand",
"agent_zero",
"albert_cleary",
"alex_power",
"alex_wilder",
"alice",
"amazoness",
"amphibian",
"angel",
"anita_blake",
"annihilus",
"anthem",
"apocalypse",
"aqueduct",
"arachne",
"archangel",
"arclight",
"ares",
"argent",
"avengers",
"azazel",
"banshee",
"baron_strucker",
"baron_zemo",
"barracuda",
"bastion",
"beast",
"bedlam",
"ben_grimm",
"ben_parker",
"ben_urich",
"betty_brant",
"betty_ross",
"beyonder",
"big_bertha",
"bill_hollister",
"bishop",
"black_bird",
"black_bolt",
"black_cat",
"black_crow",
"black_knight",
"black_panther",
"black_queen",
"black_tarantula",
"black_tom",
"black_widow",
"blackheart",
"blacklash",
"blade",
"blazing_skull",
"blindfold",
"blink",
"blizzard",
"blob",
"blockbuster",
"blonde_phantom",
"bloodaxe",
"bloodscream",
"bloodstorm",
"bloodstrike",
"blue_blade",
"blue_marvel",
"blue_shield",
"blur",
"boom_boom",
"boomer",
"boomerang",
"bromley",
"brood",
"brother_voodoo",
"bruce_banner",
"bucky",
"bug",
"bulldozer",
"bullseye",
"bushwacker",
"butterfly",
"cable",
"callisto",
"calypso",
"cammi",
"cannonball",
"captain_america",
"captain_britain",
"captain_cross",
"captain_flint",
"captain_marvel",
"captain_midlands",
"captain_stacy",
"captain_universe",
"cardiac",
"caretaker",
"cargill",
"carlie_cooper",
"carmella_unuscione",
"carnage",
"cassandra_nova",
"catseye",
"celestials",
"centennial",
"cerebro",
"cerise",
"chamber",
"chameleon",
"champions",
"changeling",
"charles_xavier",
"chat",
"chimera",
"christian_walker",
"chronomancer",
"clea",
"clint_barton",
"cloak",
"cobalt_man",
"colleen_wing",
"colonel_america",
"colossus",
"corsair",
"crusher_hogan",
"crystal",
"cyclops",
"dagger",
"daimon_hellstrom",
"dakota_north",
"daredevil",
"dark_beast",
"dark_phoenix",
"darkhawk",
"darkstar",
"darwin",
"dazzler",
"deadpool",
"deathbird",
"deathstrike",
"demogoblin",
"devos",
"dexter_bennett",
"diamondback",
"doctor_doom",
"doctor_faustus",
"doctor_octopus",
"doctor_spectrum",
"doctor_strange",
"domino",
"donald_blake",
"doomsday",
"doorman",
"dorian_gray",
"dormammu",
"dracula",
"dragon_lord",
"dragon_man",
"drax",
"dreadnoughts",
"dreaming_celestial",
"dust",
"earthquake",
"echo",
"eddie_brock",
"edwin_jarvis",
"ego",
"electro",
"elektra",
"emma_frost",
"enchantress",
"ender_wiggin",
"energizer",
"epoch",
"eternals",
"eternity",
"excalibur",
"exiles",
"exodus",
"expediter",
"ezekiel",
"ezekiel_stane",
"fabian_cortez",
"falcon",
"fallen_one",
"famine",
"fantastic_four",
"fat_cobra",
"felicia_hardy",
"fenris",
"firebird",
"firebrand",
"firedrake",
"firelord",
"firestar",
"fixer",
"flatman",
"forge",
"forgotten_one",
"frank_castle",
"franklin_richards",
"franklin_storm",
"freak",
"frightful_four",
"frog_thor",
"gabe_jones",
"galactus",
"gambit",
"gamma_corps",
"gamora",
"gargoyle",
"garia",
"gateway",
"gauntlet",
"genesis",
"george_stacy",
"gertrude_yorkes",
"ghost_rider",
"giant_girl",
"giant_man",
"gideon",
"gladiator",
"glorian",
"goblin_queen",
"golden_guardian",
"goliath",
"gorgon",
"gorilla_man",
"grandmaster",
"gravity",
"green_goblin",
"gressill",
"grey_gargoyle",
"greymalkin",
"grim_reaper",
"groot",
"guardian",
"guardsmen",
"gunslinger",
"gwen_stacy",
"hairball",
"hammerhead",
"hannibal_king",
"hardball",
"harpoon",
"harrier",
"harry_osborn",
"havok",
"hawkeye",
"hedge_knight",
"hellcat",
"hellfire_club",
"hellion",
"hemingway",
"hercules",
"hex",
"hiroim",
"hitman",
"hobgoblin",
"hulk",
"human_cannonball",
"human_fly",
"human_robot",
"human_torch",
"husk",
"hydra",
"iceman",
"ikaris",
"imperial_guard",
"impossible_man",
"inertia",
"infant_terrible",
"inhumans",
"ink",
"invaders",
"invisible_woman",
"iron_fist",
"iron_lad",
"iron_man",
"iron_monger",
"iron_patriot",
"ironclad",
"jack_flag",
"jack_murdock",
"jack_power",
"jackal",
"jackpot",
"james_howlett",
"jamie_braddock",
"jane_foster",
"jasper_sitwell",
"jazinda",
"jean_grey",
"jetstream",
"jigsaw",
"jimmy_woo",
"jocasta",
"johnny_blaze",
"johnny_storm",
"joseph",
"joshua_kane",
"joystick",
"jubilee",
"juggernaut",
"junta",
"justice",
"justin_hammer",
"kabuki",
"kang",
"karen_page",
"karma",
"karnak",
"kat_farrell",
"kate_bishop",
"katie_power",
"ken_ellis",
"khan",
"kid_colt",
"killer_shrike",
"killmonger",
"killraven",
"king_bedlam",
"king_cobra",
"kingpin",
"kinsey_walden",
"kitty_pryde",
"klaw",
"komodo",
"korath",
"korg",
"korvac",
"kree",
"krista_starr",
"kronos",
"kulan_gath",
"kylun",
"la_nuit",
"lady_bullseye",
"lady_deathstrike",
"lady_mastermind",
"lady_ursula",
"lady_vermin",
"lake",
"landau",
"layla_miller",
"leader",
"leech",
"legion",
"lenny_balinger",
"leo",
"leopardon",
"leper_queen",
"lester",
"lethal_legion",
"lifeguard",
"lightspeed",
"lila_cheney",
"lilandra",
"lilith",
"lily_hollister",
"lionheart",
"living_lightning",
"living_mummy",
"living_tribunal",
"liz_osborn",
"lizard",
"loa",
"lockheed",
"lockjaw",
"logan",
"loki",
"loners",
"longshot",
"lord_hawal",
"lord_tyger",
"lorna_dane",
"luckman",
"lucky_pierre",
"luke_cage",
"luminals",
"lyja",
"ma_gnuci",
"mac_gargan",
"mach_iv",
"machine_man",
"mad_thinker",
"madame_hydra",
"madame_masque",
"madame_web",
"maddog",
"madelyne_pryor",
"madripoor",
"madrox",
"maelstrom",
"maestro",
"magdalene",
"maggott",
"magik",
"maginty",
"magma",
"magneto",
"magus",
"major_mapleleaf",
"makkari",
"malcolm_colcord",
"malice",
"mandarin",
"mandrill",
"mandroid",
"manta",
"mantis",
"marauders",
"maria_hill",
"mariko_yashida",
"marrow",
"marten_broadcloak",
"martin_li",
"marvel_apes",
"marvel_boy",
"marvel_zombies",
"marvex",
"masked_marvel",
"masque",
"master_chief",
"master_mold",
"mastermind",
"mathemanic",
"matthew_murdock",
"mattie_franklin",
"mauler",
"maverick",
"maximus",
"may_parker",
"medusa",
"meggan",
"meltdown",
"menace",
"mentallo",
"mentor",
"mephisto",
"mephistopheles",
"mercury",
"mesmero",
"metal_master",
"meteorite",
"micromacro",
"microbe",
"microchip",
"micromax",
"midnight",
"miek",
"mikhail_rasputin",
"millenium_guard",
"mimic",
"mindworm",
"miracleman",
"miss_america",
"mister_fear",
"mister_sinister",
"misty_knight",
"mockingbird",
"moira_mactaggert",
"mojo",
"mole_man",
"molecule_man",
"molly_hayes",
"molten_man",
"mongoose",
"mongu",
"monster_badoon",
"moon_knight",
"moondragon",
"moonstone",
"morbius",
"mordo",
"morg",
"morgan_stark",
"morlocks",
"morlun",
"morph",
"mother_askani",
"mulholland_black",
"multiple_man",
"mysterio",
"mystique",
"namor",
"namora",
"namorita",
"naoko",
"natasha_romanoff",
"nebula",
"nehzno",
"nekra",
"nemesis",
"network",
"newton_destine",
"next_avengers",
"nextwave",
"nick_fury",
"nico_minoru",
"nicolaos",
"night_nurse",
"night_thrasher",
"nightcrawler",
"nighthawk",
"nightmare",
"nightshade",
"nitro",
"nocturne",
"nomad",
"norman_osborn",
"norrin_radd",
"northstar",
"nova",
"nuke",
"obadiah_stane",
"odin",
"ogun",
"old_lace",
"omega_flight",
"omega_red",
"omega_sentinel",
"onslaught",
"oracle",
"orphan",
"otto_octavius",
"outlaw_kid",
"overlord",
"owl",
"ozymandias",
"paibok",
"paladin",
"pandemic",
"paper_doll",
"patch",
"patriot",
"payback",
"penance",
"pepper_potts",
"pestilence",
"pet_avengers",
"pete_wisdom",
"peter_parker",
"peter_quill",
"phalanx",
"phantom_reporter",
"phil_sheldon",
"photon",
"piledriver",
"pixie",
"plazm",
"polaris",
"post",
"power_man",
"power_pack",
"praxagora",
"preak",
"pretty_boy",
"pride",
"prima",
"princess_powerful",
"prism",
"prodigy",
"proemial_gods",
"professor_monster",
"proteus",
"proudstar",
"prowler",
"psylocke",
"psynapse",
"puck",
"puff_adder",
"puma",
"punisher",
"puppet_master",
"purifiers",
"purple_man",
"pyro",
"quasar",
"quasimodo",
"queen_noir",
"quentin_quire",
"quicksilver",
"rachel_grey",
"radioactive_man",
"rafael_vega",
"rage",
"raider",
"randall",
"randall_flagg",
"random",
"rattler",
"ravenous",
"rawhide_kid",
"raza",
"reaper",
"reavers",
"red_ghost",
"red_hulk",
"red_shift",
"red_skull",
"red_wolf",
"redwing",
"reptil",
"retro_girl",
"revanche",
"rhino",
"rhodey",
"richard_fisk",
"rick_jones",
"ricochet",
"rictor",
"riptide",
"risque",
"robbie_robertson",
"robin_chapel",
"rocket_raccoon",
"rocket_racer",
"rockslide",
"rogue",
"roland_deschain",
"romulus",
"ronan",
"roughhouse",
"roulette",
"roxanne_simpson",
"rumiko_fujikawa",
"runaways",
"sabra",
"sabretooth",
"sage",
"sally_floyd",
"salo",
"sandman",
"santa_claus",
"saracen",
"sasquatch",
"satana",
"sauron",
"scalphunter",
"scarecrow",
"scarlet_spider",
"scarlet_witch",
"scorpion",
"scourge",
"scrambler",
"scream",
"screwball",
"sebastian_shaw",
"secret_warriors",
"selene",
"senator_kelly",
"sentinel",
"sentinels",
"sentry",
"ser_duncan",
"serpent_society",
"sersi",
"shadow_king",
"shadowcat",
"shaman",
"shape",
"shard",
"sharon_carter",
"sharon_ventura",
"shatterstar",
"shen",
"sheva_callister",
"shinko_yamashiro",
"shinobi_shaw",
"shiva",
"shiver_man",
"shocker",
"shockwave",
"shooting_star",
"shotgun",
"shriek",
"silhouette",
"silk_fever",
"silver_centurion",
"silver_fox",
"silver_sable",
"silver_samurai",
"silver_surfer",
"silverclaw",
"silvermane",
"sinister_six",
"sir_ram",
"siren",
"sister_grimm",
"skaar",
"skin",
"skreet",
"skrulls",
"skullbuster",
"slapstick",
"slayback",
"sleeper",
"sleepwalker",
"slipstream",
"slyde",
"smasher",
"smiling_tiger",
"snowbird",
"solo",
"songbird",
"spacker_dave",
"spectrum",
"speed",
"speed_demon",
"speedball",
"spencer_smythe",
"sphinx",
"spiral",
"spirit",
"spitfire",
"spot",
"sprite",
"spyke",
"squadron_sinister",
"squadron_supreme",
"squirrel_girl",
"star_brand",
"starbolt",
"stardust",
"starfox",
"starhawk",
"starjammers",
"stark_industries",
"stature",
"steel_serpent",
"stellaris",
"stepford_cuckoos",
"stephen_strange",
"steve_rogers",
"stick",
"stingray",
"stone_men",
"storm",
"stranger",
"strong_guy",
"stryfe",
"sue_storm",
"sugar_man",
"sumo",
"sunfire",
"sunset_bain",
"sunspot",
"supernaut",
"supreme_intelligence",
"surge",
"susan_delgado",
"swarm",
"sway",
"switch",
"swordsman",
"synch",
"tag",
"talisman",
"talkback",
"talon",
"talos",
"tana_nile",
"tarantula",
"tarot",
"taskmaster",
"tattoo",
"ted_forrester",
"tempest",
"tenebrous",
"terrax",
"terror",
"texas_twister",
"thaddeus_ross",
"thanos",
"the_anarchist",
"the_call",
"the_captain",
"the_enforcers",
"the_executioner",
"the_fallen",
"the_fury",
"the_hand",
"the_hood",
"the_hunter",
"the_initiative",
"the_leader",
"the_liberteens",
"the_order",
"the_phantom",
"the_professor",
"the_renegades",
"the_santerians",
"the_spike",
"the_stranger",
"the_twelve",
"the_watchers",
"thena",
"thing",
"thor",
"thor_girl",
"thunderball",
"thunderbird",
"thunderbolt",
"thunderbolt_ross",
"thunderbolts",
"thundra",
"tiger_shark",
"tigra",
"timeslip",
"tinkerer",
"titania",
"titanium_man",
"toad",
"toad_men",
"tomas",
"tombstone",
"tomorrow_man",
"tony_stark",
"toro",
"toxin",
"trauma",
"triathlon",
"trish_tilby",
"triton",
"true_believers",
"turbo",
"tusk",
"tyger_tiger",
"typhoid_mary",
"tyrannus",
"ulik",
"ultimates",
"ultimatum",
"ultimo",
"ultragirl",
"ultron",
"umar",
"unicorn",
"union_jack",
"unus",
"valeria_richards",
"valkyrie",
"vampiro",
"vance_astro",
"vanisher",
"vapor",
"vargas",
"vector",
"veda",
"vengeance",
"venom",
"venus",
"vermin",
"vertigo",
"victor_mancha",
"vin_gonzales",
"vindicator",
"violations",
"viper",
"virginia_dare",
"vision",
"vivisector",
"vulcan",
"vulture",
"wallflower",
"wallop",
"wallow",
"war_machine",
"warbird",
"warbound",
"warhawk",
"warlock",
"warpath",
"warstar",
"wasp",
"weapon_omega",
"wendell_rand",
"wendell_vaughn",
"wendigo",
"whiplash",
"whirlwind",
"whistler",
"white_queen",
"white_tiger",
"whizzer",
"wiccan",
"wild_child",
"wild_pack",
"wildside",
"william_stryker",
"wilson_fisk",
"wind_dancer",
"winter_soldier",
"wither",
"wolf_cub",
"wolfpack",
"wolfsbane",
"wolverine",
"wonder_man",
"wong",
"wraith",
"wrecker",
"wrecking_crew",
"xavin",
"xorn",
"yellow_claw",
"yellowjacket",
"young_avengers",
"zaladane",
"zaran",
"zarda",
"zarek",
"zeigeist",
"zemo",
"zodiak",
"zombie",
"zuras",
"zzzax",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PrefixMode {
#[default]
Timestamp,
Index,
Supabase,
Unix,
None,
}
impl PrefixMode {
#[must_use]
pub fn generate_prefix(&self, idx: u32) -> String {
match self {
Self::Timestamp => generate_timestamp_prefix(),
Self::Index => format!("{idx:04}"),
Self::Supabase => generate_supabase_prefix(),
Self::Unix => generate_unix_prefix(),
Self::None => String::new(),
}
}
}
impl FromStr for PrefixMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let result = match s.to_lowercase().as_str() {
"index" => Self::Index,
"supabase" => Self::Supabase,
"unix" => Self::Unix,
"none" => Self::None,
_ => Self::Timestamp,
};
Ok(result)
}
}
#[must_use]
pub fn generate_timestamp_prefix() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let time_of_day = secs % 86400;
let (year, month, day) = days_to_ymd(i64::try_from(days).unwrap_or(i64::MAX));
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
format!("{year:04}{month:02}{day:02}{hours:02}{minutes:02}{seconds:02}")
}
fn generate_supabase_prefix() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("{}", now.as_millis())
}
fn generate_unix_prefix() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("{}", now.as_secs())
}
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = u32::try_from(z - era * 146_097).unwrap_or(0);
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = i64::from(yoe) + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(i32::try_from(y).unwrap_or(i32::MAX), m, d)
}
#[must_use]
pub fn generate_random_suffix() -> String {
let seed: u64 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
let hash = hasher.finish();
let adj_mod = hash % ADJECTIVES.len() as u64;
let hero_mod = (hash >> 32) % HEROES.len() as u64;
let adj_idx = usize::try_from(adj_mod).unwrap_or(0);
let hero_idx = usize::try_from(hero_mod).unwrap_or(0);
format!("{}_{}", ADJECTIVES[adj_idx], HEROES[hero_idx])
}
pub fn generate_migration_tag(custom_name: Option<&str>) -> String {
let prefix = generate_timestamp_prefix();
let suffix = custom_name.map_or_else(generate_random_suffix, std::string::ToString::to_string);
format!("{prefix}_{suffix}")
}
pub fn generate_migration_tag_with_mode(
mode: PrefixMode,
idx: u32,
custom_name: Option<&str>,
) -> String {
let prefix = mode.generate_prefix(idx);
let suffix = custom_name.map_or_else(generate_random_suffix, std::string::ToString::to_string);
if prefix.is_empty() {
suffix
} else {
format!("{prefix}_{suffix}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_timestamp_prefix() {
let prefix = generate_timestamp_prefix();
assert_eq!(prefix.len(), 14);
assert!(prefix.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn test_generate_random_suffix() {
let suffix = generate_random_suffix();
let underscore_pos = suffix.find('_').expect("suffix should contain underscore");
let adjective = &suffix[..underscore_pos];
let hero = &suffix[underscore_pos + 1..];
assert!(
ADJECTIVES.contains(&adjective),
"adjective '{}' not found",
adjective
);
assert!(HEROES.contains(&hero), "hero '{}' not found", hero);
}
#[test]
fn test_generate_migration_tag() {
let tag = generate_migration_tag(None);
assert!(tag.len() > 14, "tag should be longer than timestamp");
let timestamp = &tag[0..14];
assert!(
timestamp.chars().all(|c| c.is_ascii_digit()),
"first 14 chars should be digits"
);
assert_eq!(&tag[14..15], "_", "underscore after timestamp");
let suffix = &tag[15..];
let underscore_pos = suffix.find('_').expect("suffix should contain underscore");
let adjective = &suffix[..underscore_pos];
let hero = &suffix[underscore_pos + 1..];
assert!(
ADJECTIVES.contains(&adjective),
"adjective '{}' not found",
adjective
);
assert!(HEROES.contains(&hero), "hero '{}' not found", hero);
}
#[test]
fn test_generate_migration_tag_with_custom_name() {
let tag = generate_migration_tag(Some("initial_setup"));
assert!(tag.ends_with("_initial_setup"));
assert!(tag.len() > 14); }
#[test]
fn test_prefix_modes() {
let idx_prefix = PrefixMode::Index.generate_prefix(0);
assert_eq!(idx_prefix, "0000");
let ts_prefix = PrefixMode::Timestamp.generate_prefix(0);
assert_eq!(ts_prefix.len(), 14);
let none_prefix = PrefixMode::None.generate_prefix(0);
assert!(none_prefix.is_empty());
}
#[test]
fn test_generate_tag_with_mode() {
let tag = generate_migration_tag_with_mode(PrefixMode::Index, 5, None);
assert!(tag.starts_with("0005_"));
let tag = generate_migration_tag_with_mode(PrefixMode::Timestamp, 0, Some("custom"));
assert!(tag.ends_with("_custom"));
let parts: Vec<_> = tag.split('_').collect();
assert_eq!(parts[0].len(), 14);
}
#[test]
fn migration_name_validation_accepts_single_components() {
for name in ["initial_setup", "add-users", "release 1", "café"] {
assert_eq!(validate_migration_name(name), Ok(()), "{name}");
}
}
#[test]
fn migration_name_validation_rejects_paths_and_controls() {
for name in [
"",
" ",
".",
"..",
"../escape",
"a/b",
"a\\b",
"bad\0name",
] {
assert!(validate_migration_name(name).is_err(), "{name:?}");
}
}
}