From bd22aeba678659d26b9f3f42366c193280265037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:41:46 +0100 Subject: [PATCH] consolidate scraper and refresh logic, and improve OpenAPI documentation --- ...10ffead845478d359af83b70d98ff8d1945f2.json | 29 - ...91ff02e2c609c161e9c935a76fa9d63e61696.json | 42 -- ...b20ff9f1ff928912d871a708a088f2d011ba7.json | 15 - Cargo.lock | 554 ++++++++++++++---- Cargo.toml | 8 +- scraper/Cargo.toml | 7 +- scraper/src/dish.rs | 35 +- scraper/src/lib.rs | 1 - scraper/src/main.rs | 59 +- scraper/src/refresh.rs | 20 +- scraper/src/util.rs | 62 +- shared/Cargo.toml | 3 +- shared/src/canteen.rs | 13 +- web-api/Cargo.toml | 11 +- web-api/src/dish.rs | 40 +- web-api/src/endpoints/menu.rs | 44 +- web-api/src/endpoints/metadata.rs | 33 +- web-api/src/endpoints/mod.rs | 32 +- web-api/src/endpoints/nutrition.rs | 21 +- web-api/src/endpoints/price_history.rs | 126 ++-- web-api/src/main.rs | 13 + web-api/src/menu.rs | 18 +- web-api/src/util.rs | 6 + 23 files changed, 726 insertions(+), 466 deletions(-) delete mode 100644 .sqlx/query-65858112433addbff921108a5b110ffead845478d359af83b70d98ff8d1945f2.json delete mode 100644 .sqlx/query-781e98dce280715896a347808d891ff02e2c609c161e9c935a76fa9d63e61696.json delete mode 100644 .sqlx/query-f804f9c634a34945d7aa0cd3162b20ff9f1ff928912d871a708a088f2d011ba7.json diff --git a/.sqlx/query-65858112433addbff921108a5b110ffead845478d359af83b70d98ff8d1945f2.json b/.sqlx/query-65858112433addbff921108a5b110ffead845478d359af83b70d98ff8d1945f2.json deleted file mode 100644 index bcb84ab..0000000 --- a/.sqlx/query-65858112433addbff921108a5b110ffead845478d359af83b70d98ff8d1945f2.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT DISTINCT scraped_for, canteen FROM canteens_scraped WHERE scraped_for >= $1 AND scraped_for <= $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "scraped_for", - "type_info": "Date" - }, - { - "ordinal": 1, - "name": "canteen", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Date", - "Date" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "65858112433addbff921108a5b110ffead845478d359af83b70d98ff8d1945f2" -} diff --git a/.sqlx/query-781e98dce280715896a347808d891ff02e2c609c161e9c935a76fa9d63e61696.json b/.sqlx/query-781e98dce280715896a347808d891ff02e2c609c161e9c935a76fa9d63e61696.json deleted file mode 100644 index 80c86ba..0000000 --- a/.sqlx/query-781e98dce280715896a347808d891ff02e2c609c161e9c935a76fa9d63e61696.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT date, price_students, price_employees, price_guests FROM meals WHERE canteen = $1 AND LOWER(\"name\") = $2 AND is_latest = TRUE ORDER BY date DESC LIMIT $3;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "date", - "type_info": "Date" - }, - { - "ordinal": 1, - "name": "price_students", - "type_info": "Numeric" - }, - { - "ordinal": 2, - "name": "price_employees", - "type_info": "Numeric" - }, - { - "ordinal": 3, - "name": "price_guests", - "type_info": "Numeric" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "781e98dce280715896a347808d891ff02e2c609c161e9c935a76fa9d63e61696" -} diff --git a/.sqlx/query-f804f9c634a34945d7aa0cd3162b20ff9f1ff928912d871a708a088f2d011ba7.json b/.sqlx/query-f804f9c634a34945d7aa0cd3162b20ff9f1ff928912d871a708a088f2d011ba7.json deleted file mode 100644 index 5119b58..0000000 --- a/.sqlx/query-f804f9c634a34945d7aa0cd3162b20ff9f1ff928912d871a708a088f2d011ba7.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE meals SET is_latest = FALSE WHERE date = $1 AND canteen = $2 AND is_latest = TRUE", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Date", - "Text" - ] - }, - "nullable": [] - }, - "hash": "f804f9c634a34945d7aa0cd3162b20ff9f1ff928912d871a708a088f2d011ba7" -} diff --git a/Cargo.lock b/Cargo.lock index 11c8969..ba914bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -210,7 +210,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -219,6 +219,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -264,6 +275,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "atoi" version = "2.0.0" @@ -293,23 +310,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" - -[[package]] -name = "bigdecimal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", - "serde", -] +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" [[package]] name = "bitflags" @@ -320,6 +323,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -329,6 +344,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "brotli" version = "8.0.2" @@ -352,9 +390,31 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "byteorder" @@ -379,9 +439,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -554,7 +614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -593,24 +653,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.113", "unicode-xid", ] @@ -634,7 +694,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -645,9 +705,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -688,6 +748,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -718,9 +788,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" @@ -770,6 +840,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -847,7 +923,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -934,9 +1010,9 @@ dependencies = [ [[package]] name = "governor" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", "dashmap", @@ -976,9 +1052,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -993,6 +1069,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1145,7 +1230,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body", "httparse", @@ -1171,7 +1256,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -1338,6 +1423,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1348,9 +1435,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1367,9 +1454,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -1408,9 +1495,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libm" @@ -1420,13 +1507,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -1527,17 +1614,17 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mensa-upb-api" -version = "0.4.0" +version = "0.4.1" dependencies = [ "actix-cors", "actix-governor", "actix-web", "anyhow", - "bigdecimal", "chrono", "dotenvy", "itertools", "mensa-upb-scraper", + "rust_decimal", "serde", "serde_json", "shared", @@ -1546,11 +1633,14 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-actix-web", + "utoipa-rapidoc", ] [[package]] name = "mensa-upb-scraper" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "chrono", @@ -1558,7 +1648,6 @@ dependencies = [ "dotenvy", "futures", "itertools", - "num-bigint", "reqwest", "scraper", "shared", @@ -1618,16 +1707,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1710,7 +1789,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1771,7 +1850,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -1824,9 +1903,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "potential_utf" @@ -1859,14 +1938,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "proc-macro-crate" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -1895,7 +2003,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror", "tokio", "tracing", @@ -1932,9 +2040,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1952,6 +2060,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2029,6 +2143,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -2065,16 +2188,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] -name = "reqwest" -version = "0.12.26" +name = "rend" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body", "http-body-util", @@ -2102,7 +2234,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -2119,6 +2251,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.9" @@ -2139,6 +2300,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2156,9 +2333,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "ring", @@ -2170,9 +2347,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "web-time", "zeroize", @@ -2197,9 +2374,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scopeguard" @@ -2222,6 +2399,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "selectors" version = "0.33.0" @@ -2274,20 +2457,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2349,6 +2532,7 @@ dependencies = [ "serde", "sqlx", "strum", + "utoipa", ] [[package]] @@ -2359,10 +2543,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2382,6 +2567,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -2471,7 +2662,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", - "bigdecimal", "bytes", "chrono", "crc", @@ -2489,6 +2679,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", + "rust_decimal", "rustls", "serde", "serde_json", @@ -2513,7 +2704,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.113", ] [[package]] @@ -2536,7 +2727,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.113", "tokio", "url", ] @@ -2549,7 +2740,6 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags", "byteorder", "bytes", @@ -2574,6 +2764,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", + "rust_decimal", "serde", "sha1", "sha2", @@ -2594,7 +2785,6 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags", "byteorder", "chrono", @@ -2612,9 +2802,9 @@ dependencies = [ "log", "md-5", "memchr", - "num-bigint", "once_cell", "rand 0.8.5", + "rust_decimal", "serde", "serde_json", "sha2", @@ -2713,7 +2903,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2724,9 +2914,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -2750,9 +2951,15 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tendril" version = "0.4.3" @@ -2781,7 +2988,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2851,9 +3058,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2874,7 +3081,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -2889,9 +3096,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2900,9 +3107,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2911,6 +3118,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -2958,9 +3195,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -2976,14 +3213,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3118,6 +3355,53 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-actix-web" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f" +dependencies = [ + "actix-service", + "actix-web", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.113", +] + +[[package]] +name = "utoipa-rapidoc" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f8f5abd341cce16bb4f09a8bafc087d4884a004f25fb980e538d51d6501dab" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "uuid" version = "1.19.0" @@ -3221,7 +3505,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.113", "wasm-bindgen-shared", ] @@ -3272,14 +3556,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -3337,7 +3621,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -3348,7 +3632,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -3597,6 +3881,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -3609,6 +3902,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3628,7 +3930,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", "synstructure", ] @@ -3649,7 +3951,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] [[package]] @@ -3669,7 +3971,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", "synstructure", ] @@ -3709,9 +4011,15 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.113", ] +[[package]] +name = "zmij" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index ce20ed7..fcca72a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ resolver = "2" [workspace.package] license = "MIT" -authors = ["Moritz Hölting"] +authors = ["Moritz Hölting "] repository = "https://github.com/moritz-hoelting/mensa-upb-api" readme = "README.md" @@ -20,9 +20,11 @@ chrono = "0.4.42" dotenvy = "0.15.7" futures = "0.3.31" itertools = "0.14.0" +rust_decimal = "1.39.0" serde = { version = "1.0.228", features = ["derive"] } sqlx = "0.8.2" strum = "0.27.2" -tokio = "1.48.0" -tracing = "0.1.43" +tokio = "1.49.0" +tracing = "0.1.44" tracing-subscriber = "0.3.22" +utoipa = "5.4.0" diff --git a/scraper/Cargo.toml b/scraper/Cargo.toml index ef087ff..e00b8da 100644 --- a/scraper/Cargo.toml +++ b/scraper/Cargo.toml @@ -5,8 +5,8 @@ license.workspace = true authors.workspace = true repository.workspace = true readme.workspace = true -version = "0.2.0" -edition = "2021" +version = "0.2.1" +edition = "2024" publish = false [dependencies] @@ -16,11 +16,10 @@ const_format = "0.2.33" dotenvy = { workspace = true } futures = { workspace = true } itertools = { workspace = true } -num-bigint = "0.4.6" reqwest = { version = "0.12.9", default-features = false, features = ["charset", "rustls-tls", "http2"] } scraper = "0.25.0" shared = { path = "../shared" } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "uuid", "bigdecimal"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "uuid", "rust_decimal"] } strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tracing = { workspace = true } diff --git a/scraper/src/dish.rs b/scraper/src/dish.rs index 5124e10..946f879 100644 --- a/scraper/src/dish.rs +++ b/scraper/src/dish.rs @@ -1,9 +1,8 @@ use std::sync::LazyLock; -use num_bigint::BigInt; use scraper::{ElementRef, Selector}; use shared::DishType; -use sqlx::types::BigDecimal; +use sqlx::types::Decimal; use crate::util::normalize_price_bigdecimal; @@ -20,9 +19,9 @@ static HTML_NUTRITIONS_SELECTOR: LazyLock = pub struct Dish { pub name: String, pub image_src: Option, - pub price_students: BigDecimal, - pub price_employees: BigDecimal, - pub price_guests: BigDecimal, + pub price_students: Decimal, + pub price_employees: Decimal, + pub price_guests: Decimal, pub vegetarian: bool, pub vegan: bool, pub dish_type: DishType, @@ -32,22 +31,22 @@ pub struct Dish { #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] pub struct NutritionValues { pub kjoule: Option, - pub protein: Option, - pub carbs: Option, - pub fat: Option, + pub protein: Option, + pub carbs: Option, + pub fat: Option, } impl Dish { pub fn get_name(&self) -> &str { &self.name } - pub fn get_price_students(&self) -> &BigDecimal { + pub fn get_price_students(&self) -> &Decimal { &self.price_students } - pub fn get_price_employees(&self) -> &BigDecimal { + pub fn get_price_employees(&self) -> &Decimal { &self.price_employees } - pub fn get_price_guests(&self) -> &BigDecimal { + pub fn get_price_guests(&self) -> &Decimal { &self.price_guests } pub fn get_image_src(&self) -> Option<&str> { @@ -185,9 +184,9 @@ impl NutritionValues { pub fn normalize(self) -> Self { Self { kjoule: self.kjoule, - protein: self.protein.map(|p| p.with_prec(6).with_scale(2)), - carbs: self.carbs.map(|c| c.with_prec(6).with_scale(2)), - fat: self.fat.map(|f| f.with_prec(6).with_scale(2)), + protein: self.protein.map(|p| p.normalize().round_dp(2)), + carbs: self.carbs.map(|c| c.normalize().round_dp(2)), + fat: self.fat.map(|f| f.normalize().round_dp(2)), } } } @@ -198,18 +197,18 @@ impl PartialOrd for Dish { } } -fn price_to_bigdecimal(s: Option<&str>) -> BigDecimal { +fn price_to_bigdecimal(s: Option<&str>) -> Decimal { s.and_then(|p| { p.trim_end_matches(" €") .replace(',', ".") - .parse::() + .parse::() .ok() }) .map(normalize_price_bigdecimal) - .unwrap_or_else(|| BigDecimal::from_bigint(BigInt::from(99999), 2)) + .unwrap_or_else(|| Decimal::from(99999)) } -fn grams_to_bigdecimal(s: &str) -> Option { +fn grams_to_bigdecimal(s: &str) -> Option { s.trim_end_matches("g") .replace(',', ".") .trim() diff --git a/scraper/src/lib.rs b/scraper/src/lib.rs index 094b2be..4ae0588 100644 --- a/scraper/src/lib.rs +++ b/scraper/src/lib.rs @@ -10,7 +10,6 @@ pub use dish::Dish; pub use menu::scrape_menu; pub use refresh::check_refresh; use shared::Canteen; -pub use util::scrape_canteens_at_days_and_insert; #[derive(Debug, Clone)] struct CustomError(String); diff --git a/scraper/src/main.rs b/scraper/src/main.rs index d523ed4..d2990fd 100644 --- a/scraper/src/main.rs +++ b/scraper/src/main.rs @@ -1,11 +1,19 @@ -use std::collections::HashSet; +use std::sync::LazyLock; use anyhow::Result; use chrono::{Duration, Utc}; -use itertools::Itertools as _; -use mensa_upb_scraper::{util, FILTER_CANTEENS}; +use futures::{future, StreamExt}; +use mensa_upb_scraper::{check_refresh, util, FILTER_CANTEENS}; use shared::Canteen; use strum::IntoEnumIterator as _; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::EnvFilter; + +static CANTEENS: LazyLock> = LazyLock::new(|| { + Canteen::iter() + .filter(|c| !FILTER_CANTEENS.contains(c)) + .collect::>() +}); #[tokio::main] async fn main() -> Result<()> { @@ -13,40 +21,31 @@ async fn main() -> Result<()> { let db = util::get_db()?; - tracing_subscriber::fmt::init(); + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .from_env() + .expect("Invalid filter") + .add_directive("mensa_upb_scraper=debug".parse().unwrap()); + tracing_subscriber::fmt().with_env_filter(env_filter).init(); sqlx::migrate!("../migrations").run(&db).await?; tracing::info!("Starting up..."); - let start_date = Utc::now().date_naive(); - let end_date = (Utc::now() + Duration::days(6)).date_naive(); - - let already_scraped = sqlx::query!( - "SELECT DISTINCT scraped_for, canteen FROM canteens_scraped WHERE scraped_for >= $1 AND scraped_for <= $2", - start_date, - end_date - ) - .fetch_all(&db) - .await? - .into_iter() - .map(|r| { - ( - r.scraped_for, - r.canteen.parse::().expect("Invalid db entry"), - ) - }) - .collect::>(); - - let date_canteen_combinations = (0..7) + let handles = (0..7) .map(|d| (Utc::now() + Duration::days(d)).date_naive()) - .cartesian_product(Canteen::iter()) - .filter(|entry @ (_, canteen)| { - !FILTER_CANTEENS.contains(canteen) && !already_scraped.contains(entry) - }) - .collect::>(); + .map(|date| { + let db = db.clone(); + tokio::spawn(async move { check_refresh(&db, date, &CANTEENS).await }) + }); - util::scrape_canteens_at_days_and_insert(&db, &date_canteen_combinations).await?; + future::join_all(handles).await; + + futures::stream::iter((0..7).map(|d| (Utc::now() + Duration::days(d)).date_naive())) + .for_each_concurrent(None, async |date| { + check_refresh(&db, date, &CANTEENS).await; + }) + .await; tracing::info!("Finished scraping menu"); diff --git a/scraper/src/refresh.rs b/scraper/src/refresh.rs index dacb243..ef58cde 100644 --- a/scraper/src/refresh.rs +++ b/scraper/src/refresh.rs @@ -12,9 +12,9 @@ use sqlx::QueryBuilder; use strum::IntoEnumIterator as _; use crate::{ + Dish, dish::NutritionValues, util::{self, add_menu_to_db, normalize_price_bigdecimal}, - Dish, }; static NON_FILTERED_CANTEENS: LazyLock> = LazyLock::new(|| { @@ -28,7 +28,7 @@ static NON_FILTERED_CANTEENS: LazyLock> = LazyLock::new(|| { #[tracing::instrument(skip(db))] pub async fn check_refresh(db: &sqlx::PgPool, date: NaiveDate, canteens: &[Canteen]) -> bool { - if date > Utc::now().date_naive() + chrono::Duration::days(7) { + if date > Utc::now().date_naive() + chrono::Duration::days(31) { tracing::debug!("Not refreshing menu for date {date} as it is too far in the future"); return false; } @@ -148,13 +148,14 @@ fn needs_refresh(last_refreshed: chrono::DateTime, date_entry: chrono::Naiv } } +#[tracing::instrument(skip(db, date, stale_dishes, new_dishes), fields(date = %date, stale_dish_count = %stale_dishes.len(), new_dish_count = %new_dishes.len()))] async fn update_stale_dishes( db: &sqlx::PgPool, date: NaiveDate, stale_dishes: &HashSet<&(Canteen, Dish)>, new_dishes: &HashSet<&(Canteen, Dish)>, canteens: &[Canteen], -) -> Result<(), sqlx::Error> { +) -> anyhow::Result<()> { let mut tx = db.begin().await?; if !stale_dishes.is_empty() { @@ -169,6 +170,10 @@ async fn update_stale_dishes( .build() .execute(&mut *tx) .await?; + + if new_dishes.is_empty() { + tracing::debug!("No new dishes to add after marking stale dishes"); + } } let chunks = new_dishes @@ -184,12 +189,9 @@ async fn update_stale_dishes( g.map(|(_, dish)| dish).cloned().collect::>(), ) }) - .chain( - canteens - .iter() - .map(|canteen| (*canteen, Vec::new())) - .unique_by(|(c, _)| *c), - ); + .chain(canteens.iter().map(|canteen| (*canteen, Vec::new()))) + .unique_by(|(c, _)| *c) + .collect::>(); for (canteen, menu) in new_dishes_iter { add_menu_to_db(&mut tx, &date, canteen, menu).await?; diff --git a/scraper/src/util.rs b/scraper/src/util.rs index b6886b7..49d3ab6 100644 --- a/scraper/src/util.rs +++ b/scraper/src/util.rs @@ -4,7 +4,7 @@ use anyhow::Result; use chrono::NaiveDate; use futures::{Stream, StreamExt as _}; use shared::{Canteen, DishType}; -use sqlx::{postgres::PgPoolOptions, types::BigDecimal, PgPool, PgTransaction}; +use sqlx::{postgres::PgPoolOptions, types::Decimal, PgPool, PgTransaction}; use crate::{scrape_menu, Dish}; @@ -13,62 +13,6 @@ pub fn get_db() -> Result { .connect_lazy(&env::var("DATABASE_URL").expect("missing DATABASE_URL env variable"))?) } -pub async fn scrape_canteens_at_days_and_insert( - db: &PgPool, - date_canteen_combinations: &[(NaiveDate, Canteen)], -) -> Result<()> { - let (tx, mut rx) = tokio::sync::mpsc::channel::<(NaiveDate, Canteen, Vec)>(128); - - let mut transaction = db.begin().await?; - - for (date, canteen) in date_canteen_combinations { - sqlx::query!( - "UPDATE meals SET is_latest = FALSE WHERE date = $1 AND canteen = $2 AND is_latest = TRUE", - date, - canteen.get_identifier() - ) - .execute(&mut *transaction) - .await - .ok(); - } - - let insert_handle = tokio::spawn(async move { - while let Some((date, canteen, menu)) = rx.recv().await { - add_menu_to_db(&mut transaction, &date, canteen, menu).await?; - } - - transaction.commit().await - }); - - let errs = scrape_canteens_at_days(date_canteen_combinations) - .then(|res| { - let tx = tx.clone(); - async move { - match res { - Ok((date, canteen, menu)) => { - tx.send((date, canteen, menu)).await.ok(); - Ok(()) - } - Err(err) => { - tracing::error!("Error scraping menu: {err}"); - Err(err) - } - } - } - }) - .collect::>() - .await; - - drop(tx); - insert_handle.await??; - - if let Some(err) = errs.into_iter().find_map(Result::err) { - return Err(err); - } - - Ok(()) -} - pub fn scrape_canteens_at_days<'a>( date_canteen_combinations: &'a [(NaiveDate, Canteen)], ) -> impl Stream)>> + 'a { @@ -125,6 +69,6 @@ pub async fn add_menu_to_db( Ok(()) } -pub fn normalize_price_bigdecimal(price: BigDecimal) -> BigDecimal { - price.with_prec(6).with_scale(2) +pub fn normalize_price_bigdecimal(price: Decimal) -> Decimal { + price.normalize().round_dp(2) } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 687aaea..6a9ad20 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -10,4 +10,5 @@ readme.workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } -sqlx = { workspace = true } \ No newline at end of file +sqlx = { workspace = true } +utoipa = { workspace = true } diff --git a/shared/src/canteen.rs b/shared/src/canteen.rs index ae20e2a..259994b 100644 --- a/shared/src/canteen.rs +++ b/shared/src/canteen.rs @@ -4,7 +4,18 @@ use serde::{Deserialize, Serialize}; use strum::EnumIter; #[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumIter, Hash, Serialize, Deserialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + EnumIter, + Hash, + Serialize, + Deserialize, + utoipa::ToSchema, )] #[serde(rename_all = "kebab-case")] pub enum Canteen { diff --git a/web-api/Cargo.toml b/web-api/Cargo.toml index ea75aaf..5080773 100644 --- a/web-api/Cargo.toml +++ b/web-api/Cargo.toml @@ -5,8 +5,8 @@ license.workspace = true authors.workspace = true repository.workspace = true readme.workspace = true -version = "0.4.0" -edition = "2021" +version = "0.4.1" +edition = "2024" publish = false [dependencies] @@ -14,16 +14,19 @@ actix-cors = "0.7.1" actix-governor = { version = "0.10.0", features = ["log"] } actix-web = "4.12.1" anyhow = { workspace = true } -bigdecimal = { version = "0.4.9", features = ["serde"] } chrono = { workspace = true, features = ["serde"] } dotenvy = { workspace = true } itertools = { workspace = true } mensa-upb-scraper = { path = "../scraper" } +rust_decimal = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = "1.0.145" shared = { path = "../shared" } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "uuid", "bigdecimal"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "uuid", "rust_decimal"] } strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tracing = "0.1.43" tracing-subscriber = { workspace = true, features = ["env-filter"] } +utoipa = { workspace = true, features = ["actix_extras", "chrono", "decimal"] } +utoipa-actix-web = "0.1.2" +utoipa-rapidoc = { version = "6.0.0", features = ["actix-web"] } diff --git a/web-api/src/dish.rs b/web-api/src/dish.rs index 59e41f7..2f9f136 100644 --- a/web-api/src/dish.rs +++ b/web-api/src/dish.rs @@ -1,9 +1,9 @@ -use bigdecimal::BigDecimal; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use shared::Canteen; use sqlx::prelude::FromRow; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct Dish { pub name: String, pub image_src: Option, @@ -13,19 +13,27 @@ pub struct Dish { pub canteens: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct DishPrices { - pub students: BigDecimal, - pub employees: BigDecimal, - pub guests: BigDecimal, + pub students: Decimal, + pub employees: Decimal, + pub guests: Decimal, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow, utoipa::ToSchema)] +#[schema(examples( + json!({ + "kjoules": 1500, + "carbohydrates": "45.5", + "proteins": "30.0", + "fats": "10.0" + }) +))] pub struct DishNutrients { pub kjoules: Option, - pub carbohydrates: Option, - pub proteins: Option, - pub fats: Option, + pub carbohydrates: Option, + pub proteins: Option, + pub fats: Option, } impl Dish { @@ -52,9 +60,9 @@ impl PartialOrd for Dish { impl DishPrices { pub fn normalize(self) -> Self { Self { - students: self.students.with_prec(5).with_scale(2), - employees: self.employees.with_prec(5).with_scale(2), - guests: self.guests.with_prec(5).with_scale(2), + students: self.students.normalize().round_dp(2), + employees: self.employees.normalize().round_dp(2), + guests: self.guests.normalize().round_dp(2), } } } @@ -63,9 +71,9 @@ impl DishNutrients { pub fn normalize(self) -> Self { Self { kjoules: self.kjoules, - carbohydrates: self.carbohydrates.map(|v| v.with_prec(6).with_scale(2)), - proteins: self.proteins.map(|v| v.with_prec(6).with_scale(2)), - fats: self.fats.map(|v| v.with_prec(6).with_scale(2)), + carbohydrates: self.carbohydrates.map(|v| v.normalize().round_dp(2)), + proteins: self.proteins.map(|v| v.normalize().round_dp(2)), + fats: self.fats.map(|v| v.normalize().round_dp(2)), } } } diff --git a/web-api/src/endpoints/menu.rs b/web-api/src/endpoints/menu.rs index cd2fb0e..d4eae6e 100644 --- a/web-api/src/endpoints/menu.rs +++ b/web-api/src/endpoints/menu.rs @@ -1,21 +1,21 @@ -use actix_web::{ - get, - web::{self, ServiceConfig}, - HttpResponse, Responder, -}; +use actix_web::{get, web, HttpResponse, Responder}; use chrono::NaiveDate; use itertools::Itertools as _; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; +use utoipa_actix_web::service_config::ServiceConfig; -use crate::{util, Menu}; +use crate::{ + util::{self, GenericServerError}, + Menu, +}; pub fn configure(cfg: &mut ServiceConfig) { cfg.service(menu); } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] struct MenuQuery { date: Option, @@ -23,7 +23,35 @@ struct MenuQuery { no_update: bool, } -#[get("/menu/{canteen}")] +#[expect(dead_code)] +#[derive(utoipa::ToSchema)] +pub(super) struct InvalidCanteenError { + error: &'static str, + + /// Which of the given canteen identifiers is invalid + invalid: Vec, +} + +#[utoipa::path( + summary = "Get menu of canteen(s)", + description = "Get the menu of a canteen(s) (at specified date).", + params( + ("canteens" = String, Path, description = "Comma-separated list of canteen identifiers to get the menu for", example = "forum,academica"), + ("date" = Option, Query, description = "Date to get the menu for (defaults to today)"), + ("noUpdate" = Option, Query, description = "If set to true, the menu will not be updated before querying (default: false)", example = false), + ), + responses( + (status = OK, description = "The menu of the specified canteen(s).", body = [Menu]), + (status = BAD_REQUEST, description = "Invalid canteen identifier.", body = InvalidCanteenError, example = json!({ + "error": "Invalid canteen identifier", + "invalid": ["invalid_canteen_1", "invalid_canteen_2"] + })), + (status = INTERNAL_SERVER_ERROR, description = "Server failed to answer request.", body = GenericServerError, example = json!({ + "error": "Failed to query database", + })) + ) +)] +#[get("/menu/{canteens}")] async fn menu( path: web::Path, query: web::Query, diff --git a/web-api/src/endpoints/metadata.rs b/web-api/src/endpoints/metadata.rs index cd189f7..a19709c 100644 --- a/web-api/src/endpoints/metadata.rs +++ b/web-api/src/endpoints/metadata.rs @@ -1,24 +1,35 @@ use std::sync::OnceLock; -use actix_web::{ - get, - web::{self, ServiceConfig}, - HttpResponse, Responder, -}; +use actix_web::{get, web, HttpResponse, Responder}; use chrono::NaiveDate; +use serde::Serialize; use serde_json::json; use sqlx::PgPool; +use utoipa_actix_web::service_config::ServiceConfig; + +use crate::util::GenericServerError; pub fn configure(cfg: &mut ServiceConfig) { - cfg.service(web::scope("/metadata").service(earliest_meal_date)); + cfg.service(utoipa_actix_web::scope("/metadata").service(earliest_meal_date)); } static EARLIEST_MEAL_DATE: OnceLock = OnceLock::new(); +#[derive(Serialize, utoipa::ToSchema)] +struct DateResponse { + date: NaiveDate, +} + +#[utoipa::path(summary = "Earliest meal date", description = "Get the date of the earliest meal saved.", responses( + (status = OK, description = "Get the date of the earliest meal saved.", body = DateResponse), + (status = INTERNAL_SERVER_ERROR, description = "Server failed to answer request.", body = GenericServerError) +))] #[get("/earliest-meal-date")] async fn earliest_meal_date(db: web::Data) -> impl Responder { if let Some(earliest_date) = EARLIEST_MEAL_DATE.get() { - earliest_meal_date_ok_response(*earliest_date) + HttpResponse::Ok().json(DateResponse { + date: *earliest_date, + }) } else { match sqlx::query_scalar!( r#"SELECT MIN(date) AS "date!" FROM meals WHERE is_latest = TRUE;"# @@ -28,7 +39,7 @@ async fn earliest_meal_date(db: web::Data) -> impl Responder { { Ok(date) => { EARLIEST_MEAL_DATE.set(date).ok(); - earliest_meal_date_ok_response(date) + HttpResponse::Ok().json(DateResponse { date }) } Err(err) => { tracing::error!("Failed to query datebase: {err}"); @@ -39,9 +50,3 @@ async fn earliest_meal_date(db: web::Data) -> impl Responder { } } } - -fn earliest_meal_date_ok_response(date: NaiveDate) -> HttpResponse { - HttpResponse::Ok().json(json!({ - "date": date, - })) -} diff --git a/web-api/src/endpoints/mod.rs b/web-api/src/endpoints/mod.rs index ca1b980..38808fd 100644 --- a/web-api/src/endpoints/mod.rs +++ b/web-api/src/endpoints/mod.rs @@ -1,8 +1,7 @@ -use actix_web::{get, web::ServiceConfig, HttpResponse, Responder}; -use itertools::Itertools as _; -use serde_json::json; +use actix_web::{get, HttpResponse, Responder}; use shared::Canteen; use strum::IntoEnumIterator as _; +use utoipa_actix_web::service_config::ServiceConfig; mod menu; mod metadata; @@ -17,11 +16,28 @@ pub fn configure(cfg: &mut ServiceConfig) { .configure(price_history::configure); } +#[derive(serde::Serialize, utoipa::ToSchema)] +struct IndexResponse { + /// The current version of the API. + version: &'static str, + /// A short description of the API. + description: &'static str, + /// A list of supported canteens. + supported_canteens: Vec, +} + +#[utoipa::path(summary = "Get API version and capabilities", description = "Get information about the api version and capabilities.", responses((status = 200, body = IndexResponse, example = json!(IndexResponse { + version: env!("CARGO_PKG_VERSION"), + description: env!("CARGO_PKG_DESCRIPTION"), + supported_canteens: Canteen::iter().map(|c| c.get_identifier().to_string()).collect::>() +}))))] #[get("/")] async fn index() -> impl Responder { - HttpResponse::Ok().json(json!({ - "version": env!("CARGO_PKG_VERSION"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "supportedCanteens": Canteen::iter().map(|c| c.get_identifier().to_string()).collect_vec(), - })) + HttpResponse::Ok().json(IndexResponse { + version: env!("CARGO_PKG_VERSION"), + description: env!("CARGO_PKG_DESCRIPTION"), + supported_canteens: Canteen::iter() + .map(|c| c.get_identifier().to_string()) + .collect(), + }) } diff --git a/web-api/src/endpoints/nutrition.rs b/web-api/src/endpoints/nutrition.rs index d830506..74d9abd 100644 --- a/web-api/src/endpoints/nutrition.rs +++ b/web-api/src/endpoints/nutrition.rs @@ -1,25 +1,32 @@ -use actix_web::{ - get, - web::{self, ServiceConfig}, - HttpResponse, Responder, -}; +use actix_web::{get, web, HttpResponse, Responder}; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; +use utoipa_actix_web::service_config::ServiceConfig; -use crate::dish::DishNutrients; +use crate::{dish::DishNutrients, util::GenericServerError}; pub fn configure(cfg: &mut ServiceConfig) { cfg.service(nutrition); } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] struct NutritionQuery { date: Option, } +#[utoipa::path( + summary = "Get nutrition values of some dish", + description = "Query nutrition values of some dish (at certain date).", + params(("name" = String, Path, description = "Name of the dish to query nutrition values for", example = "Bratwurst mit Currysauce und Pommes Frites")), + responses( + (status = OK, description = "Get nutrition values of some dish.", body = DishNutrients), + (status = NOT_FOUND, description = "No dish with a matching name could be found.", body = GenericServerError), + (status = INTERNAL_SERVER_ERROR, description = "Server failed to answer request.", body = GenericServerError) + ) +)] #[get("/nutrition/{name}")] async fn nutrition( path: web::Path, diff --git a/web-api/src/endpoints/price_history.rs b/web-api/src/endpoints/price_history.rs index 54ed95e..764ab63 100644 --- a/web-api/src/endpoints/price_history.rs +++ b/web-api/src/endpoints/price_history.rs @@ -1,24 +1,25 @@ use std::collections::BTreeMap; -use actix_web::{ - get, - web::{self, ServiceConfig}, - HttpResponse, Responder, -}; -use bigdecimal::BigDecimal; +use actix_web::{get, web, HttpResponse, Responder}; use chrono::NaiveDate; use itertools::Itertools; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::{prelude::FromRow, PgPool}; +use utoipa_actix_web::service_config::ServiceConfig; -use crate::{util, DishPrices}; +use crate::{ + endpoints::menu::InvalidCanteenError, + util::{self, GenericServerError}, + DishPrices, +}; pub fn configure(cfg: &mut ServiceConfig) { cfg.service(price_history); } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] struct PriceHistoryQuery { canteens: Option, @@ -29,11 +30,50 @@ struct PriceHistoryQuery { struct PriceHistoryRow { date: NaiveDate, canteen: String, - price_students: BigDecimal, - price_employees: BigDecimal, - price_guests: BigDecimal, + price_students: Decimal, + price_employees: Decimal, + price_guests: Decimal, } +#[utoipa::path( + summary = "Get price history of a dish", + description = "Query the price history of a dish (optionally filtered by canteen(s)).", + params( + ("name" = String, Path, description = "Name of the dish to query price history for", example = "Bratwurst mit Currysauce und Pommes Frites"), + ("canteens" = Option, Query, description = "Comma-separated list of canteen identifiers to filter the price history by", example = "forum,academica"), + ("limit" = Option, Query, description = "Maximum number of entries to return", minimum = 1, maximum = 1000, example = 100), + ), + responses( + (status = OK, description = "Query the price history of a dish.", body = BTreeMap>, example = json!({ + "forum": { + "2024-06-01": { + "students": "2.50", + "employees": "3.50", + "guests": "4.50" + }, + "2024-05-31": { + "students": "2.40", + "employees": "3.40", + "guests": "4.40" + } + }, + "academica": { + "2024-06-01": { + "students": "2.60", + "employees": "3.60", + "guests": "4.60" + } + } + })), + (status = BAD_REQUEST, description = "Invalid canteen identifier.", body = InvalidCanteenError, example = json!({ + "error": "Invalid canteen identifier", + "invalid": ["invalid_canteen_1", "invalid_canteen_2"] + })), + (status = INTERNAL_SERVER_ERROR, description = "Server failed to answer request.", body = GenericServerError, example = json!({ + "error": "Failed to query database", + })) + ) +)] #[get("/price-history/{name}")] async fn price_history( path: web::Path, @@ -46,52 +86,13 @@ async fn price_history( .as_deref() .map(util::parse_canteens_comma_separated); let dish_name = path.into_inner(); - let limit = query.limit.unwrap_or(1000) as i64; + let limit = query.limit.unwrap_or(1000).clamp(1, 1000) as i64; if let Some(canteens) = canteens { if canteens.iter().all(Result::is_ok) { let canteens = canteens.into_iter().filter_map(Result::ok).collect_vec(); - if canteens.len() == 1 { - let canteen = canteens.into_iter().next().expect("length is 1"); - - let res = sqlx::query!( - r#"SELECT date, price_students, price_employees, price_guests FROM meals WHERE canteen = $1 AND LOWER("name") = $2 AND is_latest = TRUE ORDER BY date DESC LIMIT $3;"#, - canteen.get_identifier(), - dish_name.to_lowercase(), - limit, - ) - .fetch_all(db) - .await; - - match res { - Ok(recs) => { - let structured = recs - .into_iter() - .map(|r| { - ( - r.date, - DishPrices { - students: r.price_students, - employees: r.price_employees, - guests: r.price_guests, - } - .normalize(), - ) - }) - .collect::>(); - - HttpResponse::Ok().json(structured) - } - Err(err) => { - tracing::error!("Failed to query database: {err:?}"); - HttpResponse::InternalServerError().json(json!({ - "error": "Failed to query database", - })) - } - } - } else { - let res = sqlx::query_as!(PriceHistoryRow, + let res = sqlx::query_as!(PriceHistoryRow, r#"SELECT date, canteen, price_students, price_employees, price_guests FROM meals WHERE canteen = ANY($1) AND LOWER("name") = $2 AND is_latest = TRUE ORDER BY date DESC LIMIT $3;"#, &canteens.iter().map(|c| c.get_identifier().to_string()).collect_vec(), dish_name.to_lowercase(), @@ -100,18 +101,17 @@ async fn price_history( .fetch_all(db) .await; - match res { - Ok(recs) => { - let structured = structure_multiple_canteens(recs); + match res { + Ok(recs) => { + let structured = structure_multiple_canteens(recs); - HttpResponse::Ok().json(structured) - } - Err(err) => { - tracing::error!("Failed to query database: {err:?}"); - HttpResponse::InternalServerError().json(json!({ - "error": "Failed to query database", - })) - } + HttpResponse::Ok().json(structured) + } + Err(err) => { + tracing::error!("Failed to query database: {err:?}"); + HttpResponse::InternalServerError().json(json!({ + "error": "Failed to query database", + })) } } } else { diff --git a/web-api/src/main.rs b/web-api/src/main.rs index 8c5f4c5..ac7ece0 100644 --- a/web-api/src/main.rs +++ b/web-api/src/main.rs @@ -12,6 +12,13 @@ use mensa_upb_api::get_governor; use sqlx::postgres::PgPoolOptions; use tracing::{debug, error, info, level_filters::LevelFilter}; use tracing_subscriber::EnvFilter; +use utoipa::OpenApi as _; +use utoipa_actix_web::AppExt as _; +use utoipa_rapidoc::RapiDoc; + +#[derive(utoipa::OpenApi)] +#[openapi(info(title = "Mensa UPB API"))] +struct ApiDoc; #[tokio::main] async fn main() -> Result<()> { @@ -80,7 +87,13 @@ async fn main() -> Result<()> { .wrap(Governor::new(&governor_conf)) .wrap(cors) .app_data(web::Data::new(db.clone())) + .into_utoipa_app() + .openapi(ApiDoc::openapi()) .configure(mensa_upb_api::endpoints::configure) + .openapi_service(|api| { + RapiDoc::with_openapi("/api-docs/openapi.json", api).path("/rapidoc") + }) + .into_app() }) .bind((interface.as_str(), port))? .run() diff --git a/web-api/src/menu.rs b/web-api/src/menu.rs index e6a4280..ff57da2 100644 --- a/web-api/src/menu.rs +++ b/web-api/src/menu.rs @@ -7,7 +7,7 @@ use std::str::FromStr as _; use crate::{Dish, DishPrices}; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)] pub struct Menu { date: NaiveDate, main_dishes: Vec, @@ -27,21 +27,17 @@ impl Menu { .map(|c| c.get_identifier().to_string()) .collect::>(); - let query_db = async || { - sqlx::query!(r#"SELECT name, array_agg(DISTINCT canteen ORDER BY canteen) AS "canteens!", dish_type AS "dish_type: DishType", image_src, price_students, price_employees, price_guests, vegan, vegetarian + if allow_refresh { + check_refresh(db, date, canteens).await; + }; + + let result = sqlx::query!(r#"SELECT name, array_agg(DISTINCT canteen ORDER BY canteen) AS "canteens!", dish_type AS "dish_type: DishType", image_src, price_students, price_employees, price_guests, vegan, vegetarian FROM meals WHERE date = $1 AND canteen = ANY($2) AND is_latest = TRUE GROUP BY name, dish_type, image_src, price_students, price_employees, price_guests, vegan, vegetarian ORDER BY name"#, date, &canteens_str) .fetch_all(db) - .await - }; - - let mut result = query_db().await?; - - if allow_refresh && check_refresh(db, date, canteens).await { - result = query_db().await?; - } + .await?; let mut main_dishes = Vec::new(); let mut side_dishes = Vec::new(); diff --git a/web-api/src/util.rs b/web-api/src/util.rs index 4c008da..408139a 100644 --- a/web-api/src/util.rs +++ b/web-api/src/util.rs @@ -5,3 +5,9 @@ use shared::Canteen; pub fn parse_canteens_comma_separated(s: &str) -> Vec> { s.split(',').map(Canteen::from_str).collect() } + +#[expect(dead_code)] +#[derive(utoipa::ToSchema)] +pub(crate) struct GenericServerError { + error: &'static str, +}