From da72ab1446d419a3d25052dd4bc19cc0e38aa804 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 4 May 2026 11:19:51 +0200 Subject: [PATCH] feat(metadata): Implement OMDB metadata provider and refactor metadata client - Added `OmdbProvider` to fetch movie metadata from the OMDB API. - Refactored `MetadataClient` to use `MetadataSearchCriteria` for fetching movie metadata. - Updated `MetadataClientImpl` to support fetching metadata using OMDB. - Modified `log_review` use case to utilize the new metadata fetching mechanism. - Updated tests and presentation layer to accommodate changes in metadata handling. - Added dependencies for `reqwest` and `async-trait` in relevant `Cargo.toml` files. --- .env.example | 3 +- Cargo.lock | 632 +++++++++++++++++- Cargo.toml | 13 +- crates/adapters/metadata/Cargo.toml | 4 + crates/adapters/metadata/src/lib.rs | 58 +- crates/adapters/metadata/src/omdb.rs | 119 ++++ .../application/src/use_cases/log_review.rs | 7 +- crates/domain/src/ports.rs | 7 +- crates/presentation/Cargo.toml | 1 + crates/presentation/src/extractors.rs | 2 +- crates/presentation/src/main.rs | 29 +- crates/presentation/tests/api_test.rs | 6 +- 12 files changed, 827 insertions(+), 54 deletions(-) create mode 100644 crates/adapters/metadata/src/omdb.rs diff --git a/.env.example b/.env.example index 55bdb81..2f9b67e 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ DATABASE_URL=sqlite:./dev.db PORT=3000 JWT_SECRET= JWT_TTL_SECONDS= -ALLOW_REGISTRATION=true \ No newline at end of file +ALLOW_REGISTRATION=true +OMDB_API_KEY= \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7b68908..7a2d767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,7 +143,7 @@ dependencies = [ "chrono", "domain", "jsonwebtoken", - "rand_core", + "rand_core 0.6.4", "serde", "uuid", ] @@ -154,6 +154,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.9" @@ -291,6 +313,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -300,6 +324,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -314,6 +344,25 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "common" version = "0.1.0" @@ -336,6 +385,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -453,6 +522,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -471,6 +546,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -526,6 +610,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -541,6 +631,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -635,6 +731,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -643,7 +753,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -654,6 +764,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -780,6 +909,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -788,6 +918,22 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] @@ -796,13 +942,23 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -950,12 +1106,87 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.97" @@ -1054,6 +1285,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1088,6 +1325,12 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metadata" version = "0.1.0" +dependencies = [ + "async-trait", + "domain", + "reqwest", + "serde", +] [[package]] name = "mime" @@ -1146,7 +1389,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1193,6 +1436,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "parking" version = "2.2.1" @@ -1229,7 +1478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1334,6 +1583,7 @@ dependencies = [ "domain", "dotenvy", "http-body-util", + "metadata", "serde", "serde_json", "sqlite", @@ -1367,6 +1617,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1376,6 +1682,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1389,8 +1701,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1400,7 +1722,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1412,6 +1744,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1447,6 +1788,47 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -1474,7 +1856,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1491,12 +1873,22 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -1505,21 +1897,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1537,12 +1970,53 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1669,9 +2143,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple_asn1" version = "0.6.4" @@ -1858,7 +2348,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -1897,7 +2387,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -1974,6 +2464,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1986,6 +2479,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "template-askama" version = "0.1.0" @@ -2109,6 +2623,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2164,12 +2688,14 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", + "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", @@ -2249,6 +2775,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.0" @@ -2348,6 +2880,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2391,6 +2942,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.120" @@ -2457,6 +3018,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -2485,6 +3075,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2526,6 +3125,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 5b5e9eb..d28bc20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "crates/adapters/auth", "crates/adapters/metadata", "crates/adapters/rss", - "crates/adapters/sqlite", "crates/adapters/template-askama", + "crates/adapters/sqlite", + "crates/adapters/template-askama", "crates/application", "crates/common", "crates/domain", @@ -23,8 +24,13 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } async-trait = "0.1" uuid = { version = "1.23.0", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } -sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "uuid", "macros"] } -template-askama = { path = "crates/adapters/template-askama" } +sqlx = { version = "0.8.6", features = [ + "runtime-tokio-rustls", + "sqlite", + "uuid", + "macros", +] } +reqwest = { version = "0.13", features = ["json", "query"] } domain = { path = "crates/domain" } common = { path = "crates/common" } @@ -34,3 +40,4 @@ auth = { path = "crates/adapters/auth" } metadata = { path = "crates/adapters/metadata" } rss = { path = "crates/adapters/rss" } sqlite = { path = "crates/adapters/sqlite" } +template-askama = { path = "crates/adapters/template-askama" } diff --git a/crates/adapters/metadata/Cargo.toml b/crates/adapters/metadata/Cargo.toml index c09029a..0e5a596 100644 --- a/crates/adapters/metadata/Cargo.toml +++ b/crates/adapters/metadata/Cargo.toml @@ -4,3 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +async-trait = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +domain = { workspace = true } diff --git a/crates/adapters/metadata/src/lib.rs b/crates/adapters/metadata/src/lib.rs index b93cf3f..cad0ec6 100644 --- a/crates/adapters/metadata/src/lib.rs +++ b/crates/adapters/metadata/src/lib.rs @@ -1,14 +1,54 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use async_trait::async_trait; +use domain::{ + errors::DomainError, + models::Movie, + ports::{MetadataClient, MetadataSearchCriteria}, + value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear}, +}; + +mod omdb; + +pub(crate) struct ProviderMovie { + pub imdb_id: ExternalMetadataId, + pub title: MovieTitle, + pub release_year: ReleaseYear, + pub director: Option, + pub poster_url: Option, } -#[cfg(test)] -mod tests { - use super::*; +#[async_trait] +pub(crate) trait MetadataProvider: Send + Sync { + async fn fetch(&self, criteria: &MetadataSearchCriteria) -> Result; +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +pub struct MetadataClientImpl { + provider: Box, +} + +impl MetadataClientImpl { + pub fn new_omdb(api_key: String) -> Self { + Self { + provider: Box::new(omdb::OmdbProvider::new(api_key)), + } + } +} + +#[async_trait] +impl MetadataClient for MetadataClientImpl { + async fn fetch_movie_metadata( + &self, + criteria: &MetadataSearchCriteria, + ) -> Result { + let pm = self.provider.fetch(criteria).await?; + Ok(Movie::new(Some(pm.imdb_id), pm.title, pm.release_year, pm.director, None)) + } + + async fn get_poster_url( + &self, + external_metadata_id: &ExternalMetadataId, + ) -> Result, DomainError> { + let criteria = MetadataSearchCriteria::ImdbId(external_metadata_id.clone()); + let pm = self.provider.fetch(&criteria).await?; + Ok(pm.poster_url) } } diff --git a/crates/adapters/metadata/src/omdb.rs b/crates/adapters/metadata/src/omdb.rs new file mode 100644 index 0000000..87de19d --- /dev/null +++ b/crates/adapters/metadata/src/omdb.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use domain::{ + errors::DomainError, + ports::MetadataSearchCriteria, + value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear}, +}; +use serde::Deserialize; + +use crate::{MetadataProvider, ProviderMovie}; + +pub(crate) struct OmdbProvider { + client: reqwest::Client, + api_key: String, + base_url: String, +} + +impl OmdbProvider { + pub(crate) fn new(api_key: String) -> Self { + Self { + client: reqwest::Client::new(), + api_key, + base_url: "http://www.omdbapi.com/".to_string(), + } + } +} + +#[derive(Deserialize)] +struct OmdbResponse { + #[serde(rename = "Title")] + title: String, + #[serde(rename = "Year")] + year: String, + #[serde(rename = "Director")] + director: String, + #[serde(rename = "Poster")] + poster: String, + #[serde(rename = "imdbID")] + imdb_id: String, + #[serde(rename = "Response")] + response: String, + #[serde(rename = "Error")] + error: Option, +} + +#[async_trait] +impl MetadataProvider for OmdbProvider { + async fn fetch(&self, criteria: &MetadataSearchCriteria) -> Result { + let mut url = reqwest::Url::parse(&self.base_url) + .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; + + { + let mut params = url.query_pairs_mut(); + params.append_pair("apikey", &self.api_key); + match criteria { + MetadataSearchCriteria::ImdbId(id) => { + params.append_pair("i", id.value()); + } + MetadataSearchCriteria::Title { title, year } => { + params.append_pair("t", title); + if let Some(y) = year { + params.append_pair("y", &y.to_string()); + } + } + } + } + + let http_resp = self + .client + .get(url) + .send() + .await + .map_err(|e: reqwest::Error| DomainError::InfrastructureError(e.to_string()))? + .error_for_status() + .map_err(|e: reqwest::Error| DomainError::InfrastructureError(e.to_string()))?; + + let resp: OmdbResponse = http_resp + .json() + .await + .map_err(|e: reqwest::Error| DomainError::InfrastructureError(e.to_string()))?; + + if resp.response != "True" { + let msg = resp.error.unwrap_or_default(); + return if msg.to_lowercase().contains("not found") { + Err(DomainError::NotFound(msg)) + } else { + Err(DomainError::InfrastructureError(msg)) + }; + } + + let year: u16 = resp + .year + .chars() + .take(4) + .collect::() + .parse() + .map_err(|_| { + DomainError::InfrastructureError(format!("Unparseable year: {}", resp.year)) + })?; + + let imdb_id = ExternalMetadataId::new(resp.imdb_id) + .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; + let title = MovieTitle::new(resp.title) + .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; + let release_year = ReleaseYear::new(year) + .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; + + let director = match resp.director.as_str() { + "N/A" | "" => None, + d => Some(d.to_string()), + }; + + let poster_url = match resp.poster.as_str() { + "N/A" | "" => None, + url => PosterUrl::new(url.to_string()).ok(), + }; + + Ok(ProviderMovie { imdb_id, title, release_year, director, poster_url }) + } +} diff --git a/crates/application/src/use_cases/log_review.rs b/crates/application/src/use_cases/log_review.rs index 752ec9b..670d764 100644 --- a/crates/application/src/use_cases/log_review.rs +++ b/crates/application/src/use_cases/log_review.rs @@ -2,6 +2,7 @@ use domain::{ errors::DomainError, events::DomainEvent, models::{Movie, Review}, + ports::MetadataSearchCriteria, value_objects::{Comment, ExternalMetadataId, MovieTitle, Rating, ReleaseYear, UserId}, }; @@ -47,7 +48,11 @@ async fn resolve_external_movie( return Ok(Some((m, false))); } - match ctx.metadata_client.fetch_movie_metadata(&tmdb_id).await { + match ctx + .metadata_client + .fetch_movie_metadata(&MetadataSearchCriteria::ImdbId(tmdb_id)) + .await + { Ok(m) => Ok(Some((m, true))), Err(e) => { tracing::warn!( diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index bfefd74..f408c01 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -34,11 +34,16 @@ pub trait MovieRepository: Send + Sync { async fn get_review_history(&self, movie_id: &MovieId) -> Result; } +pub enum MetadataSearchCriteria { + ImdbId(ExternalMetadataId), + Title { title: String, year: Option }, +} + #[async_trait] pub trait MetadataClient: Send + Sync { async fn fetch_movie_metadata( &self, - external_metadata_id: &ExternalMetadataId, + criteria: &MetadataSearchCriteria, ) -> Result; async fn get_poster_url( &self, diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index 201dccb..e0a50f4 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -22,6 +22,7 @@ async-trait = { workspace = true } domain = { workspace = true } application = { workspace = true } auth = { workspace = true } +metadata = { workspace = true } sqlite = { workspace = true } sqlx = { workspace = true } template-askama = { workspace = true } diff --git a/crates/presentation/src/extractors.rs b/crates/presentation/src/extractors.rs index 1498745..1a8c0ef 100644 --- a/crates/presentation/src/extractors.rs +++ b/crates/presentation/src/extractors.rs @@ -80,7 +80,7 @@ mod tests { } struct PanicMeta; struct PanicFetcher; struct PanicStorage; struct PanicEvent; struct PanicHasher; struct PanicAuth; struct PanicUserRepo; - #[async_trait::async_trait] impl domain::ports::MetadataClient for PanicMeta { async fn fetch_movie_metadata(&self, _: &domain::value_objects::ExternalMetadataId) -> Result { panic!() } async fn get_poster_url(&self, _: &domain::value_objects::ExternalMetadataId) -> Result, domain::errors::DomainError> { panic!() } } + #[async_trait::async_trait] impl domain::ports::MetadataClient for PanicMeta { async fn fetch_movie_metadata(&self, _: &domain::ports::MetadataSearchCriteria) -> Result { panic!() } async fn get_poster_url(&self, _: &domain::value_objects::ExternalMetadataId) -> Result, domain::errors::DomainError> { panic!() } } #[async_trait::async_trait] impl domain::ports::PosterFetcherClient for PanicFetcher { async fn fetch_poster_bytes(&self, _: &domain::value_objects::PosterUrl) -> Result, domain::errors::DomainError> { panic!() } } #[async_trait::async_trait] impl domain::ports::PosterStorage for PanicStorage { async fn store_poster(&self, _: &domain::value_objects::MovieId, _: &[u8]) -> Result { panic!() } async fn get_poster(&self, _: &domain::value_objects::PosterPath) -> Result, domain::errors::DomainError> { panic!() } } #[async_trait::async_trait] impl domain::ports::EventPublisher for PanicEvent { async fn publish(&self, _: &domain::events::DomainEvent) -> Result<(), domain::errors::DomainError> { panic!() } } diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index cb5c6d5..daf9e0a 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -5,9 +5,8 @@ use async_trait::async_trait; use domain::{ errors::DomainError, events::DomainEvent, - models::Movie, - ports::{EventPublisher, MetadataClient, PosterFetcherClient, PosterStorage}, - value_objects::{ExternalMetadataId, MovieId, PosterPath, PosterUrl}, + ports::{EventPublisher, PosterFetcherClient, PosterStorage}, + value_objects::{MovieId, PosterPath, PosterUrl}, }; use sqlx::SqlitePool; use tokio::net::TcpListener; @@ -15,31 +14,12 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use application::{config::AppConfig, context::AppContext}; use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService}; +use metadata::MetadataClientImpl; use sqlite::{SqliteMovieRepository, SqliteUserRepository}; use template_askama::AskamaHtmlRenderer; use presentation::{routes, state::AppState}; -struct StubMetadataClient; - -#[async_trait] -impl MetadataClient for StubMetadataClient { - async fn fetch_movie_metadata(&self, _id: &ExternalMetadataId) -> Result { - Err(DomainError::InfrastructureError( - "metadata client not implemented".into(), - )) - } - - async fn get_poster_url( - &self, - _id: &ExternalMetadataId, - ) -> Result, DomainError> { - Err(DomainError::InfrastructureError( - "metadata client not implemented".into(), - )) - } -} - struct StubPosterFetcher; #[async_trait] @@ -102,6 +82,7 @@ async fn main() -> anyhow::Result<()> { async fn wire_dependencies() -> anyhow::Result { let auth_config = AuthConfig::from_env()?; let app_config = AppConfig::from_env(); + let omdb_api_key = std::env::var("OMDB_API_KEY").context("OMDB_API_KEY must be set")?; let pool = SqlitePool::connect("sqlite://reviews.db") .await @@ -118,7 +99,7 @@ async fn wire_dependencies() -> anyhow::Result { let app_ctx = AppContext { repository: Arc::new(movie_repo), - metadata_client: Arc::new(StubMetadataClient), + metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)), poster_fetcher: Arc::new(StubPosterFetcher), poster_storage: Arc::new(StubPosterStorage), event_publisher: Arc::new(StubEventPublisher), diff --git a/crates/presentation/tests/api_test.rs b/crates/presentation/tests/api_test.rs index 5f29aca..bd900b5 100644 --- a/crates/presentation/tests/api_test.rs +++ b/crates/presentation/tests/api_test.rs @@ -12,8 +12,8 @@ use domain::{ events::DomainEvent, models::{Movie, User}, ports::{ - AuthService, EventPublisher, GeneratedToken, MetadataClient, PasswordHasher, - PosterFetcherClient, PosterStorage, UserRepository, + AuthService, EventPublisher, GeneratedToken, MetadataClient, MetadataSearchCriteria, + PasswordHasher, PosterFetcherClient, PosterStorage, UserRepository, }, value_objects::{ Email, ExternalMetadataId, MovieId, PasswordHash, PosterPath, PosterUrl, UserId, @@ -37,7 +37,7 @@ impl EventPublisher for NoopEventPublisher { struct PanicMeta; #[async_trait] impl MetadataClient for PanicMeta { - async fn fetch_movie_metadata(&self, _: &ExternalMetadataId) -> Result { + async fn fetch_movie_metadata(&self, _: &MetadataSearchCriteria) -> Result { panic!("metadata not wired in tests") } async fn get_poster_url(&self, _: &ExternalMetadataId) -> Result, DomainError> {