├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── README.md ├── example ├── Cargo.toml ├── docker-compose.yaml ├── fixtures │ └── mariadb │ │ └── schema.sql ├── src │ ├── lib.rs │ └── model.rs └── tests │ └── it_works.rs ├── img └── meme.jpg ├── symbols-models ├── Cargo.toml └── src │ └── lib.rs └── symbols ├── Cargo.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 120 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["symbols", "symbols-models"] 5 | 6 | exclude = ["example"] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symbols 2 | 3 | ## What is this? 4 | 5 | ![What the Fuck Did You Just Bring Upon This Cursed Land](./img/meme.jpg) 6 | 7 | This is an utility to build a proc-macro that connects to a database, retrieves data from given table and populates an enum variants with primary keys values.
8 | It also generates a method for every non-primary-key field, and, when there are multiple primary keys, a costructor for every possible subset of primary keys. 9 | 10 | ## Replacements 11 | 12 | Replacements are done using annotated parameters.
13 | Basic replacements are written in the form `#[macro(field = "enum")]` or `#[macro(field(type = "enum"))]`, where we are telling to replace string values from `field` with variants from enum `enum`, variant names will be the CamelCase version of field value.
14 | Advanced replacements are done in the form `#[macro(field(type = "bar", fn = "foo"))]`, where we are telling to replace string values from `field` with a call to method `foo` from struct/enum `bar`, method output is expected to be of type `bar`.
15 | *WARNING:* Since all produced methods are `const`, also methods you pass this way must be `const`. 16 | 17 | ### Cache 18 | 19 | To avoid flooding the database with requests, light up macro run times and be able to work offline, data cache files are stored in temp folder under the name of `.cache`.
20 | Quick way to delete it is to run 21 | ```bash 22 | find /tmp -name *.cache -delete 23 | ``` 24 | 25 | ### Examples 26 | 27 | You can find a basic example in [example](./example) folder, it uses a mariadb container to load a database, you can run it with: 28 | ```bash 29 | docker-compose run rust 30 | ``` 31 | Example code 32 | ```rust 33 | #[example::example( 34 | table = "best_selling_video_games", 35 | platforms(type = "Platforms", fn = "from_str"), 36 | developer = "Developer", 37 | publisher(type = "Publisher", fn = "from_str") 38 | )] 39 | pub enum BestSellingVideoGame {} 40 | ``` 41 | would expand to 42 | ```rust 43 | pub enum BestSellingVideoGame { 44 | AnimalCrossingNewHorizons, 45 | Borderlands2, 46 | CallOfDutyBlackOps, 47 | CallOfDutyBlackOpsIi, 48 | CallOfDutyModernWarfare, 49 | CallOfDutyModernWarfare2, 50 | CallOfDutyModernWarfare3, 51 | DiabloIiiReaperOfSouls, 52 | DuckHunt, 53 | Fifa18, 54 | GrandTheftAutoIv, 55 | GrandTheftAutoV, 56 | GrandTheftAutoSanAndreas, 57 | HumanFallFlat, 58 | KinectAdventures, 59 | MarioKart8Deluxe, 60 | MarioKartDs, 61 | MarioKartWii, 62 | Minecraft, 63 | NewSuperMarioBros, 64 | NewSuperMarioBrosUDeluxeLuigiU, 65 | NewSuperMarioBrosWii, 66 | Nintendogs, 67 | PacMan, 68 | PokemonDiamondPearlPlatinum, 69 | PokemonGoldSilverCrystal, 70 | PokemonRedGreenBlueYellow, 71 | PokemonRubySapphireEmerald, 72 | PokemonSunMoonUltraSunUltraMoon, 73 | PokemonSwordShield, 74 | PubgBattlegrounds, 75 | RedDeadRedemption, 76 | RedDeadRedemption2, 77 | SonicTheHedgehog, 78 | SuperMario64Ds, 79 | SuperMarioBros, 80 | SuperMarioBros3, 81 | SuperMarioOdyssey, 82 | SuperMarioWorld, 83 | SuperSmashBrosUltimate, 84 | Terraria, 85 | TetrisEa, 86 | TetrisNintendo, 87 | TheElderScrollsVSkyrim, 88 | TheLegendOfZeldaBreathOfTheWild, 89 | TheWitcher3HeartsOfStoneBloodAndWine, 90 | WiiFitPlus, 91 | WiiPlay, 92 | WiiSports, 93 | WiiSportsResort, 94 | } 95 | 96 | impl BestSellingVideoGame { 97 | pub const fn initial_release_date(&self) -> &'static str { 98 | match self { 99 | BestSellingVideoGame::AnimalCrossingNewHorizons => "March 20, 2020", 100 | BestSellingVideoGame::Borderlands2 => "September 18, 2012", 101 | BestSellingVideoGame::CallOfDutyBlackOps => "November 9, 2010", 102 | BestSellingVideoGame::CallOfDutyBlackOpsIi => "November 12, 2012", 103 | BestSellingVideoGame::CallOfDutyModernWarfare => "October 25, 2019", 104 | BestSellingVideoGame::CallOfDutyModernWarfare2 => "November 10, 2009", 105 | BestSellingVideoGame::CallOfDutyModernWarfare3 => "November 8, 2011", 106 | BestSellingVideoGame::DiabloIiiReaperOfSouls => "May 16, 2012", 107 | BestSellingVideoGame::DuckHunt => "April 21, 1984", 108 | BestSellingVideoGame::Fifa18 => "September 29, 2017", 109 | BestSellingVideoGame::GrandTheftAutoIv => "April 29, 2008", 110 | BestSellingVideoGame::GrandTheftAutoV => "September 17, 2013", 111 | BestSellingVideoGame::GrandTheftAutoSanAndreas => "October 26, 2004", 112 | BestSellingVideoGame::HumanFallFlat => "July 22, 2016", 113 | BestSellingVideoGame::KinectAdventures => "November 4, 2010", 114 | BestSellingVideoGame::MarioKart8Deluxe => "May 29, 2014", 115 | BestSellingVideoGame::MarioKartDs => "November 14, 2005", 116 | BestSellingVideoGame::MarioKartWii => "April 10, 2008", 117 | BestSellingVideoGame::Minecraft => "November 18, 2011", 118 | BestSellingVideoGame::NewSuperMarioBros => "May 15, 2006", 119 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => "November 18, 2012", 120 | BestSellingVideoGame::NewSuperMarioBrosWii => "November 11, 2009", 121 | BestSellingVideoGame::Nintendogs => "April 21, 2005", 122 | BestSellingVideoGame::PacMan => "May 22, 1980", 123 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => "September 28, 2006", 124 | BestSellingVideoGame::PokemonGoldSilverCrystal => "November 21, 1999", 125 | BestSellingVideoGame::PokemonRedGreenBlueYellow => "February 27, 1996", 126 | BestSellingVideoGame::PokemonRubySapphireEmerald => "November 21, 2002", 127 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => "November 18, 2016", 128 | BestSellingVideoGame::PokemonSwordShield => "November 15, 2019", 129 | BestSellingVideoGame::PubgBattlegrounds => "December 20, 2017", 130 | BestSellingVideoGame::RedDeadRedemption => "May 18, 2010", 131 | BestSellingVideoGame::RedDeadRedemption2 => "October 26, 2018", 132 | BestSellingVideoGame::SonicTheHedgehog => "June 23, 1991", 133 | BestSellingVideoGame::SuperMario64Ds => "June 23, 1996", 134 | BestSellingVideoGame::SuperMarioBros => "September 13, 1985", 135 | BestSellingVideoGame::SuperMarioBros3 => "October 23, 1988", 136 | BestSellingVideoGame::SuperMarioOdyssey => "October 27, 2017", 137 | BestSellingVideoGame::SuperMarioWorld => "November 21, 1990", 138 | BestSellingVideoGame::SuperSmashBrosUltimate => "December 7, 2018", 139 | BestSellingVideoGame::Terraria => "May 16, 2011", 140 | BestSellingVideoGame::TetrisEa => "September 12, 2006", 141 | BestSellingVideoGame::TetrisNintendo => "June 14, 1989", 142 | BestSellingVideoGame::TheElderScrollsVSkyrim => "November 11, 2011", 143 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => "March 3, 2017", 144 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => "May 19, 2015", 145 | BestSellingVideoGame::WiiFitPlus => "December 1, 2007", 146 | BestSellingVideoGame::WiiPlay => "December 2, 2006", 147 | BestSellingVideoGame::WiiSports => "November 19, 2006", 148 | BestSellingVideoGame::WiiSportsResort => "June 25, 2009", 149 | } 150 | } 151 | pub const fn rank(&self) -> i8 { 152 | match self { 153 | BestSellingVideoGame::AnimalCrossingNewHorizons => 15, 154 | BestSellingVideoGame::Borderlands2 => 33, 155 | BestSellingVideoGame::CallOfDutyBlackOps => 32, 156 | BestSellingVideoGame::CallOfDutyBlackOpsIi => 38, 157 | BestSellingVideoGame::CallOfDutyModernWarfare => 20, 158 | BestSellingVideoGame::CallOfDutyModernWarfare2 => 48, 159 | BestSellingVideoGame::CallOfDutyModernWarfare3 => 31, 160 | BestSellingVideoGame::DiabloIiiReaperOfSouls => 20, 161 | BestSellingVideoGame::DuckHunt => 25, 162 | BestSellingVideoGame::Fifa18 => 39, 163 | BestSellingVideoGame::GrandTheftAutoIv => 35, 164 | BestSellingVideoGame::GrandTheftAutoV => 2, 165 | BestSellingVideoGame::GrandTheftAutoSanAndreas => 27, 166 | BestSellingVideoGame::HumanFallFlat => 20, 167 | BestSellingVideoGame::KinectAdventures => 39, 168 | BestSellingVideoGame::MarioKart8Deluxe => 7, 169 | BestSellingVideoGame::MarioKartDs => 44, 170 | BestSellingVideoGame::MarioKartWii => 16, 171 | BestSellingVideoGame::Minecraft => 1, 172 | BestSellingVideoGame::NewSuperMarioBros => 18, 173 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => 50, 174 | BestSellingVideoGame::NewSuperMarioBrosWii => 19, 175 | BestSellingVideoGame::Nintendogs => 42, 176 | BestSellingVideoGame::PacMan => 13, 177 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => 36, 178 | BestSellingVideoGame::PokemonGoldSilverCrystal => 24, 179 | BestSellingVideoGame::PokemonRedGreenBlueYellow => 8, 180 | BestSellingVideoGame::PokemonRubySapphireEmerald => 49, 181 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => 34, 182 | BestSellingVideoGame::PokemonSwordShield => 43, 183 | BestSellingVideoGame::PubgBattlegrounds => 5, 184 | BestSellingVideoGame::RedDeadRedemption => 46, 185 | BestSellingVideoGame::RedDeadRedemption2 => 11, 186 | BestSellingVideoGame::SonicTheHedgehog => 41, 187 | BestSellingVideoGame::SuperMario64Ds => 47, 188 | BestSellingVideoGame::SuperMarioBros => 6, 189 | BestSellingVideoGame::SuperMarioBros3 => 37, 190 | BestSellingVideoGame::SuperMarioOdyssey => 45, 191 | BestSellingVideoGame::SuperMarioWorld => 30, 192 | BestSellingVideoGame::SuperSmashBrosUltimate => 29, 193 | BestSellingVideoGame::Terraria => 9, 194 | BestSellingVideoGame::TetrisEa => 3, 195 | BestSellingVideoGame::TetrisNintendo => 11, 196 | BestSellingVideoGame::TheElderScrollsVSkyrim => 20, 197 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => 28, 198 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => 14, 199 | BestSellingVideoGame::WiiFitPlus => 10, 200 | BestSellingVideoGame::WiiPlay => 26, 201 | BestSellingVideoGame::WiiSports => 4, 202 | BestSellingVideoGame::WiiSportsResort => 17, 203 | } 204 | } 205 | pub const fn sales(&self) -> &'static str { 206 | match self { 207 | BestSellingVideoGame::AnimalCrossingNewHorizons => "37,620,000", 208 | BestSellingVideoGame::Borderlands2 => "26,000,000", 209 | BestSellingVideoGame::CallOfDutyBlackOps => "26,200,000", 210 | BestSellingVideoGame::CallOfDutyBlackOpsIi => "24,200,000", 211 | BestSellingVideoGame::CallOfDutyModernWarfare => "30,000,000", 212 | BestSellingVideoGame::CallOfDutyModernWarfare2 => "22,700,000", 213 | BestSellingVideoGame::CallOfDutyModernWarfare3 => "26,500,000", 214 | BestSellingVideoGame::DiabloIiiReaperOfSouls => "30,000,000", 215 | BestSellingVideoGame::DuckHunt => "28,300,000", 216 | BestSellingVideoGame::Fifa18 => "24,000,000", 217 | BestSellingVideoGame::GrandTheftAutoIv => "25,000,000", 218 | BestSellingVideoGame::GrandTheftAutoV => "160,000,000", 219 | BestSellingVideoGame::GrandTheftAutoSanAndreas => "27,500,000", 220 | BestSellingVideoGame::HumanFallFlat => "30,000,000", 221 | BestSellingVideoGame::KinectAdventures => "24,000,000", 222 | BestSellingVideoGame::MarioKart8Deluxe => "51,810,000", 223 | BestSellingVideoGame::MarioKartDs => "23,600,000", 224 | BestSellingVideoGame::MarioKartWii => "37,380,000", 225 | BestSellingVideoGame::Minecraft => "238,000,000", 226 | BestSellingVideoGame::NewSuperMarioBros => "30,800,000", 227 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => "21,600,000", 228 | BestSellingVideoGame::NewSuperMarioBrosWii => "30,320,000", 229 | BestSellingVideoGame::Nintendogs => "23,960,000", 230 | BestSellingVideoGame::PacMan => "42,071,635", 231 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => "24,730,000", 232 | BestSellingVideoGame::PokemonGoldSilverCrystal => "29,490,000", 233 | BestSellingVideoGame::PokemonRedGreenBlueYellow => "47,520,000", 234 | BestSellingVideoGame::PokemonRubySapphireEmerald => "22,540,000", 235 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => "25,310,000", 236 | BestSellingVideoGame::PokemonSwordShield => "23,900,000", 237 | BestSellingVideoGame::PubgBattlegrounds => "75,000,000", 238 | BestSellingVideoGame::RedDeadRedemption => "23,000,000", 239 | BestSellingVideoGame::RedDeadRedemption2 => "43,000,000", 240 | BestSellingVideoGame::SonicTheHedgehog => "23,982,960", 241 | BestSellingVideoGame::SuperMario64Ds => "22,960,000", 242 | BestSellingVideoGame::SuperMarioBros => "58,000,000", 243 | BestSellingVideoGame::SuperMarioBros3 => "24,430,000", 244 | BestSellingVideoGame::SuperMarioOdyssey => "23,020,000", 245 | BestSellingVideoGame::SuperMarioWorld => "26,662,500", 246 | BestSellingVideoGame::SuperSmashBrosUltimate => "27,400,000", 247 | BestSellingVideoGame::Terraria => "44,000,000", 248 | BestSellingVideoGame::TetrisEa => "100,000,000", 249 | BestSellingVideoGame::TetrisNintendo => "43,000,000", 250 | BestSellingVideoGame::TheElderScrollsVSkyrim => "30,000,000", 251 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => "27,490,000", 252 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => "40,000,000", 253 | BestSellingVideoGame::WiiFitPlus => "43,800,000", 254 | BestSellingVideoGame::WiiPlay => "28,020,000", 255 | BestSellingVideoGame::WiiSports => "82,900,000", 256 | BestSellingVideoGame::WiiSportsResort => "33,140,000", 257 | } 258 | } 259 | pub const fn as_str(&self) -> &'static str { 260 | match self { 261 | BestSellingVideoGame::AnimalCrossingNewHorizons => "Animal Crossing: New Horizons", 262 | BestSellingVideoGame::Borderlands2 => "Borderlands 2", 263 | BestSellingVideoGame::CallOfDutyBlackOps => "Call of Duty: Black Ops", 264 | BestSellingVideoGame::CallOfDutyBlackOpsIi => "Call of Duty: Black Ops II", 265 | BestSellingVideoGame::CallOfDutyModernWarfare => "Call of Duty: Modern Warfare", 266 | BestSellingVideoGame::CallOfDutyModernWarfare2 => "Call of Duty: Modern Warfare 2", 267 | BestSellingVideoGame::CallOfDutyModernWarfare3 => "Call of Duty: Modern Warfare 3", 268 | BestSellingVideoGame::DiabloIiiReaperOfSouls => "Diablo III / Reaper of Souls", 269 | BestSellingVideoGame::DuckHunt => "Duck Hunt", 270 | BestSellingVideoGame::Fifa18 => "FIFA 18", 271 | BestSellingVideoGame::GrandTheftAutoIv => "Grand Theft Auto IV", 272 | BestSellingVideoGame::GrandTheftAutoV => "Grand Theft Auto V", 273 | BestSellingVideoGame::GrandTheftAutoSanAndreas => "Grand Theft Auto: San Andreas", 274 | BestSellingVideoGame::HumanFallFlat => "Human: Fall Flat", 275 | BestSellingVideoGame::KinectAdventures => "Kinect Adventures!", 276 | BestSellingVideoGame::MarioKart8Deluxe => "Mario Kart 8 / Deluxe", 277 | BestSellingVideoGame::MarioKartDs => "Mario Kart DS", 278 | BestSellingVideoGame::MarioKartWii => "Mario Kart Wii", 279 | BestSellingVideoGame::Minecraft => "Minecraft", 280 | BestSellingVideoGame::NewSuperMarioBros => "New Super Mario Bros.", 281 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => { 282 | "New Super Mario Bros. U / Deluxe / Luigi U" 283 | } 284 | BestSellingVideoGame::NewSuperMarioBrosWii => "New Super Mario Bros. Wii", 285 | BestSellingVideoGame::Nintendogs => "Nintendogs", 286 | BestSellingVideoGame::PacMan => "Pac-Man", 287 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => { 288 | "Pokemon Diamond / Pearl / Platinum" 289 | } 290 | BestSellingVideoGame::PokemonGoldSilverCrystal => "Pokemon Gold / Silver / Crystal", 291 | BestSellingVideoGame::PokemonRedGreenBlueYellow => { 292 | "Pokemon Red / Green / Blue / Yellow" 293 | } 294 | BestSellingVideoGame::PokemonRubySapphireEmerald => "Pokemon Ruby / Sapphire / Emerald", 295 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => { 296 | "Pokemon Sun / Moon / Ultra Sun / Ultra Moon" 297 | } 298 | BestSellingVideoGame::PokemonSwordShield => "Pokemon Sword / Shield", 299 | BestSellingVideoGame::PubgBattlegrounds => "PUBG: Battlegrounds", 300 | BestSellingVideoGame::RedDeadRedemption => "Red Dead Redemption", 301 | BestSellingVideoGame::RedDeadRedemption2 => "Red Dead Redemption 2", 302 | BestSellingVideoGame::SonicTheHedgehog => "Sonic the Hedgehog", 303 | BestSellingVideoGame::SuperMario64Ds => "Super Mario 64 / DS", 304 | BestSellingVideoGame::SuperMarioBros => "Super Mario Bros.", 305 | BestSellingVideoGame::SuperMarioBros3 => "Super Mario Bros. 3", 306 | BestSellingVideoGame::SuperMarioOdyssey => "Super Mario Odyssey", 307 | BestSellingVideoGame::SuperMarioWorld => "Super Mario World", 308 | BestSellingVideoGame::SuperSmashBrosUltimate => "Super Smash Bros. Ultimate", 309 | BestSellingVideoGame::Terraria => "Terraria", 310 | BestSellingVideoGame::TetrisEa => "Tetris (EA)", 311 | BestSellingVideoGame::TetrisNintendo => "Tetris (Nintendo)", 312 | BestSellingVideoGame::TheElderScrollsVSkyrim => "The Elder Scrolls V: Skyrim", 313 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => { 314 | "The Legend of Zelda: Breath of the Wild" 315 | } 316 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => { 317 | "The Witcher 3 / Hearts of Stone / Blood and Wine" 318 | } 319 | BestSellingVideoGame::WiiFitPlus => "Wii Fit / Plus", 320 | BestSellingVideoGame::WiiPlay => "Wii Play", 321 | BestSellingVideoGame::WiiSports => "Wii Sports", 322 | BestSellingVideoGame::WiiSportsResort => "Wii Sports Resort", 323 | } 324 | } 325 | pub const fn series(&self) -> &'static str { 326 | match self { 327 | BestSellingVideoGame::AnimalCrossingNewHorizons => "Animal Crossing", 328 | BestSellingVideoGame::Borderlands2 => "Borderlands", 329 | BestSellingVideoGame::CallOfDutyBlackOps => "Call of Duty", 330 | BestSellingVideoGame::CallOfDutyBlackOpsIi => "Call of Duty", 331 | BestSellingVideoGame::CallOfDutyModernWarfare => "Call of Duty", 332 | BestSellingVideoGame::CallOfDutyModernWarfare2 => "Call of Duty", 333 | BestSellingVideoGame::CallOfDutyModernWarfare3 => "Call of Duty", 334 | BestSellingVideoGame::DiabloIiiReaperOfSouls => "Diablo", 335 | BestSellingVideoGame::DuckHunt => "None", 336 | BestSellingVideoGame::Fifa18 => "FIFA", 337 | BestSellingVideoGame::GrandTheftAutoIv => "Grand Theft Auto", 338 | BestSellingVideoGame::GrandTheftAutoV => "Grand Theft Auto", 339 | BestSellingVideoGame::GrandTheftAutoSanAndreas => "Grand Theft Auto", 340 | BestSellingVideoGame::HumanFallFlat => "None", 341 | BestSellingVideoGame::KinectAdventures => "None", 342 | BestSellingVideoGame::MarioKart8Deluxe => "Mario Kart", 343 | BestSellingVideoGame::MarioKartDs => "Mario Kart", 344 | BestSellingVideoGame::MarioKartWii => "Mario Kart", 345 | BestSellingVideoGame::Minecraft => "Minecraft", 346 | BestSellingVideoGame::NewSuperMarioBros => "Super Mario", 347 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => "Super Mario", 348 | BestSellingVideoGame::NewSuperMarioBrosWii => "Super Mario", 349 | BestSellingVideoGame::Nintendogs => "None", 350 | BestSellingVideoGame::PacMan => "Pac-Man", 351 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => "Pokemon", 352 | BestSellingVideoGame::PokemonGoldSilverCrystal => "Pokemon", 353 | BestSellingVideoGame::PokemonRedGreenBlueYellow => "Pokemon", 354 | BestSellingVideoGame::PokemonRubySapphireEmerald => "Pokemon", 355 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => "Pokemon", 356 | BestSellingVideoGame::PokemonSwordShield => "Pokemon", 357 | BestSellingVideoGame::PubgBattlegrounds => "PUBG Universe", 358 | BestSellingVideoGame::RedDeadRedemption => "Red Dead", 359 | BestSellingVideoGame::RedDeadRedemption2 => "Red Dead", 360 | BestSellingVideoGame::SonicTheHedgehog => "Sonic the Hedgehog", 361 | BestSellingVideoGame::SuperMario64Ds => "Super Mario", 362 | BestSellingVideoGame::SuperMarioBros => "Super Mario", 363 | BestSellingVideoGame::SuperMarioBros3 => "Super Mario", 364 | BestSellingVideoGame::SuperMarioOdyssey => "Super Mario", 365 | BestSellingVideoGame::SuperMarioWorld => "Super Mario", 366 | BestSellingVideoGame::SuperSmashBrosUltimate => "Super Smash Bros.", 367 | BestSellingVideoGame::Terraria => "None", 368 | BestSellingVideoGame::TetrisEa => "Tetris", 369 | BestSellingVideoGame::TetrisNintendo => "Tetris", 370 | BestSellingVideoGame::TheElderScrollsVSkyrim => "The Elder Scrolls", 371 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => "The Legend of Zelda", 372 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => "The Witcher", 373 | BestSellingVideoGame::WiiFitPlus => "Wii", 374 | BestSellingVideoGame::WiiPlay => "Wii", 375 | BestSellingVideoGame::WiiSports => "Wii", 376 | BestSellingVideoGame::WiiSportsResort => "Wii", 377 | } 378 | } 379 | pub const fn platforms(&self) -> Platforms { 380 | match self { 381 | BestSellingVideoGame::AnimalCrossingNewHorizons => { 382 | Platforms::from_str("Nintendo Switch") 383 | } 384 | BestSellingVideoGame::Borderlands2 => Platforms::from_str("Multi-platform"), 385 | BestSellingVideoGame::CallOfDutyBlackOps => Platforms::from_str("Multi-platform"), 386 | BestSellingVideoGame::CallOfDutyBlackOpsIi => Platforms::from_str("Multi-platform"), 387 | BestSellingVideoGame::CallOfDutyModernWarfare => Platforms::from_str("Multi-platform"), 388 | BestSellingVideoGame::CallOfDutyModernWarfare2 => Platforms::from_str("Multi-platform"), 389 | BestSellingVideoGame::CallOfDutyModernWarfare3 => Platforms::from_str("Multi-platform"), 390 | BestSellingVideoGame::DiabloIiiReaperOfSouls => Platforms::from_str("Multi-platform"), 391 | BestSellingVideoGame::DuckHunt => Platforms::from_str("NES"), 392 | BestSellingVideoGame::Fifa18 => Platforms::from_str("Multi-platform"), 393 | BestSellingVideoGame::GrandTheftAutoIv => Platforms::from_str("Multi-platform"), 394 | BestSellingVideoGame::GrandTheftAutoV => Platforms::from_str("Multi-platform"), 395 | BestSellingVideoGame::GrandTheftAutoSanAndreas => Platforms::from_str("Multi-platform"), 396 | BestSellingVideoGame::HumanFallFlat => Platforms::from_str("Multi-platform"), 397 | BestSellingVideoGame::KinectAdventures => Platforms::from_str("Xbox 360"), 398 | BestSellingVideoGame::MarioKart8Deluxe => Platforms::from_str("Wii U / Switch"), 399 | BestSellingVideoGame::MarioKartDs => Platforms::from_str("Nintendo DS"), 400 | BestSellingVideoGame::MarioKartWii => Platforms::from_str("Wii"), 401 | BestSellingVideoGame::Minecraft => Platforms::from_str("Multi-platform"), 402 | BestSellingVideoGame::NewSuperMarioBros => Platforms::from_str("Nintendo DS"), 403 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => { 404 | Platforms::from_str("Wii U / Nintendo Switch") 405 | } 406 | BestSellingVideoGame::NewSuperMarioBrosWii => Platforms::from_str("Wii"), 407 | BestSellingVideoGame::Nintendogs => Platforms::from_str("Nintendo DS"), 408 | BestSellingVideoGame::PacMan => Platforms::from_str("Multi-platform"), 409 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => Platforms::from_str("Nintendo DS"), 410 | BestSellingVideoGame::PokemonGoldSilverCrystal => Platforms::from_str("Game Boy Color"), 411 | BestSellingVideoGame::PokemonRedGreenBlueYellow => { 412 | Platforms::from_str("Game Boy / Color") 413 | } 414 | BestSellingVideoGame::PokemonRubySapphireEmerald => { 415 | Platforms::from_str("Game Boy Advance") 416 | } 417 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => { 418 | Platforms::from_str("Nintendo 3DS") 419 | } 420 | BestSellingVideoGame::PokemonSwordShield => Platforms::from_str("Nintendo Switch"), 421 | BestSellingVideoGame::PubgBattlegrounds => Platforms::from_str("Multi-platform"), 422 | BestSellingVideoGame::RedDeadRedemption => Platforms::from_str("PS3 / Xbox 360"), 423 | BestSellingVideoGame::RedDeadRedemption2 => Platforms::from_str("Multi-platform"), 424 | BestSellingVideoGame::SonicTheHedgehog => Platforms::from_str("Multi-platform"), 425 | BestSellingVideoGame::SuperMario64Ds => Platforms::from_str("Nintendo 64 / DS"), 426 | BestSellingVideoGame::SuperMarioBros => Platforms::from_str("Multi-platform"), 427 | BestSellingVideoGame::SuperMarioBros3 => Platforms::from_str("Multi-platform"), 428 | BestSellingVideoGame::SuperMarioOdyssey => Platforms::from_str("Nintendo Switch"), 429 | BestSellingVideoGame::SuperMarioWorld => Platforms::from_str("Multi-platform"), 430 | BestSellingVideoGame::SuperSmashBrosUltimate => Platforms::from_str("Nintendo Switch"), 431 | BestSellingVideoGame::Terraria => Platforms::from_str("Multi-platform"), 432 | BestSellingVideoGame::TetrisEa => Platforms::from_str("Multi-platform"), 433 | BestSellingVideoGame::TetrisNintendo => Platforms::from_str("Game Boy / NES"), 434 | BestSellingVideoGame::TheElderScrollsVSkyrim => Platforms::from_str("Multi-platform"), 435 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => { 436 | Platforms::from_str("Wii U / Switch") 437 | } 438 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => { 439 | Platforms::from_str("Multi-platform") 440 | } 441 | BestSellingVideoGame::WiiFitPlus => Platforms::from_str("Wii"), 442 | BestSellingVideoGame::WiiPlay => Platforms::from_str("Wii"), 443 | BestSellingVideoGame::WiiSports => Platforms::from_str("Wii"), 444 | BestSellingVideoGame::WiiSportsResort => Platforms::from_str("Wii"), 445 | } 446 | } 447 | pub const fn publisher(&self) -> Publisher { 448 | match self { 449 | BestSellingVideoGame::AnimalCrossingNewHorizons => Publisher::from_str("Nintendo"), 450 | BestSellingVideoGame::Borderlands2 => Publisher::from_str("2K Games"), 451 | BestSellingVideoGame::CallOfDutyBlackOps => Publisher::from_str("Activision"), 452 | BestSellingVideoGame::CallOfDutyBlackOpsIi => Publisher::from_str("Activision"), 453 | BestSellingVideoGame::CallOfDutyModernWarfare => Publisher::from_str("Activision"), 454 | BestSellingVideoGame::CallOfDutyModernWarfare2 => Publisher::from_str("Activision"), 455 | BestSellingVideoGame::CallOfDutyModernWarfare3 => Publisher::from_str("Activision"), 456 | BestSellingVideoGame::DiabloIiiReaperOfSouls => { 457 | Publisher::from_str("Blizzard Entertainment") 458 | } 459 | BestSellingVideoGame::DuckHunt => Publisher::from_str("Nintendo"), 460 | BestSellingVideoGame::Fifa18 => Publisher::from_str("EA Sports"), 461 | BestSellingVideoGame::GrandTheftAutoIv => Publisher::from_str("Rockstar Games"), 462 | BestSellingVideoGame::GrandTheftAutoV => Publisher::from_str("Rockstar Games"), 463 | BestSellingVideoGame::GrandTheftAutoSanAndreas => Publisher::from_str("Rockstar Games"), 464 | BestSellingVideoGame::HumanFallFlat => Publisher::from_str("Curve Digital"), 465 | BestSellingVideoGame::KinectAdventures => Publisher::from_str("Xbox Game Studios"), 466 | BestSellingVideoGame::MarioKart8Deluxe => Publisher::from_str("Nintendo"), 467 | BestSellingVideoGame::MarioKartDs => Publisher::from_str("Nintendo"), 468 | BestSellingVideoGame::MarioKartWii => Publisher::from_str("Nintendo"), 469 | BestSellingVideoGame::Minecraft => Publisher::from_str("Xbox Game Studios"), 470 | BestSellingVideoGame::NewSuperMarioBros => Publisher::from_str("Nintendo"), 471 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => Publisher::from_str("Nintendo"), 472 | BestSellingVideoGame::NewSuperMarioBrosWii => Publisher::from_str("Nintendo"), 473 | BestSellingVideoGame::Nintendogs => Publisher::from_str("Nintendo"), 474 | BestSellingVideoGame::PacMan => Publisher::from_str("Namco"), 475 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => { 476 | Publisher::from_str("Nintendo / The Pokemon Company") 477 | } 478 | BestSellingVideoGame::PokemonGoldSilverCrystal => Publisher::from_str("Nintendo"), 479 | BestSellingVideoGame::PokemonRedGreenBlueYellow => Publisher::from_str("Nintendo"), 480 | BestSellingVideoGame::PokemonRubySapphireEmerald => { 481 | Publisher::from_str("Nintendo / The Pokemon Company") 482 | } 483 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => { 484 | Publisher::from_str("Nintendo / The Pokemon Company") 485 | } 486 | BestSellingVideoGame::PokemonSwordShield => { 487 | Publisher::from_str("Nintendo / The Pokemon Company") 488 | } 489 | BestSellingVideoGame::PubgBattlegrounds => Publisher::from_str("PUBG Corporation"), 490 | BestSellingVideoGame::RedDeadRedemption => Publisher::from_str("Rockstar Games"), 491 | BestSellingVideoGame::RedDeadRedemption2 => Publisher::from_str("Rockstar Games"), 492 | BestSellingVideoGame::SonicTheHedgehog => Publisher::from_str("Sega"), 493 | BestSellingVideoGame::SuperMario64Ds => Publisher::from_str("Nintendo"), 494 | BestSellingVideoGame::SuperMarioBros => Publisher::from_str("Nintendo"), 495 | BestSellingVideoGame::SuperMarioBros3 => Publisher::from_str("Nintendo"), 496 | BestSellingVideoGame::SuperMarioOdyssey => Publisher::from_str("Nintendo"), 497 | BestSellingVideoGame::SuperMarioWorld => Publisher::from_str("Nintendo"), 498 | BestSellingVideoGame::SuperSmashBrosUltimate => Publisher::from_str("Nintendo"), 499 | BestSellingVideoGame::Terraria => Publisher::from_str("Re-Logic / 505 Games"), 500 | BestSellingVideoGame::TetrisEa => Publisher::from_str("Electronic Arts"), 501 | BestSellingVideoGame::TetrisNintendo => Publisher::from_str("Nintendo"), 502 | BestSellingVideoGame::TheElderScrollsVSkyrim => { 503 | Publisher::from_str("Bethesda Softworks") 504 | } 505 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => { 506 | Publisher::from_str("Nintendo") 507 | } 508 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => { 509 | Publisher::from_str("CD Projekt") 510 | } 511 | BestSellingVideoGame::WiiFitPlus => Publisher::from_str("Nintendo"), 512 | BestSellingVideoGame::WiiPlay => Publisher::from_str("Nintendo"), 513 | BestSellingVideoGame::WiiSports => Publisher::from_str("Nintendo"), 514 | BestSellingVideoGame::WiiSportsResort => Publisher::from_str("Nintendo"), 515 | } 516 | } 517 | pub const fn developer(&self) -> Developer { 518 | match self { 519 | BestSellingVideoGame::AnimalCrossingNewHorizons => Developer::NintendoEpd, 520 | BestSellingVideoGame::Borderlands2 => Developer::GearboxSoftware, 521 | BestSellingVideoGame::CallOfDutyBlackOps => Developer::Treyarch, 522 | BestSellingVideoGame::CallOfDutyBlackOpsIi => Developer::Treyarch, 523 | BestSellingVideoGame::CallOfDutyModernWarfare => Developer::InfinityWard, 524 | BestSellingVideoGame::CallOfDutyModernWarfare2 => Developer::InfinityWard, 525 | BestSellingVideoGame::CallOfDutyModernWarfare3 => Developer::InfinityWardSledgehammer, 526 | BestSellingVideoGame::DiabloIiiReaperOfSouls => Developer::BlizzardEntertainment, 527 | BestSellingVideoGame::DuckHunt => Developer::NintendoRD1, 528 | BestSellingVideoGame::Fifa18 => Developer::EaVancouver, 529 | BestSellingVideoGame::GrandTheftAutoIv => Developer::RockstarNorth, 530 | BestSellingVideoGame::GrandTheftAutoV => Developer::RockstarNorth, 531 | BestSellingVideoGame::GrandTheftAutoSanAndreas => Developer::RockstarNorth, 532 | BestSellingVideoGame::HumanFallFlat => Developer::NoBrakesGames, 533 | BestSellingVideoGame::KinectAdventures => Developer::GoodScienceStudio, 534 | BestSellingVideoGame::MarioKart8Deluxe => Developer::NintendoEad, 535 | BestSellingVideoGame::MarioKartDs => Developer::NintendoEad, 536 | BestSellingVideoGame::MarioKartWii => Developer::NintendoEad, 537 | BestSellingVideoGame::Minecraft => Developer::MojangStudios, 538 | BestSellingVideoGame::NewSuperMarioBros => Developer::NintendoEad, 539 | BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU => Developer::NintendoEad, 540 | BestSellingVideoGame::NewSuperMarioBrosWii => Developer::NintendoEad, 541 | BestSellingVideoGame::Nintendogs => Developer::NintendoEad, 542 | BestSellingVideoGame::PacMan => Developer::Namco, 543 | BestSellingVideoGame::PokemonDiamondPearlPlatinum => Developer::GameFreak, 544 | BestSellingVideoGame::PokemonGoldSilverCrystal => Developer::GameFreak, 545 | BestSellingVideoGame::PokemonRedGreenBlueYellow => Developer::GameFreak, 546 | BestSellingVideoGame::PokemonRubySapphireEmerald => Developer::GameFreak, 547 | BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon => Developer::GameFreak, 548 | BestSellingVideoGame::PokemonSwordShield => Developer::GameFreak, 549 | BestSellingVideoGame::PubgBattlegrounds => Developer::PubgCorporation, 550 | BestSellingVideoGame::RedDeadRedemption => Developer::RockstarSanDiego, 551 | BestSellingVideoGame::RedDeadRedemption2 => Developer::RockstarStudios, 552 | BestSellingVideoGame::SonicTheHedgehog => Developer::SonicTeam, 553 | BestSellingVideoGame::SuperMario64Ds => Developer::NintendoEad, 554 | BestSellingVideoGame::SuperMarioBros => Developer::NintendoRD4, 555 | BestSellingVideoGame::SuperMarioBros3 => Developer::NintendoEad, 556 | BestSellingVideoGame::SuperMarioOdyssey => Developer::NintendoEpd, 557 | BestSellingVideoGame::SuperMarioWorld => Developer::NintendoEad, 558 | BestSellingVideoGame::SuperSmashBrosUltimate => Developer::BandaiNamcoStudiosSoraLtd, 559 | BestSellingVideoGame::Terraria => Developer::ReLogic, 560 | BestSellingVideoGame::TetrisEa => Developer::EaMobile, 561 | BestSellingVideoGame::TetrisNintendo => Developer::NintendoRD1, 562 | BestSellingVideoGame::TheElderScrollsVSkyrim => Developer::BethesdaGameStudios, 563 | BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild => Developer::NintendoEpd, 564 | BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine => Developer::CdProjektRed, 565 | BestSellingVideoGame::WiiFitPlus => Developer::NintendoEad, 566 | BestSellingVideoGame::WiiPlay => Developer::NintendoEad, 567 | BestSellingVideoGame::WiiSports => Developer::NintendoEad, 568 | BestSellingVideoGame::WiiSportsResort => Developer::NintendoEad, 569 | } 570 | } 571 | } 572 | 573 | impl<'a> TryFrom<&'a str> for BestSellingVideoGame { 574 | type Error = String; 575 | fn try_from(s: &'a str) -> Result { 576 | match s { 577 | "Animal Crossing: New Horizons" => Ok(BestSellingVideoGame::AnimalCrossingNewHorizons), 578 | "Borderlands 2" => Ok(BestSellingVideoGame::Borderlands2), 579 | "Call of Duty: Black Ops" => Ok(BestSellingVideoGame::CallOfDutyBlackOps), 580 | "Call of Duty: Black Ops II" => Ok(BestSellingVideoGame::CallOfDutyBlackOpsIi), 581 | "Call of Duty: Modern Warfare" => Ok(BestSellingVideoGame::CallOfDutyModernWarfare), 582 | "Call of Duty: Modern Warfare 2" => Ok(BestSellingVideoGame::CallOfDutyModernWarfare2), 583 | "Call of Duty: Modern Warfare 3" => Ok(BestSellingVideoGame::CallOfDutyModernWarfare3), 584 | "Diablo III / Reaper of Souls" => Ok(BestSellingVideoGame::DiabloIiiReaperOfSouls), 585 | "Duck Hunt" => Ok(BestSellingVideoGame::DuckHunt), 586 | "FIFA 18" => Ok(BestSellingVideoGame::Fifa18), 587 | "Grand Theft Auto IV" => Ok(BestSellingVideoGame::GrandTheftAutoIv), 588 | "Grand Theft Auto V" => Ok(BestSellingVideoGame::GrandTheftAutoV), 589 | "Grand Theft Auto: San Andreas" => Ok(BestSellingVideoGame::GrandTheftAutoSanAndreas), 590 | "Human: Fall Flat" => Ok(BestSellingVideoGame::HumanFallFlat), 591 | "Kinect Adventures!" => Ok(BestSellingVideoGame::KinectAdventures), 592 | "Mario Kart 8 / Deluxe" => Ok(BestSellingVideoGame::MarioKart8Deluxe), 593 | "Mario Kart DS" => Ok(BestSellingVideoGame::MarioKartDs), 594 | "Mario Kart Wii" => Ok(BestSellingVideoGame::MarioKartWii), 595 | "Minecraft" => Ok(BestSellingVideoGame::Minecraft), 596 | "New Super Mario Bros." => Ok(BestSellingVideoGame::NewSuperMarioBros), 597 | "New Super Mario Bros. U / Deluxe / Luigi U" => { 598 | Ok(BestSellingVideoGame::NewSuperMarioBrosUDeluxeLuigiU) 599 | } 600 | "New Super Mario Bros. Wii" => Ok(BestSellingVideoGame::NewSuperMarioBrosWii), 601 | "Nintendogs" => Ok(BestSellingVideoGame::Nintendogs), 602 | "Pac-Man" => Ok(BestSellingVideoGame::PacMan), 603 | "Pokemon Diamond / Pearl / Platinum" => { 604 | Ok(BestSellingVideoGame::PokemonDiamondPearlPlatinum) 605 | } 606 | "Pokemon Gold / Silver / Crystal" => Ok(BestSellingVideoGame::PokemonGoldSilverCrystal), 607 | "Pokemon Red / Green / Blue / Yellow" => { 608 | Ok(BestSellingVideoGame::PokemonRedGreenBlueYellow) 609 | } 610 | "Pokemon Ruby / Sapphire / Emerald" => { 611 | Ok(BestSellingVideoGame::PokemonRubySapphireEmerald) 612 | } 613 | "Pokemon Sun / Moon / Ultra Sun / Ultra Moon" => { 614 | Ok(BestSellingVideoGame::PokemonSunMoonUltraSunUltraMoon) 615 | } 616 | "Pokemon Sword / Shield" => Ok(BestSellingVideoGame::PokemonSwordShield), 617 | "PUBG: Battlegrounds" => Ok(BestSellingVideoGame::PubgBattlegrounds), 618 | "Red Dead Redemption" => Ok(BestSellingVideoGame::RedDeadRedemption), 619 | "Red Dead Redemption 2" => Ok(BestSellingVideoGame::RedDeadRedemption2), 620 | "Sonic the Hedgehog" => Ok(BestSellingVideoGame::SonicTheHedgehog), 621 | "Super Mario 64 / DS" => Ok(BestSellingVideoGame::SuperMario64Ds), 622 | "Super Mario Bros." => Ok(BestSellingVideoGame::SuperMarioBros), 623 | "Super Mario Bros. 3" => Ok(BestSellingVideoGame::SuperMarioBros3), 624 | "Super Mario Odyssey" => Ok(BestSellingVideoGame::SuperMarioOdyssey), 625 | "Super Mario World" => Ok(BestSellingVideoGame::SuperMarioWorld), 626 | "Super Smash Bros. Ultimate" => Ok(BestSellingVideoGame::SuperSmashBrosUltimate), 627 | "Terraria" => Ok(BestSellingVideoGame::Terraria), 628 | "Tetris (EA)" => Ok(BestSellingVideoGame::TetrisEa), 629 | "Tetris (Nintendo)" => Ok(BestSellingVideoGame::TetrisNintendo), 630 | "The Elder Scrolls V: Skyrim" => Ok(BestSellingVideoGame::TheElderScrollsVSkyrim), 631 | "The Legend of Zelda: Breath of the Wild" => { 632 | Ok(BestSellingVideoGame::TheLegendOfZeldaBreathOfTheWild) 633 | } 634 | "The Witcher 3 / Hearts of Stone / Blood and Wine" => { 635 | Ok(BestSellingVideoGame::TheWitcher3HeartsOfStoneBloodAndWine) 636 | } 637 | "Wii Fit / Plus" => Ok(BestSellingVideoGame::WiiFitPlus), 638 | "Wii Play" => Ok(BestSellingVideoGame::WiiPlay), 639 | "Wii Sports" => Ok(BestSellingVideoGame::WiiSports), 640 | "Wii Sports Resort" => Ok(BestSellingVideoGame::WiiSportsResort), 641 | _ => Err(format!("Unknown BestSellingVideoGame {}", s)), 642 | } 643 | } 644 | } 645 | ``` 646 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = { version = "1.0.51", default-features = false } 12 | quote = "1.0.23" 13 | sea-orm = { version = "1.0.0", features = [ 14 | "sqlx-mysql", 15 | "runtime-tokio-rustls", 16 | ] } 17 | serde = { version = "1.0.152", features = ["derive"] } 18 | syn = { version = "1.0.109", features = ["full"] } 19 | symbols = "1.0.0" 20 | tokio = "1.25.0" 21 | -------------------------------------------------------------------------------- /example/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | rust: 4 | image: "rust:latest" 5 | restart: "no" 6 | depends_on: 7 | - mariadb 8 | working_dir: /app/example 9 | volumes: 10 | - ..:/app 11 | - /app/example/target 12 | environment: 13 | DATABASE_URL: mysql://mariadb:mariadb@mariadb/example 14 | command: "cargo test" 15 | mariadb: 16 | image: "mariadb:latest" 17 | restart: "no" 18 | environment: 19 | MARIADB_ROOT_PASSWORD: reallystrongpassword 20 | MARIADB_USER: mariadb 21 | MARIADB_PASSWORD: mariadb 22 | MARIADB_DATABASE: example 23 | volumes: 24 | - ./fixtures/mariadb:/docker-entrypoint-initdb.d 25 | -------------------------------------------------------------------------------- /example/fixtures/mariadb/schema.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE IF EXISTS `best_selling_video_games`; 3 | 4 | CREATE TABLE `best_selling_video_games` ( 5 | `rank` tinyint(2) NOT NULL, 6 | `name` varchar(255) NOT NULL, 7 | `sales` varchar(255) NOT NULL, 8 | `series` varchar(255) NOT NULL, 9 | `platforms` varchar(255) NOT NULL, 10 | `initial_release_date` varchar(255) NOT NULL, 11 | `developer` varchar(255) NOT NULL, 12 | `publisher` varchar(255) NOT NULL, 13 | PRIMARY KEY (`name`) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 15 | 16 | -- list taken from https://en.wikipedia.org/wiki/List_of_best-selling_video_games 17 | -- normalized with regexp ^(\d+)\s+\t([^\t]+)\s+\t([\d,]+)\s+\t([^\t]+)\s+\t([^\t]+)\s+\t([^\t]+)\s+\t([^\t]+)\s+\t([^\t]+)\s+\t$ 18 | INSERT INTO `best_selling_video_games` VALUES 19 | (1, 'Minecraft', '238,000,000', 'Minecraft', 'Multi-platform', 'November 18, 2011', 'Mojang Studios', 'Xbox Game Studios'), 20 | (2, 'Grand Theft Auto V', '160,000,000', 'Grand Theft Auto', 'Multi-platform', 'September 17, 2013', 'Rockstar North', 'Rockstar Games'), 21 | (3, 'Tetris (EA)', '100,000,000', 'Tetris', 'Multi-platform', 'September 12, 2006', 'EA Mobile', 'Electronic Arts'), 22 | (4, 'Wii Sports', '82,900,000', 'Wii', 'Wii', 'November 19, 2006', 'Nintendo EAD', 'Nintendo'), 23 | (5, 'PUBG: Battlegrounds', '75,000,000', 'PUBG Universe', 'Multi-platform', 'December 20, 2017', 'PUBG Corporation', 'PUBG Corporation'), 24 | (6, 'Super Mario Bros.', '58,000,000', 'Super Mario', 'Multi-platform', 'September 13, 1985', 'Nintendo R&D4', 'Nintendo'), 25 | (7, 'Mario Kart 8 / Deluxe', '51,810,000', 'Mario Kart', 'Wii U / Switch', 'May 29, 2014', 'Nintendo EAD', 'Nintendo'), 26 | (8, 'Pokemon Red / Green / Blue / Yellow', '47,520,000', 'Pokemon', 'Game Boy / Color', 'February 27, 1996', 'Game Freak', 'Nintendo'), 27 | (9, 'Terraria', '44,000,000', 'None', 'Multi-platform', 'May 16, 2011', 'Re-Logic', 'Re-Logic / 505 Games'), 28 | (10, 'Wii Fit / Plus', '43,800,000', 'Wii', 'Wii', 'December 1, 2007', 'Nintendo EAD', 'Nintendo'), 29 | (11, 'Red Dead Redemption 2', '43,000,000', 'Red Dead', 'Multi-platform', 'October 26, 2018', 'Rockstar Studios', 'Rockstar Games'), 30 | (11, 'Tetris (Nintendo)', '43,000,000', 'Tetris', 'Game Boy / NES', 'June 14, 1989', 'Nintendo R&D1', 'Nintendo'), 31 | (13, 'Pac-Man', '42,071,635', 'Pac-Man', 'Multi-platform', 'May 22, 1980', 'Namco', 'Namco'), 32 | (14, 'The Witcher 3 / Hearts of Stone / Blood and Wine', '40,000,000', 'The Witcher', 'Multi-platform', 'May 19, 2015', 'CD Projekt Red', 'CD Projekt'), 33 | (15, 'Animal Crossing: New Horizons', '37,620,000', 'Animal Crossing', 'Nintendo Switch', 'March 20, 2020', 'Nintendo EPD', 'Nintendo'), 34 | (16, 'Mario Kart Wii', '37,380,000', 'Mario Kart', 'Wii', 'April 10, 2008', 'Nintendo EAD', 'Nintendo'), 35 | (17, 'Wii Sports Resort', '33,140,000', 'Wii', 'Wii', 'June 25, 2009', 'Nintendo EAD', 'Nintendo'), 36 | (18, 'New Super Mario Bros.', '30,800,000', 'Super Mario', 'Nintendo DS', 'May 15, 2006', 'Nintendo EAD', 'Nintendo'), 37 | (19, 'New Super Mario Bros. Wii', '30,320,000', 'Super Mario', 'Wii', 'November 11, 2009', 'Nintendo EAD', 'Nintendo'), 38 | (20, 'The Elder Scrolls V: Skyrim', '30,000,000', 'The Elder Scrolls', 'Multi-platform', 'November 11, 2011', 'Bethesda Game Studios', 'Bethesda Softworks'), 39 | (20, 'Call of Duty: Modern Warfare', '30,000,000', 'Call of Duty', 'Multi-platform', 'October 25, 2019', 'Infinity Ward', 'Activision'), 40 | (20, 'Diablo III / Reaper of Souls', '30,000,000', 'Diablo', 'Multi-platform', 'May 16, 2012', 'Blizzard Entertainment', 'Blizzard Entertainment'), 41 | (20, 'Human: Fall Flat', '30,000,000', 'None', 'Multi-platform', 'July 22, 2016', 'No Brakes Games', 'Curve Digital'), 42 | (24, 'Pokemon Gold / Silver / Crystal', '29,490,000', 'Pokemon', 'Game Boy Color', 'November 21, 1999', 'Game Freak', 'Nintendo'), 43 | (25, 'Duck Hunt', '28,300,000', 'None', 'NES', 'April 21, 1984', 'Nintendo R&D1', 'Nintendo'), 44 | (26, 'Wii Play', '28,020,000', 'Wii', 'Wii', 'December 2, 2006', 'Nintendo EAD', 'Nintendo'), 45 | (27, 'Grand Theft Auto: San Andreas', '27,500,000', 'Grand Theft Auto', 'Multi-platform', 'October 26, 2004', 'Rockstar North', 'Rockstar Games'), 46 | (28, 'The Legend of Zelda: Breath of the Wild', '27,490,000', 'The Legend of Zelda', 'Wii U / Switch', 'March 3, 2017', 'Nintendo EPD', 'Nintendo'), 47 | (29, 'Super Smash Bros. Ultimate', '27,400,000', 'Super Smash Bros.', 'Nintendo Switch', 'December 7, 2018', 'Bandai Namco Studios / Sora Ltd.', 'Nintendo'), 48 | (30, 'Super Mario World', '26,662,500', 'Super Mario', 'Multi-platform', 'November 21, 1990', 'Nintendo EAD', 'Nintendo'), 49 | (31, 'Call of Duty: Modern Warfare 3', '26,500,000', 'Call of Duty', 'Multi-platform', 'November 8, 2011', 'Infinity Ward / Sledgehammer', 'Activision'), 50 | (32, 'Call of Duty: Black Ops', '26,200,000', 'Call of Duty', 'Multi-platform', 'November 9, 2010', 'Treyarch', 'Activision'), 51 | (33, 'Borderlands 2', '26,000,000', 'Borderlands', 'Multi-platform', 'September 18, 2012', 'Gearbox Software', '2K Games'), 52 | (34, 'Pokemon Sun / Moon / Ultra Sun / Ultra Moon', '25,310,000', 'Pokemon', 'Nintendo 3DS', 'November 18, 2016', 'Game Freak', 'Nintendo / The Pokemon Company'), 53 | (35, 'Grand Theft Auto IV', '25,000,000', 'Grand Theft Auto', 'Multi-platform', 'April 29, 2008', 'Rockstar North', 'Rockstar Games'), 54 | (36, 'Pokemon Diamond / Pearl / Platinum', '24,730,000', 'Pokemon', 'Nintendo DS', 'September 28, 2006', 'Game Freak', 'Nintendo / The Pokemon Company'), 55 | (37, 'Super Mario Bros. 3', '24,430,000', 'Super Mario', 'Multi-platform', 'October 23, 1988', 'Nintendo EAD', 'Nintendo'), 56 | (38, 'Call of Duty: Black Ops II', '24,200,000', 'Call of Duty', 'Multi-platform', 'November 12, 2012', 'Treyarch', 'Activision'), 57 | (39, 'Kinect Adventures!', '24,000,000', 'None', 'Xbox 360', 'November 4, 2010', 'Good Science Studio', 'Xbox Game Studios'), 58 | (39, 'FIFA 18', '24,000,000', 'FIFA', 'Multi-platform', 'September 29, 2017', 'EA Vancouver', 'EA Sports'), 59 | (41, 'Sonic the Hedgehog', '23,982,960', 'Sonic the Hedgehog', 'Multi-platform', 'June 23, 1991', 'Sonic Team', 'Sega'), 60 | (42, 'Nintendogs', '23,960,000', 'None', 'Nintendo DS', 'April 21, 2005', 'Nintendo EAD', 'Nintendo'), 61 | (43, 'Pokemon Sword / Shield', '23,900,000', 'Pokemon', 'Nintendo Switch', 'November 15, 2019', 'Game Freak', 'Nintendo / The Pokemon Company'), 62 | (44, 'Mario Kart DS', '23,600,000', 'Mario Kart', 'Nintendo DS', 'November 14, 2005', 'Nintendo EAD', 'Nintendo'), 63 | (45, 'Super Mario Odyssey', '23,020,000', 'Super Mario', 'Nintendo Switch', 'October 27, 2017', 'Nintendo EPD', 'Nintendo'), 64 | (46, 'Red Dead Redemption', '23,000,000', 'Red Dead', 'PS3 / Xbox 360', 'May 18, 2010', 'Rockstar San Diego', 'Rockstar Games'), 65 | (47, 'Super Mario 64 / DS', '22,960,000', 'Super Mario', 'Nintendo 64 / DS', 'June 23, 1996', 'Nintendo EAD', 'Nintendo'), 66 | (48, 'Call of Duty: Modern Warfare 2', '22,700,000', 'Call of Duty', 'Multi-platform', 'November 10, 2009', 'Infinity Ward', 'Activision'), 67 | (49, 'Pokemon Ruby / Sapphire / Emerald', '22,540,000', 'Pokemon', 'Game Boy Advance', 'November 21, 2002', 'Game Freak', 'Nintendo / The Pokemon Company'), 68 | (50, 'New Super Mario Bros. U / Deluxe / Luigi U', '21,600,000', 'Super Mario', 'Wii U / Nintendo Switch', 'November 18, 2012', 'Nintendo EAD', 'Nintendo'); 69 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | 5 | use sea_orm::{ConnectOptions, Database, DatabaseConnection}; 6 | 7 | use syn::{parse_macro_input, AttributeArgs, ItemEnum, Lit, Meta, NestedMeta}; 8 | 9 | use symbols::symbols; 10 | 11 | mod model; 12 | 13 | #[proc_macro_attribute] 14 | pub fn example( 15 | args: proc_macro::TokenStream, 16 | input: proc_macro::TokenStream, 17 | ) -> proc_macro::TokenStream { 18 | let mut item = parse_macro_input!(input as ItemEnum); 19 | let args = parse_macro_input!(args as AttributeArgs); 20 | 21 | get_enum(&mut item, &args) 22 | .unwrap_or_else(syn::Error::into_compile_error) 23 | .into() 24 | } 25 | 26 | async fn get_conn() -> syn::Result { 27 | let url = std::env::var("DATABASE_URL").map_err(|e| syn::Error::new(Span::call_site(), e))?; 28 | let mut options = ConnectOptions::new(url); 29 | options 30 | .min_connections(1) 31 | .max_connections(1) 32 | .connect_timeout(Duration::from_secs(1)) 33 | .idle_timeout(Duration::from_secs(1)); 34 | Database::connect(options) 35 | .await 36 | .map_err(|e| syn::Error::new(Span::call_site(), e)) 37 | } 38 | 39 | fn get_enum(item: &mut ItemEnum, args: &[NestedMeta]) -> syn::Result { 40 | // search for table, simply #[macro(table = "table_name")] 41 | let table = args 42 | .iter() 43 | .find_map(|arg| { 44 | if let NestedMeta::Meta(Meta::NameValue(mv)) = arg { 45 | if mv.path.is_ident("table") { 46 | if let Lit::Str(s) = &mv.lit { 47 | return Some(s.value()); 48 | } 49 | } 50 | } 51 | None 52 | }) 53 | .ok_or_else(|| syn::Error::new(Span::call_site(), "Missing table attribute"))?; 54 | 55 | // start an async runtime to be able to use sea-orm 56 | tokio::runtime::Builder::new_current_thread() 57 | .enable_all() 58 | .build() 59 | .unwrap() 60 | .block_on(async move { 61 | match table.as_str() { 62 | "best_selling_video_games" => { 63 | symbols::(item, args, get_conn).await 64 | } 65 | _ => Err(syn::Error::new( 66 | Span::call_site(), 67 | format!("Unrecognized table \"{}\"", table), 68 | )), 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /example/src/model.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use symbols::EntityFilter; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] 8 | #[sea_orm(table_name = "best_selling_video_games")] 9 | pub struct Model { 10 | pub rank: i8, 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub name: String, 13 | pub sales: String, 14 | pub series: String, 15 | pub platforms: String, 16 | pub initial_release_date: String, 17 | pub developer: String, 18 | pub publisher: String, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 22 | pub enum Relation {} 23 | 24 | impl ActiveModelBehavior for ActiveModel {} 25 | 26 | impl EntityFilter for Entity {} 27 | 28 | impl PartialEq for Column { 29 | fn eq(&self, other: &Self) -> bool { 30 | matches!( 31 | (self, other), 32 | (Column::Rank, Column::Rank) 33 | | (Column::Name, Column::Name) 34 | | (Column::Sales, Column::Sales) 35 | | (Column::Series, Column::Series) 36 | | (Column::Platforms, Column::Platforms) 37 | | (Column::InitialReleaseDate, Column::InitialReleaseDate) 38 | | (Column::Developer, Column::Developer) 39 | | (Column::Publisher, Column::Publisher) 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/tests/it_works.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use sea_orm::{EnumIter, Iterable}; 4 | 5 | #[example::example( 6 | table = "best_selling_video_games", 7 | platforms(type = "Platforms", fn = "from_str"), 8 | developer = "Developer", 9 | publisher(type = "Publisher", fn = "from_str") 10 | )] 11 | #[derive(Debug, EnumIter)] 12 | pub enum BestSellingVideoGame {} 13 | 14 | #[derive(Debug)] 15 | pub enum Platform { 16 | MultiPlatform, 17 | NintendoSwitch, 18 | Nes, 19 | Xbox360, 20 | NintendoDs, 21 | Wii, 22 | WiiU, 23 | Ps2, 24 | GameBoyColor, 25 | GameBoyAdvance, 26 | Nintendo3ds, 27 | Nintendo64, 28 | GameBoy, 29 | } 30 | 31 | pub struct Platforms(&'static [Platform]); 32 | 33 | impl Platforms { 34 | const fn from_str(s: &'static str) -> Self { 35 | match s.as_bytes() { 36 | b"Game Boy Advance" => Platforms(&[Platform::GameBoyAdvance]), 37 | b"Game Boy / Color" => Platforms(&[Platform::GameBoy, Platform::GameBoyColor]), 38 | b"Game Boy Color" => Platforms(&[Platform::GameBoyColor]), 39 | b"Game Boy / NES" => Platforms(&[Platform::GameBoy, Platform::Nes]), 40 | b"Multi-platform" => Platforms(&[Platform::MultiPlatform]), 41 | b"NES" => Platforms(&[Platform::Nes]), 42 | b"Nintendo 3DS" => Platforms(&[Platform::Nintendo3ds]), 43 | b"Nintendo 64 / DS" => Platforms(&[Platform::Nintendo64, Platform::NintendoDs]), 44 | b"Nintendo DS" => Platforms(&[Platform::NintendoDs]), 45 | b"Nintendo Switch" => Platforms(&[Platform::NintendoSwitch]), 46 | b"PS3 / Xbox 360" => Platforms(&[Platform::Ps2, Platform::Xbox360]), 47 | b"Wii" => Platforms(&[Platform::Wii]), 48 | b"Wii U / Nintendo Switch" | b"Wii U / Switch" => { 49 | Platforms(&[Platform::WiiU, Platform::NintendoSwitch]) 50 | } 51 | b"Xbox 360" => Platforms(&[Platform::Xbox360]), 52 | _ => unreachable!(), 53 | } 54 | } 55 | } 56 | 57 | impl fmt::Debug for Platforms { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | write!(f, "{:?}", self.0) 60 | } 61 | } 62 | 63 | #[derive(Debug)] 64 | pub enum Developer { 65 | BandaiNamcoStudiosSoraLtd, 66 | BethesdaGameStudios, 67 | BlizzardEntertainment, 68 | CdProjektRed, 69 | EaMobile, 70 | EaVancouver, 71 | GameFreak, 72 | GearboxSoftware, 73 | GoodScienceStudio, 74 | InfinityWard, 75 | InfinityWardSledgehammer, 76 | MojangStudios, 77 | Namco, 78 | NintendoEad, 79 | NintendoEpd, 80 | NintendoRD1, 81 | NintendoRD4, 82 | NoBrakesGames, 83 | PubgCorporation, 84 | ReLogic, 85 | RockstarNorth, 86 | RockstarSanDiego, 87 | RockstarStudios, 88 | SonicTeam, 89 | Treyarch, 90 | } 91 | 92 | #[derive(Debug)] 93 | pub enum Publisher { 94 | TwoKGames, 95 | Activision, 96 | BethesdaSoftworks, 97 | BlizzardEntertainment, 98 | CdProjekt, 99 | CurveDigital, 100 | EaSports, 101 | ElectronicArts, 102 | Namco, 103 | Nintendo, 104 | NintendoThePokemonCompany, 105 | PubgCorporation, 106 | ReLogic505Games, 107 | RockstarGames, 108 | Sega, 109 | XboxGameStudios, 110 | } 111 | 112 | impl Publisher { 113 | const fn from_str(s: &'static str) -> Self { 114 | match s.as_bytes() { 115 | b"2K Games" => Publisher::TwoKGames, 116 | b"Activision" => Publisher::Activision, 117 | b"Bethesda Softworks" => Publisher::BethesdaSoftworks, 118 | b"Blizzard Entertainment" => Publisher::BlizzardEntertainment, 119 | b"CD Projekt" => Publisher::CdProjekt, 120 | b"Curve Digital" => Publisher::CurveDigital, 121 | b"EA Sports" => Publisher::EaSports, 122 | b"Electronic Arts" => Publisher::ElectronicArts, 123 | b"Namco" => Publisher::Namco, 124 | b"Nintendo" => Publisher::Nintendo, 125 | b"Nintendo / The Pokemon Company" => Publisher::NintendoThePokemonCompany, 126 | b"PUBG Corporation" => Publisher::PubgCorporation, 127 | b"Re-Logic / 505 Games" => Publisher::ReLogic505Games, 128 | b"Rockstar Games" => Publisher::RockstarGames, 129 | b"Sega" => Publisher::Sega, 130 | b"Xbox Game Studios" => Publisher::XboxGameStudios, 131 | _ => unreachable!(), 132 | } 133 | } 134 | } 135 | 136 | #[test] 137 | fn it_works() { 138 | for game in BestSellingVideoGame::iter() { 139 | println!( 140 | "rank: {}, name: {} sales: {} series: {} platforms: {:?} initial release date: {} developer: {:?} publisher: {:?}", 141 | game.rank(), 142 | game.as_str(), 143 | game.sales(), 144 | game.series(), 145 | game.platforms(), 146 | game.initial_release_date(), 147 | game.developer(), 148 | game.publisher() 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /img/meme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nappa85/symbols/48a529ab99ce9a1e9adfe18ffad0f98a31d7d2e6/img/meme.jpg -------------------------------------------------------------------------------- /symbols-models/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "symbols-models" 3 | version = "1.0.0" 4 | edition = "2021" 5 | authors = ["Marco Napetti"] 6 | repository = "https://github.com/nappa85/symbols" 7 | documentation = "https://docs.rs/symbols-models" 8 | readme = "../README.md" 9 | license = "WTFPL" 10 | description = "Proc-macro utility to populate enums from database data (shared traits)" 11 | categories = [ 12 | "data-structures", 13 | "development-tools::procedural-macro-helpers", 14 | "database", 15 | ] 16 | keywords = ["proc-macro", "enum", "database"] 17 | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 19 | 20 | [dependencies] 21 | sea-orm = "1.0.0" 22 | -------------------------------------------------------------------------------- /symbols-models/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #![deny(missing_docs)] 3 | 4 | //! # Symbols-models 5 | //! 6 | //! Shared traits from Symbols proc-macro-utility, to be able to share models between macros and real applications. 7 | 8 | use sea_orm::{ 9 | sea_query::{Expr, SimpleExpr}, 10 | EntityTrait, 11 | }; 12 | 13 | /// This trait allows data filtering on macro execution 14 | /// It's default implementation simply adds WHERE 1 = 1 to data retrieve query 15 | /// 16 | /// Since only basic types are supported, it's important to use only basic types in models. 17 | pub trait EntityFilter: EntityTrait + Default { 18 | /// Returned expression in injected in data retrieve query to allow data filtering 19 | fn filter() -> SimpleExpr { 20 | Expr::val(1).eq(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /symbols/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "symbols" 3 | version = "1.0.0" 4 | edition = "2021" 5 | authors = ["Marco Napetti"] 6 | repository = "https://github.com/nappa85/symbols" 7 | documentation = "https://docs.rs/symbols" 8 | readme = "../README.md" 9 | license = "WTFPL" 10 | description = "Proc-macro utility to populate enums from database data" 11 | categories = [ 12 | "data-structures", 13 | "development-tools::procedural-macro-helpers", 14 | "database", 15 | ] 16 | keywords = ["proc-macro", "enum", "database"] 17 | 18 | [dependencies] 19 | bincode = "1.3.3" 20 | heck = "0.5.0" 21 | itertools = "0.13.0" 22 | proc-macro2 = { version = "1.0.51", default-features = false } 23 | quote = "1.0.23" 24 | sea-orm = "1.0.0" 25 | serde = "1.0.152" 26 | syn = { version = "1.0.109", features = ["full"] } 27 | symbols-models = "1.0.0" 28 | tracing = "0.1.37" 29 | -------------------------------------------------------------------------------- /symbols/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #![deny(missing_docs)] 3 | 4 | //! # Symbols 5 | //! 6 | //! This is an utility to build a proc-macro that connects to a database, retrieves data from given table and populates an enum variants with primary keys values 7 | //! It also generates a method for every non-primary-key field, and, when there are multiple primary keys, a costructor for every possible subset of primary keys 8 | 9 | use std::{collections::HashMap, env, fs, future::Future, io}; 10 | 11 | use heck::{ToSnakeCase, ToUpperCamelCase}; 12 | 13 | use itertools::Itertools; 14 | 15 | use proc_macro2::{Ident, Literal, Span, TokenStream}; 16 | 17 | use quote::quote; 18 | 19 | use sea_orm::{ 20 | DatabaseConnection, EntityName, EntityTrait, Iterable, ModelTrait, PrimaryKeyToColumn, QueryFilter, Value, 21 | }; 22 | 23 | use serde::{de::DeserializeOwned, Serialize}; 24 | 25 | pub use symbols_models::EntityFilter; 26 | 27 | use syn::{punctuated::Punctuated, token::Comma, Fields, ItemEnum, Lit, LitBool, Meta, NestedMeta, Variant}; 28 | 29 | use tracing::{error, info}; 30 | 31 | /// Main function 32 | /// Given a database model (via generics), an enum item, a list of arguments and an async function to retrieve a database connection 33 | /// it populates the enum using primary key(s) values. 34 | /// Only string-typed primary keys are supported. 35 | /// 36 | /// When a single primary key is present, it simply generate an as_str method and a TryFrom<&str> implementation. 37 | /// When multiple primary keys are present, it generates a costructor for every possible subset of primary keys. 38 | /// 39 | /// For every non-primary key field of a supported type, it generates a const method to retrieve it. 40 | /// 41 | /// Replacements can be done on every string-typed field, even primary keys, and are done using annotated parameters. 42 | /// Two type of replacements are supported: 43 | /// * basic: written in the form #[macro(field = "enum")] or #[macro(field(type = "enum"))], where we are telling to replace string values from `field` with variants from enum `enum`, variant names will be the CamelCase version of field value. 44 | /// * advanced: written in the form #[macro(field(type = "bar", fn = "foo"))], where we are telling to replace string values from `field` with a call to method `foo` from struct/enum `bar`, method output is expected to be of type `bar`. 45 | pub async fn symbols(item: &mut ItemEnum, args: &[NestedMeta], get_conn: F) -> syn::Result 46 | where 47 | M: EntityTrait + EntityFilter + Default, 48 | ::Model: Serialize + DeserializeOwned, 49 | ::Column: PartialEq, 50 | F: Fn() -> Fut, 51 | Fut: Future>, 52 | { 53 | let name = &item.ident; 54 | let primary_keys = ::PrimaryKey::iter().map(|k| k.into_column()).collect::>(); 55 | 56 | let mut constructors = HashMap::new(); 57 | let mut methods = HashMap::new(); 58 | 59 | let data = get_data::(get_conn).await?; 60 | 61 | data.iter().try_for_each(|v| { 62 | let mut key_s = vec![]; 63 | 64 | // scan primary keys 65 | for k in &primary_keys { 66 | let val = v.get(*k); 67 | // only string values are accepted 68 | if let Value::String(Some(s)) = val { 69 | key_s.push(s.to_upper_camel_case()); 70 | 71 | // if we have a single primary key, create a method as_str and a counter-trait-impl TryFrom<&str> 72 | if primary_keys.len() == 1 { 73 | let key = Ident::new(&s.to_upper_camel_case(), Span::call_site()); 74 | let v = Literal::string(s.as_str()); 75 | 76 | let (_, method, _) = methods 77 | .entry(String::from("as_str")) 78 | .or_insert_with(|| (quote! { &'static str }, Punctuated::<_, Comma>::new(), false)); 79 | method.push(quote! { 80 | #name::#key => #v 81 | }); 82 | 83 | let (_, method, _) = methods 84 | .entry(String::from("try_from")) 85 | .or_insert_with(|| (quote! { () }, Punctuated::<_, Comma>::new(), false)); 86 | method.push(quote! { 87 | #v => Ok(#name::#key) 88 | }); 89 | } 90 | } else { 91 | return Err(syn::Error::new(Span::call_site(), format!("Unrecognized value type {val:?}"))); 92 | } 93 | } 94 | // push primary keys into enum variants 95 | let key_ident = Ident::new(&key_s.join("_"), Span::call_site()); 96 | item.variants.push(Variant { 97 | attrs: vec![], 98 | ident: key_ident.clone(), 99 | fields: Fields::Unit, 100 | discriminant: None, 101 | }); 102 | // generate constructors for every combination of primary keys 103 | if primary_keys.len() > 1 { 104 | for n in 1..=primary_keys.len() { 105 | for combo in primary_keys.iter().enumerate().combinations(n) { 106 | let cols = combo.iter().map(|(_, col)| **col).collect::>(); 107 | let method = combo 108 | .iter() 109 | .map(|(_, col)| format!("{col:?}").to_snake_case()) 110 | .collect::>() 111 | .join("_and_"); 112 | let key = combo.iter().map(|(index, _)| key_s[*index].clone()).collect::>(); 113 | let (_, method) = constructors.entry(method).or_insert_with(|| (cols, HashMap::new())); 114 | let (_, idents) = 115 | method.entry(key.join("_")).or_insert_with(|| (key, Punctuated::<_, Comma>::new())); 116 | idents.push(quote! { #name::#key_ident }); 117 | } 118 | } 119 | } 120 | 121 | // create a method for every non-primary_key column 122 | for col in ::Column::iter() { 123 | let replace = get_replacement::(col, args); 124 | 125 | // skip self-describing methods (would be an as_str clone) 126 | if primary_keys.len() == 1 && primary_keys.contains(&col) && replace.is_none() { 127 | continue; 128 | } 129 | 130 | // keep only managed data types 131 | let (t, value) = match v.get(col) { 132 | Value::Bool(b) => ( 133 | quote! { bool }, 134 | b.map(|b| { 135 | let v = LitBool::new(b, Span::call_site()); 136 | quote! { #v } 137 | }), 138 | ), 139 | Value::TinyInt(n) => ( 140 | quote! { i8 }, 141 | n.map(|n| { 142 | let v = Literal::i8_unsuffixed(n); 143 | quote! { #v } 144 | }), 145 | ), 146 | Value::SmallInt(n) => ( 147 | quote! { i16 }, 148 | n.map(|n| { 149 | let v = Literal::i16_unsuffixed(n); 150 | quote! { #v } 151 | }), 152 | ), 153 | Value::Int(n) => ( 154 | quote! { i32 }, 155 | n.map(|n| { 156 | let v = Literal::i32_unsuffixed(n); 157 | quote! { #v } 158 | }), 159 | ), 160 | Value::BigInt(n) => ( 161 | quote! { i64 }, 162 | n.map(|n| { 163 | let v = Literal::i64_unsuffixed(n); 164 | quote! { #v } 165 | }), 166 | ), 167 | Value::TinyUnsigned(n) => ( 168 | quote! { u8 }, 169 | n.map(|n| { 170 | let v = Literal::u8_unsuffixed(n); 171 | quote! { #v } 172 | }), 173 | ), 174 | Value::SmallUnsigned(n) => ( 175 | quote! { u16 }, 176 | n.map(|n| { 177 | let v = Literal::u16_unsuffixed(n); 178 | quote! { #v } 179 | }), 180 | ), 181 | Value::Unsigned(n) => ( 182 | quote! { u32 }, 183 | n.map(|n| { 184 | let v = Literal::u32_unsuffixed(n); 185 | quote! { #v } 186 | }), 187 | ), 188 | Value::BigUnsigned(n) => ( 189 | quote! { u64 }, 190 | n.map(|n| { 191 | let v = Literal::u64_unsuffixed(n); 192 | quote! { #v } 193 | }), 194 | ), 195 | Value::Float(n) => ( 196 | quote! { f32 }, 197 | n.map(|n| { 198 | let v = Literal::f32_unsuffixed(n); 199 | quote! { #v } 200 | }), 201 | ), 202 | Value::Double(n) => ( 203 | quote! { f64 }, 204 | n.map(|n| { 205 | let v = Literal::f64_unsuffixed(n); 206 | quote! { #v } 207 | }), 208 | ), 209 | Value::String(s) => match replace { 210 | Some(Replacement::Type(r)) => ( 211 | r.clone(), 212 | s.map(|s| { 213 | let ident = Ident::new(&s.to_upper_camel_case(), Span::call_site()); 214 | quote! { #r::#ident } 215 | }), 216 | ), 217 | Some(Replacement::Fn(f, Some(r))) => ( 218 | r.clone(), 219 | s.map(|s| { 220 | let v = Literal::string(s.as_str()); 221 | quote! { #r::#f(#v) } 222 | }), 223 | ), 224 | Some(Replacement::Fn(_, None)) => { 225 | // teoretically we could accept only a function, but we won't know the return type 226 | return Err(syn::Error::new( 227 | Span::call_site(), 228 | format!("Missing parameter type for field {col:?}"), 229 | )); 230 | } 231 | _ => ( 232 | quote! { &'static str }, 233 | s.map(|s| { 234 | let v = Literal::string(s.as_str()); 235 | quote! { #v } 236 | }), 237 | ), 238 | }, 239 | // disable ChronoDateTime for now, it would only produce methods for created_at and updated_at fields 240 | // Value::ChronoDateTime(dt) => (quote! { chrono::NaiveDateTime }, Lit::Verbatim(Literal)), 241 | _ => continue, 242 | }; 243 | let (_, method, option) = 244 | methods.entry(format!("{col:?}")).or_insert_with(|| (t, Punctuated::<_, Comma>::new(), false)); 245 | if let Some(v) = value { 246 | method.push(quote! { 247 | #name::#key_ident => #v 248 | }); 249 | } else { 250 | *option = true; 251 | } 252 | } 253 | 254 | Ok(()) 255 | })?; 256 | 257 | // decorate constructors 258 | let constructors = constructors.into_iter().map(|(name, (cols, body))| { 259 | let is_full = cols.len() == primary_keys.len(); 260 | let fn_name = Ident::new(&format!("get_by_{name}"), Span::call_site()); 261 | let signature = cols 262 | .iter() 263 | .map(|col| { 264 | let field_name = Ident::new(&format!("{col:?}").to_snake_case(), Span::call_site()); 265 | match get_replacement::(*col, args) { 266 | Some(Replacement::Type(r)) => quote! { #field_name: #r }, 267 | _ => quote! { #field_name: &str }, 268 | } 269 | }) 270 | .collect::>(); 271 | let m = cols 272 | .iter() 273 | .map(|col| { 274 | let field_name = Ident::new(&format!("{col:?}").to_snake_case(), Span::call_site()); 275 | quote! { #field_name } 276 | }) 277 | .collect::>(); 278 | let body = body 279 | .iter() 280 | .map(|(_, (values, array_body))| { 281 | let args = cols 282 | .iter() 283 | .enumerate() 284 | .map(|(index, col)| match get_replacement::(*col, args) { 285 | Some(Replacement::Type(r)) => { 286 | let ident = Ident::new(&values[index].to_upper_camel_case(), Span::call_site()); 287 | quote! { #r::#ident } 288 | } 289 | _ => { 290 | let v = Literal::string(values[index].as_str()); 291 | quote! { #v } 292 | } 293 | }) 294 | .collect::>(); 295 | if is_full { 296 | quote! { 297 | (#args,) => Some(#array_body) 298 | } 299 | } else { 300 | quote! { 301 | (#args,) => &[#array_body] 302 | } 303 | } 304 | }) 305 | .collect::>(); 306 | if is_full { 307 | quote! { 308 | pub const fn #fn_name(#signature) -> Option { 309 | match (#m,) { 310 | #body, 311 | _ => None, 312 | } 313 | } 314 | } 315 | } else { 316 | quote! { 317 | pub const fn #fn_name(#signature) -> &'static [Self] { 318 | match (#m,) { 319 | #body, 320 | _ => &[], 321 | } 322 | } 323 | } 324 | } 325 | }); 326 | 327 | // separate try_from from other methods 328 | let try_from = methods 329 | .remove("try_from") 330 | .map(|(_, matches, _)| { 331 | quote! { 332 | impl<'a> TryFrom<&'a str> for #name { 333 | type Error = String; 334 | fn try_from(s: &'a str) -> Result { 335 | match s { 336 | #matches, 337 | _ => Err(format!("Unknown {} {}", stringify!(#name), s)), 338 | } 339 | } 340 | } 341 | } 342 | }) 343 | .unwrap_or_default(); 344 | 345 | // decorate methods 346 | let methods: TokenStream = methods 347 | .into_iter() 348 | .map(|(name, (t, matches, option))| { 349 | let n = Ident::new(&name.to_snake_case(), Span::call_site()); 350 | if option { 351 | if matches.is_empty() { 352 | quote! { 353 | pub const fn #n(&self) -> Option<#t> { 354 | None 355 | } 356 | } 357 | } else { 358 | quote! { 359 | pub const fn #n(&self) -> Option<#t> { 360 | Some(match self { 361 | #matches, 362 | _ => return None, 363 | }) 364 | } 365 | } 366 | } 367 | } else { 368 | quote! { 369 | pub const fn #n(&self) -> #t { 370 | match self { 371 | #matches, 372 | } 373 | } 374 | } 375 | } 376 | }) 377 | .chain(constructors) 378 | .collect(); 379 | 380 | // output result 381 | Ok(quote! { 382 | #item 383 | 384 | impl #name { 385 | #methods 386 | } 387 | 388 | #try_from 389 | }) 390 | } 391 | 392 | /// Replacement types 393 | enum Replacement { 394 | Type(TokenStream), 395 | Fn(TokenStream, Option), 396 | } 397 | 398 | /// Field replacement facility 399 | /// Searches between macro arguments 400 | fn get_replacement(col: M::Column, args: &[NestedMeta]) -> Option 401 | where 402 | M: EntityTrait, 403 | M::Column: PartialEq, 404 | { 405 | let col_name = format!("{col:?}"); 406 | let field_name = col_name.to_snake_case(); 407 | // search for replacements 408 | args.iter().find_map(|arg| { 409 | // simple #[macro(field = "enum")] 410 | if let NestedMeta::Meta(Meta::NameValue(mv)) = arg { 411 | if mv.path.is_ident(&col_name) || mv.path.is_ident(&field_name) { 412 | if let Lit::Str(s) = &mv.lit { 413 | let ident = Ident::new(&s.value(), Span::call_site()); 414 | return Some(Replacement::Type(quote! { #ident })); 415 | } 416 | } 417 | } 418 | // quite complex #[macro(field(type = "enum", fn = "foo"))] 419 | if let NestedMeta::Meta(Meta::List(ml)) = arg { 420 | if ml.path.is_ident(&col_name) || ml.path.is_ident(&field_name) { 421 | return ml.nested.iter().fold(None, |mut acc, nested| { 422 | if let NestedMeta::Meta(Meta::NameValue(mv)) = nested { 423 | if let Lit::Str(s) = &mv.lit { 424 | let ident = Ident::new(&s.value(), Span::call_site()); 425 | if mv.path.is_ident("type") { 426 | if let Some(Replacement::Fn(f, None)) = acc { 427 | acc = Some(Replacement::Fn(f, Some(quote! { #ident }))); 428 | } else { 429 | acc = Some(Replacement::Type(quote! { #ident })); 430 | } 431 | } else if mv.path.is_ident("fn") { 432 | if let Some(Replacement::Type(t)) = acc { 433 | acc = Some(Replacement::Fn(quote! { #ident }, Some(t))); 434 | } else { 435 | acc = Some(Replacement::Fn(quote! { #ident }, None)); 436 | } 437 | } 438 | } 439 | } 440 | acc 441 | }); 442 | } 443 | } 444 | None 445 | }) 446 | } 447 | 448 | /// Data retrieve function with cache capabilities 449 | /// File access is sync to not have to depend on an async runtime 450 | async fn get_data(get_conn: F) -> syn::Result::Model>> 451 | where 452 | M: EntityTrait + EntityFilter + Default, 453 | ::Model: Serialize + DeserializeOwned, 454 | F: Fn() -> Fut, 455 | Fut: Future>, 456 | { 457 | let instance = M::default(); 458 | let mut cache = env::temp_dir(); 459 | cache.push(EntityName::table_name(&instance)); 460 | cache.set_extension("cache"); 461 | if cache.exists() { 462 | info!("Cache file {} exists, loading data from there", cache.display()); 463 | 464 | let file = fs::File::open(&cache) 465 | .map_err(|e| syn::Error::new(Span::call_site(), format!("Error reading {}: {}", cache.display(), e)))?; 466 | 467 | match bincode::deserialize_from(io::BufReader::new(file)) { 468 | Ok(data) => return Ok(data), 469 | Err(e) => error!("Error deserializing {}: {}", cache.display(), e), 470 | } 471 | } else { 472 | info!("Cache file {} doesn't exists, creating", cache.display()); 473 | } 474 | 475 | let conn = get_conn().await?; 476 | let data = ::find() 477 | .filter(M::filter()) 478 | .all(&conn) 479 | .await 480 | .map_err(|e| syn::Error::new(Span::call_site(), e))?; 481 | let buf = bincode::serialize(&data) 482 | .map_err(|e| syn::Error::new(Span::call_site(), format!("Error serializing {}: {}", cache.display(), e)))?; 483 | fs::write(&cache, buf) 484 | .map_err(|e| syn::Error::new(Span::call_site(), format!("Error writing {}: {}", cache.display(), e)))?; 485 | Ok(data) 486 | } 487 | --------------------------------------------------------------------------------