diff --git a/Cargo.lock b/Cargo.lock index ad0fedf..9db77f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "application" version = "0.1.0" @@ -10,6 +16,121 @@ dependencies = [ "tokio", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bootstrap" version = "0.1.0" @@ -22,12 +143,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "client-application" version = "0.1.0" @@ -65,6 +208,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config-memory" version = "0.1.0" @@ -72,6 +224,95 @@ dependencies = [ "domain", ] +[[package]] +name = "config-sqlite" +version = "0.1.0" +dependencies = [ + "domain", + "serde", + "serde_json", + "sqlx", + "tokio", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "display-terminal" version = "0.1.0" @@ -79,10 +320,36 @@ dependencies = [ "client-domain", ] +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "domain" version = "0.1.0" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -95,12 +362,531 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-api" +version = "0.1.0" +dependencies = [ + "application", + "axum", + "config-memory", + "domain", + "serde", + "serde_json", + "tcp-server", + "tokio", + "tower", + "tower-http", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.1" @@ -109,15 +895,144 @@ checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "postcard" version = "1.1.3" @@ -130,6 +1045,24 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -157,6 +1090,134 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -187,6 +1248,95 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.4" @@ -194,9 +1344,241 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.118" @@ -208,6 +1590,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tcp-client" version = "0.1.0" @@ -246,6 +1645,31 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -258,7 +1682,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -272,24 +1696,224 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -298,3 +1922,239 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index cfcf118..c195817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ members = [ "crates/adapters/tcp-server", "crates/adapters/tcp-client", "crates/adapters/display-terminal", + "crates/adapters/config-sqlite", + "crates/adapters/http-api", "crates/bootstrap", "crates/client-desktop", ] @@ -27,6 +29,13 @@ config-memory = { path = "crates/adapters/config-memory" } tcp-server = { path = "crates/adapters/tcp-server" } tcp-client = { path = "crates/adapters/tcp-client" } display-terminal = { path = "crates/adapters/display-terminal" } +config-sqlite = { path = "crates/adapters/config-sqlite" } +http-api = { path = "crates/adapters/http-api" } +axum = { version = "0.8", features = ["macros"] } +tower-http = { version = "0.6", features = ["cors"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } +serde_json = "1.0" +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } postcard = { version = "1.1", default-features = false, features = ["alloc"] } tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread", "net", "sync", "time", "io-util"] } +tower = "0.5" diff --git a/crates/adapters/config-sqlite/Cargo.toml b/crates/adapters/config-sqlite/Cargo.toml new file mode 100644 index 0000000..397db10 --- /dev/null +++ b/crates/adapters/config-sqlite/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "config-sqlite" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain.workspace = true +sqlx.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +tokio.workspace = true diff --git a/crates/adapters/config-sqlite/src/lib.rs b/crates/adapters/config-sqlite/src/lib.rs new file mode 100644 index 0000000..571f617 --- /dev/null +++ b/crates/adapters/config-sqlite/src/lib.rs @@ -0,0 +1,262 @@ +mod serialization; + +use std::time::Duration; +use sqlx::{SqlitePool, Row}; +use domain::{ + ConfigRepository, + DataSource, DataSourceId, DataSourceConfig, DataSourceType, + Layout, LayoutPreset, LayoutPresetId, + WidgetConfig, WidgetId, +}; +use serialization as ser; + +#[derive(Debug)] +pub enum SqliteConfigError { + Sql(sqlx::Error), + Serialization(String), +} + +impl std::fmt::Display for SqliteConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SqliteConfigError::Sql(e) => write!(f, "sql: {e}"), + SqliteConfigError::Serialization(e) => write!(f, "serialization: {e}"), + } + } +} + +pub struct SqliteConfigStore { + pool: SqlitePool, +} + +impl SqliteConfigStore { + pub async fn new(database_url: &str) -> Result { + let pool = SqlitePool::connect(database_url).await?; + let store = Self { pool }; + store.migrate().await?; + Ok(store) + } + + async fn migrate(&self) -> Result<(), sqlx::Error> { + sqlx::query( + "CREATE TABLE IF NOT EXISTS widgets ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + display_hint TEXT NOT NULL, + data_source_id INTEGER NOT NULL, + mappings TEXT NOT NULL, + max_data_size INTEGER NOT NULL + )" + ).execute(&self.pool).await?; + + sqlx::query( + "CREATE TABLE IF NOT EXISTS data_sources ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + source_type TEXT NOT NULL, + poll_interval_secs INTEGER NOT NULL, + config TEXT NOT NULL + )" + ).execute(&self.pool).await?; + + sqlx::query( + "CREATE TABLE IF NOT EXISTS layout ( + id INTEGER PRIMARY KEY CHECK (id = 1), + data TEXT NOT NULL + )" + ).execute(&self.pool).await?; + + sqlx::query( + "CREATE TABLE IF NOT EXISTS presets ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + layout_data TEXT NOT NULL + )" + ).execute(&self.pool).await?; + + Ok(()) + } +} + +impl ConfigRepository for SqliteConfigStore { + type Error = SqliteConfigError; + + async fn get_widget(&self, id: WidgetId) -> Result, Self::Error> { + let row = sqlx::query("SELECT * FROM widgets WHERE id = ?") + .bind(id as i64) + .fetch_optional(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + match row { + None => Ok(None), + Some(row) => Ok(Some(ser::widget_from_row(&row)?)), + } + } + + async fn list_widgets(&self) -> Result, Self::Error> { + let rows = sqlx::query("SELECT * FROM widgets") + .fetch_all(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + rows.iter().map(|r| ser::widget_from_row(r)).collect() + } + + async fn save_widget(&self, config: &WidgetConfig) -> Result<(), Self::Error> { + let mappings_json = ser::mappings_to_json(&config.mappings)?; + let hint_str = ser::display_hint_to_str(&config.display_hint); + + sqlx::query( + "INSERT OR REPLACE INTO widgets (id, name, display_hint, data_source_id, mappings, max_data_size) + VALUES (?, ?, ?, ?, ?, ?)" + ) + .bind(config.id as i64) + .bind(&config.name) + .bind(hint_str) + .bind(config.data_source_id as i64) + .bind(&mappings_json) + .bind(config.max_data_size as i64) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + Ok(()) + } + + async fn delete_widget(&self, id: WidgetId) -> Result<(), Self::Error> { + sqlx::query("DELETE FROM widgets WHERE id = ?") + .bind(id as i64) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + Ok(()) + } + + async fn get_data_source(&self, id: DataSourceId) -> Result, Self::Error> { + let row = sqlx::query("SELECT * FROM data_sources WHERE id = ?") + .bind(id as i64) + .fetch_optional(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + match row { + None => Ok(None), + Some(row) => Ok(Some(ser::data_source_from_row(&row)?)), + } + } + + async fn list_data_sources(&self) -> Result, Self::Error> { + let rows = sqlx::query("SELECT * FROM data_sources") + .fetch_all(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + rows.iter().map(|r| ser::data_source_from_row(r)).collect() + } + + async fn save_data_source(&self, source: &DataSource) -> Result<(), Self::Error> { + let config_json = ser::data_source_config_to_json(&source.config)?; + let type_str = ser::data_source_type_to_str(&source.source_type); + + sqlx::query( + "INSERT OR REPLACE INTO data_sources (id, name, source_type, poll_interval_secs, config) + VALUES (?, ?, ?, ?, ?)" + ) + .bind(source.id as i64) + .bind(&source.name) + .bind(type_str) + .bind(source.poll_interval.as_secs() as i64) + .bind(&config_json) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + Ok(()) + } + + async fn delete_data_source(&self, id: DataSourceId) -> Result<(), Self::Error> { + sqlx::query("DELETE FROM data_sources WHERE id = ?") + .bind(id as i64) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + Ok(()) + } + + async fn get_layout(&self) -> Result, Self::Error> { + let row = sqlx::query("SELECT data FROM layout WHERE id = 1") + .fetch_optional(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + match row { + None => Ok(None), + Some(row) => { + let json: String = row.get("data"); + Ok(Some(ser::layout_from_json(&json)?)) + } + } + } + + async fn save_layout(&self, layout: &Layout) -> Result<(), Self::Error> { + let json = ser::layout_to_json(layout)?; + + sqlx::query( + "INSERT OR REPLACE INTO layout (id, data) VALUES (1, ?)" + ) + .bind(&json) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + Ok(()) + } + + async fn get_preset(&self, id: LayoutPresetId) -> Result, Self::Error> { + let row = sqlx::query("SELECT * FROM presets WHERE id = ?") + .bind(id as i64) + .fetch_optional(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + match row { + None => Ok(None), + Some(row) => Ok(Some(ser::preset_from_row(&row)?)), + } + } + + async fn list_presets(&self) -> Result, Self::Error> { + let rows = sqlx::query("SELECT * FROM presets") + .fetch_all(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + rows.iter().map(|r| ser::preset_from_row(r)).collect() + } + + async fn save_preset(&self, preset: &LayoutPreset) -> Result<(), Self::Error> { + let layout_json = ser::layout_to_json(&preset.layout)?; + + sqlx::query( + "INSERT OR REPLACE INTO presets (id, name, layout_data) VALUES (?, ?, ?)" + ) + .bind(preset.id as i64) + .bind(&preset.name) + .bind(&layout_json) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + + Ok(()) + } + + async fn delete_preset(&self, id: LayoutPresetId) -> Result<(), Self::Error> { + sqlx::query("DELETE FROM presets WHERE id = ?") + .bind(id as i64) + .execute(&self.pool) + .await + .map_err(SqliteConfigError::Sql)?; + Ok(()) + } +} diff --git a/crates/adapters/config-sqlite/src/serialization.rs b/crates/adapters/config-sqlite/src/serialization.rs new file mode 100644 index 0000000..a2d4e1d --- /dev/null +++ b/crates/adapters/config-sqlite/src/serialization.rs @@ -0,0 +1,220 @@ +use std::time::Duration; +use sqlx::Row; +use sqlx::sqlite::SqliteRow; +use domain::{ + ContainerNode, DataSource, DataSourceConfig, DataSourceType, Direction, + DisplayHint, KeyMapping, Layout, LayoutChild, LayoutNode, LayoutPreset, + Sizing, WidgetConfig, +}; +use crate::SqliteConfigError; + +pub fn display_hint_to_str(hint: &DisplayHint) -> &'static str { + match hint { + DisplayHint::IconValue => "icon_value", + DisplayHint::TextBlock => "text_block", + DisplayHint::KeyValue => "key_value", + } +} + +fn display_hint_from_str(s: &str) -> Result { + match s { + "icon_value" => Ok(DisplayHint::IconValue), + "text_block" => Ok(DisplayHint::TextBlock), + "key_value" => Ok(DisplayHint::KeyValue), + _ => Err(SqliteConfigError::Serialization(format!("unknown display hint: {s}"))), + } +} + +pub fn data_source_type_to_str(t: &DataSourceType) -> &'static str { + match t { + DataSourceType::Weather => "weather", + DataSourceType::Media => "media", + DataSourceType::Xtb => "xtb", + DataSourceType::Rss => "rss", + DataSourceType::HttpJson => "http_json", + DataSourceType::Webhook => "webhook", + } +} + +fn data_source_type_from_str(s: &str) -> Result { + match s { + "weather" => Ok(DataSourceType::Weather), + "media" => Ok(DataSourceType::Media), + "xtb" => Ok(DataSourceType::Xtb), + "rss" => Ok(DataSourceType::Rss), + "http_json" => Ok(DataSourceType::HttpJson), + "webhook" => Ok(DataSourceType::Webhook), + _ => Err(SqliteConfigError::Serialization(format!("unknown source type: {s}"))), + } +} + +pub fn mappings_to_json(mappings: &[KeyMapping]) -> Result { + let entries: Vec = mappings.iter().map(|m| { + serde_json::json!({ + "source_path": m.source_path, + "target_key": m.target_key, + }) + }).collect(); + serde_json::to_string(&entries).map_err(|e| SqliteConfigError::Serialization(e.to_string())) +} + +fn mappings_from_json(json: &str) -> Result, SqliteConfigError> { + let entries: Vec = serde_json::from_str(json) + .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + + entries.iter().map(|v| { + Ok(KeyMapping { + source_path: v["source_path"].as_str() + .ok_or_else(|| SqliteConfigError::Serialization("missing source_path".into()))?.into(), + target_key: v["target_key"].as_str() + .ok_or_else(|| SqliteConfigError::Serialization("missing target_key".into()))?.into(), + }) + }).collect() +} + +pub fn data_source_config_to_json(config: &DataSourceConfig) -> Result { + let v = serde_json::json!({ + "url": config.url, + "headers": config.headers, + "api_key": config.api_key, + }); + serde_json::to_string(&v).map_err(|e| SqliteConfigError::Serialization(e.to_string())) +} + +fn data_source_config_from_json(json: &str) -> Result { + let v: serde_json::Value = serde_json::from_str(json) + .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + + let url = v["url"].as_str().map(String::from); + let api_key = v["api_key"].as_str().map(String::from); + let headers = match v["headers"].as_array() { + Some(arr) => arr.iter().filter_map(|h| { + let pair = h.as_array()?; + Some((pair[0].as_str()?.into(), pair[1].as_str()?.into())) + }).collect(), + None => vec![], + }; + + Ok(DataSourceConfig { url, headers, api_key }) +} + +pub fn layout_to_json(layout: &Layout) -> Result { + let v = node_to_json(&layout.root); + serde_json::to_string(&v).map_err(|e| SqliteConfigError::Serialization(e.to_string())) +} + +pub fn layout_from_json(json: &str) -> Result { + let v: serde_json::Value = serde_json::from_str(json) + .map_err(|e| SqliteConfigError::Serialization(e.to_string()))?; + let root = node_from_json(&v)?; + Ok(Layout { root }) +} + +fn node_to_json(node: &LayoutNode) -> serde_json::Value { + match node { + LayoutNode::Leaf(id) => serde_json::json!({ "type": "leaf", "widget_id": id }), + LayoutNode::Container(c) => { + let children: Vec = c.children.iter().map(|ch| { + let sizing = match &ch.sizing { + Sizing::Fixed(px) => serde_json::json!({ "type": "fixed", "value": px }), + Sizing::Flex(w) => serde_json::json!({ "type": "flex", "value": w }), + }; + serde_json::json!({ + "sizing": sizing, + "node": node_to_json(&ch.node), + }) + }).collect(); + + serde_json::json!({ + "type": "container", + "direction": match c.direction { Direction::Row => "row", Direction::Column => "column" }, + "gap": c.gap, + "padding": c.padding, + "children": children, + }) + } + } +} + +fn node_from_json(v: &serde_json::Value) -> Result { + let err = |msg: &str| SqliteConfigError::Serialization(msg.into()); + + match v["type"].as_str().ok_or_else(|| err("missing node type"))? { + "leaf" => { + let id = v["widget_id"].as_u64().ok_or_else(|| err("missing widget_id"))? as u16; + Ok(LayoutNode::Leaf(id)) + } + "container" => { + let direction = match v["direction"].as_str().ok_or_else(|| err("missing direction"))? { + "row" => Direction::Row, + "column" => Direction::Column, + d => return Err(err(&format!("unknown direction: {d}"))), + }; + let gap = v["gap"].as_u64().unwrap_or(0) as u8; + let padding = v["padding"].as_u64().unwrap_or(0) as u8; + let children = v["children"].as_array() + .ok_or_else(|| err("missing children"))? + .iter() + .map(|ch| { + let sizing_v = &ch["sizing"]; + let sizing = match sizing_v["type"].as_str().ok_or_else(|| err("missing sizing type"))? { + "fixed" => Sizing::Fixed(sizing_v["value"].as_u64().ok_or_else(|| err("missing fixed value"))? as u16), + "flex" => Sizing::Flex(sizing_v["value"].as_u64().ok_or_else(|| err("missing flex value"))? as u8), + s => return Err(err(&format!("unknown sizing: {s}"))), + }; + let node = node_from_json(&ch["node"])?; + Ok(LayoutChild { sizing, node }) + }) + .collect::, _>>()?; + + Ok(LayoutNode::Container(ContainerNode { direction, gap, padding, children })) + } + t => Err(err(&format!("unknown node type: {t}"))), + } +} + +pub fn widget_from_row(row: &SqliteRow) -> Result { + let id: i64 = row.get("id"); + let name: String = row.get("name"); + let hint_str: String = row.get("display_hint"); + let ds_id: i64 = row.get("data_source_id"); + let mappings_json: String = row.get("mappings"); + let max_size: i64 = row.get("max_data_size"); + + Ok(WidgetConfig { + id: id as u16, + name, + display_hint: display_hint_from_str(&hint_str)?, + data_source_id: ds_id as u16, + mappings: mappings_from_json(&mappings_json)?, + max_data_size: max_size as u16, + }) +} + +pub fn data_source_from_row(row: &SqliteRow) -> Result { + let id: i64 = row.get("id"); + let name: String = row.get("name"); + let type_str: String = row.get("source_type"); + let interval_secs: i64 = row.get("poll_interval_secs"); + let config_json: String = row.get("config"); + + Ok(DataSource { + id: id as u16, + name, + source_type: data_source_type_from_str(&type_str)?, + poll_interval: Duration::from_secs(interval_secs as u64), + config: data_source_config_from_json(&config_json)?, + }) +} + +pub fn preset_from_row(row: &SqliteRow) -> Result { + let id: i64 = row.get("id"); + let name: String = row.get("name"); + let layout_json: String = row.get("layout_data"); + + Ok(LayoutPreset { + id: id as u16, + name, + layout: layout_from_json(&layout_json)?, + }) +} diff --git a/crates/adapters/config-sqlite/tests/config_store_tests.rs b/crates/adapters/config-sqlite/tests/config_store_tests.rs new file mode 100644 index 0000000..e17e323 --- /dev/null +++ b/crates/adapters/config-sqlite/tests/config_store_tests.rs @@ -0,0 +1,203 @@ +use std::time::Duration; +use domain::{ + ConfigRepository, DisplayHint, KeyMapping, WidgetConfig, + DataSource, DataSourceConfig, DataSourceType, + Layout, LayoutNode, ContainerNode, LayoutChild, Direction, Sizing, + LayoutPreset, +}; +use config_sqlite::SqliteConfigStore; + +async fn test_store() -> SqliteConfigStore { + SqliteConfigStore::new("sqlite::memory:").await.unwrap() +} + +fn weather_widget() -> WidgetConfig { + WidgetConfig { + id: 1, + name: "weather".into(), + display_hint: DisplayHint::IconValue, + data_source_id: 10, + mappings: vec![ + KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() }, + KeyMapping { source_path: "$.icon".into(), target_key: "icon".into() }, + ], + max_data_size: 2048, + } +} + +fn weather_source() -> DataSource { + DataSource { + id: 10, + name: "openweather".into(), + source_type: DataSourceType::Weather, + poll_interval: Duration::from_secs(300), + config: DataSourceConfig { + url: Some("https://api.openweather.org".into()), + headers: vec![], + api_key: Some("test-key".into()), + }, + } +} + +fn test_layout() -> Layout { + Layout { + root: LayoutNode::Container(ContainerNode { + direction: Direction::Row, + gap: 4, + padding: 2, + children: vec![ + LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1) }, + LayoutChild { sizing: Sizing::Fixed(80), node: LayoutNode::Leaf(2) }, + ], + }), + } +} + +#[tokio::test] +async fn save_and_retrieve_widget() { + let store = test_store().await; + store.save_widget(&weather_widget()).await.unwrap(); + + let w = store.get_widget(1).await.unwrap().unwrap(); + assert_eq!(w.id, 1); + assert_eq!(w.name, "weather"); + assert_eq!(w.display_hint, DisplayHint::IconValue); + assert_eq!(w.data_source_id, 10); + assert_eq!(w.mappings.len(), 2); + assert_eq!(w.mappings[0].source_path, "$.temp"); + assert_eq!(w.max_data_size, 2048); +} + +#[tokio::test] +async fn get_nonexistent_widget_returns_none() { + let store = test_store().await; + assert!(store.get_widget(99).await.unwrap().is_none()); +} + +#[tokio::test] +async fn list_widgets_returns_all() { + let store = test_store().await; + store.save_widget(&weather_widget()).await.unwrap(); + store.save_widget(&WidgetConfig { + id: 2, + name: "portfolio".into(), + display_hint: DisplayHint::KeyValue, + data_source_id: 20, + mappings: vec![], + max_data_size: 1024, + }).await.unwrap(); + + let widgets = store.list_widgets().await.unwrap(); + assert_eq!(widgets.len(), 2); +} + +#[tokio::test] +async fn delete_widget_removes_it() { + let store = test_store().await; + store.save_widget(&weather_widget()).await.unwrap(); + store.delete_widget(1).await.unwrap(); + assert!(store.get_widget(1).await.unwrap().is_none()); +} + +#[tokio::test] +async fn save_and_retrieve_data_source() { + let store = test_store().await; + store.save_data_source(&weather_source()).await.unwrap(); + + let ds = store.get_data_source(10).await.unwrap().unwrap(); + assert_eq!(ds.id, 10); + assert_eq!(ds.name, "openweather"); + assert_eq!(ds.source_type, DataSourceType::Weather); + assert_eq!(ds.poll_interval, Duration::from_secs(300)); + assert_eq!(ds.config.url, Some("https://api.openweather.org".into())); + assert_eq!(ds.config.api_key, Some("test-key".into())); +} + +#[tokio::test] +async fn list_and_delete_data_sources() { + let store = test_store().await; + store.save_data_source(&weather_source()).await.unwrap(); + assert_eq!(store.list_data_sources().await.unwrap().len(), 1); + + store.delete_data_source(10).await.unwrap(); + assert!(store.list_data_sources().await.unwrap().is_empty()); +} + +#[tokio::test] +async fn save_and_retrieve_layout() { + let store = test_store().await; + let layout = test_layout(); + store.save_layout(&layout).await.unwrap(); + + let loaded = store.get_layout().await.unwrap().unwrap(); + assert_eq!(loaded, layout); +} + +#[tokio::test] +async fn layout_starts_as_none() { + let store = test_store().await; + assert!(store.get_layout().await.unwrap().is_none()); +} + +#[tokio::test] +async fn save_layout_replaces_previous() { + let store = test_store().await; + store.save_layout(&test_layout()).await.unwrap(); + + let new_layout = Layout { + root: LayoutNode::Leaf(42), + }; + store.save_layout(&new_layout).await.unwrap(); + + let loaded = store.get_layout().await.unwrap().unwrap(); + assert_eq!(loaded, new_layout); +} + +#[tokio::test] +async fn save_and_retrieve_preset() { + let store = test_store().await; + let preset = LayoutPreset { + id: 1, + name: "dashboard".into(), + layout: test_layout(), + }; + store.save_preset(&preset).await.unwrap(); + + let loaded = store.get_preset(1).await.unwrap().unwrap(); + assert_eq!(loaded.id, 1); + assert_eq!(loaded.name, "dashboard"); + assert_eq!(loaded.layout, test_layout()); +} + +#[tokio::test] +async fn list_and_delete_presets() { + let store = test_store().await; + store.save_preset(&LayoutPreset { + id: 1, name: "a".into(), layout: test_layout(), + }).await.unwrap(); + store.save_preset(&LayoutPreset { + id: 2, name: "b".into(), layout: test_layout(), + }).await.unwrap(); + + assert_eq!(store.list_presets().await.unwrap().len(), 2); + + store.delete_preset(1).await.unwrap(); + assert_eq!(store.list_presets().await.unwrap().len(), 1); + assert!(store.get_preset(1).await.unwrap().is_none()); +} + +#[tokio::test] +async fn save_widget_updates_existing() { + let store = test_store().await; + store.save_widget(&weather_widget()).await.unwrap(); + + let mut updated = weather_widget(); + updated.name = "updated_weather".into(); + updated.max_data_size = 4096; + store.save_widget(&updated).await.unwrap(); + + let loaded = store.get_widget(1).await.unwrap().unwrap(); + assert_eq!(loaded.name, "updated_weather"); + assert_eq!(loaded.max_data_size, 4096); + assert_eq!(store.list_widgets().await.unwrap().len(), 1); +} diff --git a/crates/adapters/http-api/Cargo.toml b/crates/adapters/http-api/Cargo.toml new file mode 100644 index 0000000..fe96f51 --- /dev/null +++ b/crates/adapters/http-api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "http-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain.workspace = true +application.workspace = true +axum.workspace = true +tower-http.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +tokio.workspace = true +tower.workspace = true +serde_json.workspace = true +config-memory.workspace = true +tcp-server.workspace = true diff --git a/crates/adapters/http-api/src/dto.rs b/crates/adapters/http-api/src/dto.rs new file mode 100644 index 0000000..af576b6 --- /dev/null +++ b/crates/adapters/http-api/src/dto.rs @@ -0,0 +1,285 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct KeyMappingDto { + pub source_path: String, + pub target_key: String, +} + +#[derive(Serialize, Deserialize)] +pub struct WidgetDto { + pub id: u16, + pub name: String, + pub display_hint: String, + pub data_source_id: u16, + pub mappings: Vec, + pub max_data_size: u16, +} + +#[derive(Serialize, Deserialize)] +pub struct CreateWidgetDto { + pub id: u16, + pub name: String, + pub display_hint: String, + pub data_source_id: u16, + pub mappings: Vec, + #[serde(default = "default_max_data_size")] + pub max_data_size: u16, +} + +fn default_max_data_size() -> u16 { 2048 } + +#[derive(Serialize, Deserialize)] +pub struct DataSourceDto { + pub id: u16, + pub name: String, + pub source_type: String, + pub poll_interval_secs: u64, + pub url: Option, + pub api_key: Option, + pub headers: Vec<(String, String)>, +} + +#[derive(Serialize, Deserialize)] +pub struct SizingDto { + #[serde(rename = "type")] + pub sizing_type: String, + pub value: u16, +} + +#[derive(Serialize, Deserialize)] +pub struct LayoutNodeDto { + #[serde(rename = "type")] + pub node_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub widget_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub direction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gap: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub padding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub children: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct LayoutChildDto { + pub sizing: SizingDto, + pub node: LayoutNodeDto, +} + +#[derive(Serialize, Deserialize)] +pub struct LayoutDto { + pub root: LayoutNodeDto, +} + +#[derive(Serialize, Deserialize)] +pub struct PresetDto { + pub id: u16, + pub name: String, + pub layout: LayoutDto, +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePresetDto { + pub id: u16, + pub name: String, + pub layout: LayoutDto, +} + +use domain::*; +use std::time::Duration; + +impl From<&WidgetConfig> for WidgetDto { + fn from(w: &WidgetConfig) -> Self { + Self { + id: w.id, + name: w.name.clone(), + display_hint: match w.display_hint { + DisplayHint::IconValue => "icon_value", + DisplayHint::TextBlock => "text_block", + DisplayHint::KeyValue => "key_value", + }.into(), + data_source_id: w.data_source_id, + mappings: w.mappings.iter().map(|m| KeyMappingDto { + source_path: m.source_path.clone(), + target_key: m.target_key.clone(), + }).collect(), + max_data_size: w.max_data_size, + } + } +} + +impl CreateWidgetDto { + pub fn into_domain(self) -> Result { + let hint = match self.display_hint.as_str() { + "icon_value" => DisplayHint::IconValue, + "text_block" => DisplayHint::TextBlock, + "key_value" => DisplayHint::KeyValue, + h => return Err(format!("unknown display_hint: {h}")), + }; + Ok(WidgetConfig { + id: self.id, + name: self.name, + display_hint: hint, + data_source_id: self.data_source_id, + mappings: self.mappings.into_iter().map(|m| KeyMapping { + source_path: m.source_path, + target_key: m.target_key, + }).collect(), + max_data_size: self.max_data_size, + }) + } +} + +impl From<&DataSource> for DataSourceDto { + fn from(ds: &DataSource) -> Self { + Self { + id: ds.id, + name: ds.name.clone(), + source_type: match ds.source_type { + DataSourceType::Weather => "weather", + DataSourceType::Media => "media", + DataSourceType::Xtb => "xtb", + DataSourceType::Rss => "rss", + DataSourceType::HttpJson => "http_json", + DataSourceType::Webhook => "webhook", + }.into(), + poll_interval_secs: ds.poll_interval.as_secs(), + url: ds.config.url.clone(), + api_key: ds.config.api_key.clone(), + headers: ds.config.headers.clone(), + } + } +} + +impl DataSourceDto { + pub fn into_domain(self) -> Result { + let source_type = match self.source_type.as_str() { + "weather" => DataSourceType::Weather, + "media" => DataSourceType::Media, + "xtb" => DataSourceType::Xtb, + "rss" => DataSourceType::Rss, + "http_json" => DataSourceType::HttpJson, + "webhook" => DataSourceType::Webhook, + t => return Err(format!("unknown source_type: {t}")), + }; + Ok(DataSource { + id: self.id, + name: self.name, + source_type, + poll_interval: Duration::from_secs(self.poll_interval_secs), + config: DataSourceConfig { + url: self.url, + api_key: self.api_key, + headers: self.headers, + }, + }) + } +} + +impl From<&LayoutNode> for LayoutNodeDto { + fn from(node: &LayoutNode) -> Self { + match node { + LayoutNode::Leaf(id) => Self { + node_type: "leaf".into(), + widget_id: Some(*id), + direction: None, gap: None, padding: None, children: None, + }, + LayoutNode::Container(c) => Self { + node_type: "container".into(), + widget_id: None, + direction: Some(match c.direction { + Direction::Row => "row", + Direction::Column => "column", + }.into()), + gap: Some(c.gap), + padding: Some(c.padding), + children: Some(c.children.iter().map(|ch| LayoutChildDto { + sizing: SizingDto { + sizing_type: match ch.sizing { + Sizing::Fixed(_) => "fixed".into(), + Sizing::Flex(_) => "flex".into(), + }, + value: match ch.sizing { + Sizing::Fixed(v) => v, + Sizing::Flex(v) => v as u16, + }, + }, + node: (&ch.node).into(), + }).collect()), + }, + } + } +} + +impl LayoutNodeDto { + pub fn into_domain(self) -> Result { + match self.node_type.as_str() { + "leaf" => { + let id = self.widget_id.ok_or("missing widget_id")?; + Ok(LayoutNode::Leaf(id)) + } + "container" => { + let direction = match self.direction.as_deref().ok_or("missing direction")? { + "row" => Direction::Row, + "column" => Direction::Column, + d => return Err(format!("unknown direction: {d}")), + }; + let children = self.children.ok_or("missing children")? + .into_iter() + .map(|ch| { + let sizing = match ch.sizing.sizing_type.as_str() { + "fixed" => Sizing::Fixed(ch.sizing.value), + "flex" => Sizing::Flex(ch.sizing.value as u8), + s => return Err(format!("unknown sizing: {s}")), + }; + let node = ch.node.into_domain()?; + Ok(LayoutChild { sizing, node }) + }) + .collect::, _>>()?; + + Ok(LayoutNode::Container(ContainerNode { + direction, + gap: self.gap.unwrap_or(0), + padding: self.padding.unwrap_or(0), + children, + })) + } + t => Err(format!("unknown node type: {t}")), + } + } +} + +impl From<&Layout> for LayoutDto { + fn from(l: &Layout) -> Self { + Self { root: (&l.root).into() } + } +} + +impl LayoutDto { + pub fn into_domain(self) -> Result { + Ok(Layout { root: self.root.into_domain()? }) + } +} + +impl From<&LayoutPreset> for PresetDto { + fn from(p: &LayoutPreset) -> Self { + Self { + id: p.id, + name: p.name.clone(), + layout: (&p.layout).into(), + } + } +} + +impl CreatePresetDto { + pub fn into_domain(self) -> Result { + Ok(LayoutPreset { + id: self.id, + name: self.name, + layout: self.layout.into_domain()?, + }) + } +} diff --git a/crates/adapters/http-api/src/lib.rs b/crates/adapters/http-api/src/lib.rs new file mode 100644 index 0000000..4d81b93 --- /dev/null +++ b/crates/adapters/http-api/src/lib.rs @@ -0,0 +1,34 @@ +mod dto; +mod routes; + +use std::sync::Arc; +use axum::Router; +use tower_http::cors::CorsLayer; +use domain::{ConfigRepository, EventPublisher}; + +pub struct AppState { + pub config: Arc, + pub events: Arc, +} + +impl Clone for AppState { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + events: self.events.clone(), + } + } +} + +pub fn router(state: AppState) -> Router +where + C: ConfigRepository + Send + Sync + 'static, + C::Error: std::fmt::Debug + Send, + E: EventPublisher + Send + Sync + 'static, + E::Error: std::fmt::Debug + Send, +{ + Router::new() + .nest("/api", routes::api_routes()) + .layer(CorsLayer::permissive()) + .with_state(state) +} diff --git a/crates/adapters/http-api/src/routes.rs b/crates/adapters/http-api/src/routes.rs new file mode 100644 index 0000000..a589038 --- /dev/null +++ b/crates/adapters/http-api/src/routes.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; +use axum::{ + Router, + extract::{Path, State}, + http::StatusCode, + response::Json, + routing::{get, post, put, delete}, +}; +use domain::{ConfigRepository, EventPublisher}; +use application::ConfigService; +use crate::AppState; +use crate::dto::*; + +type S = State>; + +pub fn api_routes() -> Router> +where + C: ConfigRepository + Send + Sync + 'static, + C::Error: std::fmt::Debug + Send, + E: EventPublisher + Send + Sync + 'static, + E::Error: std::fmt::Debug + Send, +{ + Router::new() + .route("/widgets", get(list_widgets::).post(create_widget::)) + .route("/widgets/{id}", get(get_widget::).put(update_widget::).delete(delete_widget::)) + .route("/data-sources", get(list_data_sources::).post(create_data_source::)) + .route("/data-sources/{id}", get(get_data_source::).put(update_data_source::).delete(delete_data_source::)) + .route("/layout", get(get_layout::).put(update_layout::)) + .route("/presets", get(list_presets::).post(create_preset::)) + .route("/presets/{id}", get(get_preset::).delete(delete_preset::)) + .route("/presets/{id}/load", post(load_preset::)) +} + +async fn list_widgets(State(state): S) -> Result>, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let widgets = state.config.list_widgets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(widgets.iter().map(WidgetDto::from).collect())) +} + +async fn get_widget(State(state): S, Path(id): Path) -> Result, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let widget = state.config.get_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + match widget { + Some(w) => Ok(Json(WidgetDto::from(&w))), + None => Err(StatusCode::NOT_FOUND), + } +} + +async fn create_widget(State(state): S, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.create_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::CREATED) +} + +async fn update_widget(State(state): S, Path(_id): Path, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let widget = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.update_widget(widget).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::OK) +} + +async fn delete_widget(State(state): S, Path(id): Path) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.delete_widget(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::NO_CONTENT) +} + +async fn list_data_sources(State(state): S) -> Result>, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let sources = state.config.list_data_sources().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(sources.iter().map(DataSourceDto::from).collect())) +} + +async fn get_data_source(State(state): S, Path(id): Path) -> Result, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let source = state.config.get_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + match source { + Some(s) => Ok(Json(DataSourceDto::from(&s))), + None => Err(StatusCode::NOT_FOUND), + } +} + +async fn create_data_source(State(state): S, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.create_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::CREATED) +} + +async fn update_data_source(State(state): S, Path(_id): Path, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let source = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.update_data_source(source).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::OK) +} + +async fn delete_data_source(State(state): S, Path(id): Path) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.delete_data_source(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::NO_CONTENT) +} + +async fn get_layout(State(state): S) -> Result>, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let layout = state.config.get_layout().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(layout.as_ref().map(LayoutDto::from))) +} + +async fn update_layout(State(state): S, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let layout = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.update_layout(layout).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::OK) +} + +async fn list_presets(State(state): S) -> Result>, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let presets = state.config.list_presets().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(presets.iter().map(PresetDto::from).collect())) +} + +async fn get_preset(State(state): S, Path(id): Path) -> Result, StatusCode> +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let preset = state.config.get_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + match preset { + Some(p) => Ok(Json(PresetDto::from(&p))), + None => Err(StatusCode::NOT_FOUND), + } +} + +async fn create_preset(State(state): S, Json(body): Json) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let preset = body.into_domain().map_err(|e| (StatusCode::BAD_REQUEST, e))?; + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.save_preset(preset).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::CREATED) +} + +async fn delete_preset(State(state): S, Path(id): Path) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.delete_preset(id).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::NO_CONTENT) +} + +async fn load_preset(State(state): S, Path(id): Path) -> Result +where C: ConfigRepository, C::Error: std::fmt::Debug, E: EventPublisher, E::Error: std::fmt::Debug, +{ + let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + svc.load_preset(id).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; + Ok(StatusCode::OK) +} diff --git a/crates/adapters/http-api/tests/api_tests.rs b/crates/adapters/http-api/tests/api_tests.rs new file mode 100644 index 0000000..ee89238 --- /dev/null +++ b/crates/adapters/http-api/tests/api_tests.rs @@ -0,0 +1,149 @@ +use std::sync::Arc; +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use tower::ServiceExt; +use config_memory::MemoryConfigStore; +use tcp_server::TcpEventBus; +use http_api::{AppState, router}; + +fn test_app() -> axum::Router { + let config = Arc::new(MemoryConfigStore::new()); + let events = Arc::new(TcpEventBus::new(16)); + let state = AppState { config, events }; + router(state) +} + +fn json_request(method: &str, uri: &str, body: Option<&str>) -> Request { + let mut builder = Request::builder() + .method(method) + .uri(uri) + .header("content-type", "application/json"); + + if let Some(b) = body { + builder.body(Body::from(b.to_string())).unwrap() + } else { + builder.body(Body::empty()).unwrap() + } +} + +#[tokio::test] +async fn create_and_get_widget() { + let app = test_app(); + + let body = r#"{ + "id": 1, + "name": "weather", + "display_hint": "icon_value", + "data_source_id": 10, + "mappings": [{"source_path": "$.temp", "target_key": "temperature"}] + }"#; + + let resp = app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["name"], "weather"); + assert_eq!(json["display_hint"], "icon_value"); + assert_eq!(json["data_source_id"], 10); +} + +#[tokio::test] +async fn list_widgets() { + let app = test_app(); + + let w1 = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; + let w2 = r#"{"id":2,"name":"b","display_hint":"key_value","data_source_id":2,"mappings":[]}"#; + + app.clone().oneshot(json_request("POST", "/api/widgets", Some(w1))).await.unwrap(); + app.clone().oneshot(json_request("POST", "/api/widgets", Some(w2))).await.unwrap(); + + let resp = app.oneshot(json_request("GET", "/api/widgets", None)).await.unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let json: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(json.len(), 2); +} + +#[tokio::test] +async fn delete_widget() { + let app = test_app(); + + let body = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; + app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); + + let resp = app.clone().oneshot(json_request("DELETE", "/api/widgets/1", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + + let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn create_and_get_data_source() { + let app = test_app(); + + let body = r#"{ + "id": 10, + "name": "weather_api", + "source_type": "weather", + "poll_interval_secs": 300, + "url": "https://api.openweather.org", + "api_key": "test-key", + "headers": [] + }"#; + + let resp = app.clone().oneshot(json_request("POST", "/api/data-sources", Some(body))).await.unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + let resp = app.oneshot(json_request("GET", "/api/data-sources/10", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["name"], "weather_api"); + assert_eq!(json["poll_interval_secs"], 300); +} + +#[tokio::test] +async fn update_and_get_layout() { + let app = test_app(); + + let body = r#"{ + "root": { + "type": "container", + "direction": "row", + "gap": 4, + "padding": 2, + "children": [ + {"sizing": {"type": "flex", "value": 1}, "node": {"type": "leaf", "widget_id": 1}}, + {"sizing": {"type": "fixed", "value": 80}, "node": {"type": "leaf", "widget_id": 2}} + ] + } + }"#; + + let resp = app.clone().oneshot(json_request("PUT", "/api/layout", Some(body))).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let resp = app.oneshot(json_request("GET", "/api/layout", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["root"]["type"], "container"); + assert_eq!(json["root"]["direction"], "row"); + assert_eq!(json["root"]["children"].as_array().unwrap().len(), 2); +} + +#[tokio::test] +async fn get_nonexistent_returns_404() { + let app = test_app(); + + let resp = app.clone().oneshot(json_request("GET", "/api/widgets/99", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let resp = app.oneshot(json_request("GET", "/api/data-sources/99", None)).await.unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); +} diff --git a/crates/application/tests/support/mod.rs b/crates/application/tests/support/mod.rs index 199d78f..41fc30c 100644 --- a/crates/application/tests/support/mod.rs +++ b/crates/application/tests/support/mod.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::sync::Mutex; use std::collections::HashMap; use domain::{ ConfigRepository, EventPublisher, @@ -7,19 +7,19 @@ use domain::{ }; pub struct InMemoryConfigRepository { - pub widgets: RefCell>, - pub data_sources: RefCell>, - pub layout: RefCell>, - pub presets: RefCell>, + widgets: Mutex>, + data_sources: Mutex>, + layout: Mutex>, + presets: Mutex>, } impl InMemoryConfigRepository { pub fn new() -> Self { Self { - widgets: RefCell::new(HashMap::new()), - data_sources: RefCell::new(HashMap::new()), - layout: RefCell::new(None), - presets: RefCell::new(HashMap::new()), + widgets: Mutex::new(HashMap::new()), + data_sources: Mutex::new(HashMap::new()), + layout: Mutex::new(None), + presets: Mutex::new(HashMap::new()), } } } @@ -37,82 +37,82 @@ impl ConfigRepository for InMemoryConfigRepository { type Error = Never; async fn get_widget(&self, id: WidgetId) -> Result, Self::Error> { - Ok(self.widgets.borrow().get(&id).cloned()) + Ok(self.widgets.lock().unwrap().get(&id).cloned()) } async fn list_widgets(&self) -> Result, Self::Error> { - Ok(self.widgets.borrow().values().cloned().collect()) + Ok(self.widgets.lock().unwrap().values().cloned().collect()) } async fn save_widget(&self, config: &WidgetConfig) -> Result<(), Self::Error> { - self.widgets.borrow_mut().insert(config.id, config.clone()); + self.widgets.lock().unwrap().insert(config.id, config.clone()); Ok(()) } async fn delete_widget(&self, id: WidgetId) -> Result<(), Self::Error> { - self.widgets.borrow_mut().remove(&id); + self.widgets.lock().unwrap().remove(&id); Ok(()) } async fn get_data_source(&self, id: DataSourceId) -> Result, Self::Error> { - Ok(self.data_sources.borrow().get(&id).cloned()) + Ok(self.data_sources.lock().unwrap().get(&id).cloned()) } async fn list_data_sources(&self) -> Result, Self::Error> { - Ok(self.data_sources.borrow().values().cloned().collect()) + Ok(self.data_sources.lock().unwrap().values().cloned().collect()) } async fn save_data_source(&self, source: &DataSource) -> Result<(), Self::Error> { - self.data_sources.borrow_mut().insert(source.id, source.clone()); + self.data_sources.lock().unwrap().insert(source.id, source.clone()); Ok(()) } async fn delete_data_source(&self, id: DataSourceId) -> Result<(), Self::Error> { - self.data_sources.borrow_mut().remove(&id); + self.data_sources.lock().unwrap().remove(&id); Ok(()) } async fn get_layout(&self) -> Result, Self::Error> { - Ok(self.layout.borrow().clone()) + Ok(self.layout.lock().unwrap().clone()) } async fn save_layout(&self, layout: &Layout) -> Result<(), Self::Error> { - *self.layout.borrow_mut() = Some(layout.clone()); + *self.layout.lock().unwrap() = Some(layout.clone()); Ok(()) } async fn get_preset(&self, id: LayoutPresetId) -> Result, Self::Error> { - Ok(self.presets.borrow().get(&id).cloned()) + Ok(self.presets.lock().unwrap().get(&id).cloned()) } async fn list_presets(&self) -> Result, Self::Error> { - Ok(self.presets.borrow().values().cloned().collect()) + Ok(self.presets.lock().unwrap().values().cloned().collect()) } async fn save_preset(&self, preset: &LayoutPreset) -> Result<(), Self::Error> { - self.presets.borrow_mut().insert(preset.id, preset.clone()); + self.presets.lock().unwrap().insert(preset.id, preset.clone()); Ok(()) } async fn delete_preset(&self, id: LayoutPresetId) -> Result<(), Self::Error> { - self.presets.borrow_mut().remove(&id); + self.presets.lock().unwrap().remove(&id); Ok(()) } } pub struct InMemoryEventPublisher { - pub events: RefCell>, + events: Mutex>, } impl InMemoryEventPublisher { pub fn new() -> Self { Self { - events: RefCell::new(Vec::new()), + events: Mutex::new(Vec::new()), } } pub fn emitted(&self) -> Vec { - self.events.borrow().clone() + self.events.lock().unwrap().clone() } } @@ -120,7 +120,7 @@ impl EventPublisher for InMemoryEventPublisher { type Error = Never; async fn publish(&self, event: DomainEvent) -> Result<(), Self::Error> { - self.events.borrow_mut().push(event); + self.events.lock().unwrap().push(event); Ok(()) } } diff --git a/crates/domain/src/ports/broadcast.rs b/crates/domain/src/ports/broadcast.rs index 6a8a2f8..6d194e5 100644 --- a/crates/domain/src/ports/broadcast.rs +++ b/crates/domain/src/ports/broadcast.rs @@ -1,17 +1,18 @@ +use std::future::Future; use crate::entities::WidgetId; use crate::value_objects::{Layout, WidgetState}; pub trait BroadcastPort { type Error; - async fn push_screen_update( + fn push_screen_update( &self, layout: &Layout, widgets: &[(WidgetId, WidgetState)], - ) -> Result<(), Self::Error>; + ) -> impl Future> + Send; - async fn push_data_update( + fn push_data_update( &self, updates: &[(WidgetId, WidgetState)], - ) -> Result<(), Self::Error>; + ) -> impl Future> + Send; } diff --git a/crates/domain/src/ports/config_repository.rs b/crates/domain/src/ports/config_repository.rs index 7b9a745..07d0293 100644 --- a/crates/domain/src/ports/config_repository.rs +++ b/crates/domain/src/ports/config_repository.rs @@ -1,3 +1,4 @@ +use std::future::Future; use crate::entities::{ DataSource, DataSourceId, LayoutPreset, LayoutPresetId, WidgetConfig, WidgetId, }; @@ -6,21 +7,21 @@ use crate::value_objects::Layout; pub trait ConfigRepository { type Error; - async fn get_widget(&self, id: WidgetId) -> Result, Self::Error>; - async fn list_widgets(&self) -> Result, Self::Error>; - async fn save_widget(&self, config: &WidgetConfig) -> Result<(), Self::Error>; - async fn delete_widget(&self, id: WidgetId) -> Result<(), Self::Error>; + fn get_widget(&self, id: WidgetId) -> impl Future, Self::Error>> + Send; + fn list_widgets(&self) -> impl Future, Self::Error>> + Send; + fn save_widget(&self, config: &WidgetConfig) -> impl Future> + Send; + fn delete_widget(&self, id: WidgetId) -> impl Future> + Send; - async fn get_data_source(&self, id: DataSourceId) -> Result, Self::Error>; - async fn list_data_sources(&self) -> Result, Self::Error>; - async fn save_data_source(&self, source: &DataSource) -> Result<(), Self::Error>; - async fn delete_data_source(&self, id: DataSourceId) -> Result<(), Self::Error>; + fn get_data_source(&self, id: DataSourceId) -> impl Future, Self::Error>> + Send; + fn list_data_sources(&self) -> impl Future, Self::Error>> + Send; + fn save_data_source(&self, source: &DataSource) -> impl Future> + Send; + fn delete_data_source(&self, id: DataSourceId) -> impl Future> + Send; - async fn get_layout(&self) -> Result, Self::Error>; - async fn save_layout(&self, layout: &Layout) -> Result<(), Self::Error>; + fn get_layout(&self) -> impl Future, Self::Error>> + Send; + fn save_layout(&self, layout: &Layout) -> impl Future> + Send; - async fn get_preset(&self, id: LayoutPresetId) -> Result, Self::Error>; - async fn list_presets(&self) -> Result, Self::Error>; - async fn save_preset(&self, preset: &LayoutPreset) -> Result<(), Self::Error>; - async fn delete_preset(&self, id: LayoutPresetId) -> Result<(), Self::Error>; + fn get_preset(&self, id: LayoutPresetId) -> impl Future, Self::Error>> + Send; + fn list_presets(&self) -> impl Future, Self::Error>> + Send; + fn save_preset(&self, preset: &LayoutPreset) -> impl Future> + Send; + fn delete_preset(&self, id: LayoutPresetId) -> impl Future> + Send; } diff --git a/crates/domain/src/ports/data_source_port.rs b/crates/domain/src/ports/data_source_port.rs index 93e0379..7205696 100644 --- a/crates/domain/src/ports/data_source_port.rs +++ b/crates/domain/src/ports/data_source_port.rs @@ -1,8 +1,9 @@ +use std::future::Future; use crate::entities::DataSource; use crate::value_objects::Value; pub trait DataSourcePort { type Error; - async fn poll(&self, source: &DataSource) -> Result; + fn poll(&self, source: &DataSource) -> impl Future> + Send; } diff --git a/crates/domain/src/ports/event.rs b/crates/domain/src/ports/event.rs index 51d9805..82daeaa 100644 --- a/crates/domain/src/ports/event.rs +++ b/crates/domain/src/ports/event.rs @@ -1,7 +1,8 @@ +use std::future::Future; use crate::events::DomainEvent; pub trait EventPublisher { type Error; - async fn publish(&self, event: DomainEvent) -> Result<(), Self::Error>; + fn publish(&self, event: DomainEvent) -> impl Future> + Send; }