chore: use native HTML5 dialog element and refactor backend into hexagonal architecture. (#48)
Reviewed-on: #48 Co-authored-by: Felipe Contreras Salinas <felipe@bstr.cl> Co-committed-by: Felipe Contreras Salinas <felipe@bstr.cl>
This commit is contained in:
parent
f8540e5043
commit
78cb376791
15 changed files with 880 additions and 618 deletions
154
Cargo.lock
generated
154
Cargo.lock
generated
|
|
@ -34,9 +34,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.96"
|
version = "1.0.98"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
|
|
@ -77,9 +77,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.1"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -111,12 +111,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.0"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-core",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
|
@ -144,9 +144,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-test"
|
name = "axum-test"
|
||||||
version = "17.2.0"
|
version = "17.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "317c1f4ecc1e68e0ad5decb78478421055c963ce215e736ed97463fa609cd196"
|
checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
|
|
@ -232,9 +232,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytesize"
|
name = "bytesize"
|
||||||
version = "1.3.2"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e"
|
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
|
|
@ -395,16 +395,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -427,12 +417,6 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fastrand"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -670,9 +654,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.2.0"
|
version = "1.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
|
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
|
@ -722,7 +706,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huellas"
|
name = "huellas"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -732,6 +716,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -965,12 +950,6 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.4.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
|
@ -1376,11 +1355,10 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reserve-port"
|
name = "reserve-port"
|
||||||
version = "2.1.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "359fc315ed556eb0e42ce74e76f4b1cd807b50fa6307f3de4e51f92dbe86e2d5"
|
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1442,16 +1420,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-multipart-rfc7578_2"
|
name = "rust-multipart-rfc7578_2"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc4bb9e7c9abe5fa5f30c2d8f8fefb9e0080a2c1e3c2e567318d2907054b35d3"
|
checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
@ -1462,19 +1439,6 @@ version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "0.38.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.23"
|
version = "0.23.23"
|
||||||
|
|
@ -1489,15 +1453,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-pemfile"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
@ -1535,18 +1490,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.218"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.218"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1692,9 +1647,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx"
|
name = "sqlx"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros",
|
"sqlx-macros",
|
||||||
|
|
@ -1705,10 +1660,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-core"
|
name = "sqlx-core"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
|
|
@ -1726,7 +1682,6 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pemfile",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
@ -1741,9 +1696,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros"
|
name = "sqlx-macros"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1754,9 +1709,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros-core"
|
name = "sqlx-macros-core"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
|
|
@ -1771,16 +1726,15 @@ dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn",
|
"syn",
|
||||||
"tempfile",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-mysql"
|
name = "sqlx-mysql"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1819,9 +1773,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-postgres"
|
name = "sqlx-postgres"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1856,9 +1810,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-sqlite"
|
name = "sqlx-sqlite"
|
||||||
version = "0.8.3"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"flume",
|
"flume",
|
||||||
|
|
@ -1873,6 +1827,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
@ -1928,34 +1883,20 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tempfile"
|
|
||||||
version = "3.17.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"fastrand",
|
|
||||||
"getrandom 0.3.1",
|
|
||||||
"once_cell",
|
|
||||||
"rustix",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.11"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.11"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2030,9 +1971,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.43.0"
|
version = "1.45.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2098,12 +2039,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.2"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
|
|
||||||
17
Cargo.toml
17
Cargo.toml
|
|
@ -1,12 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "huellas"
|
name = "huellas"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.96"
|
anyhow = "1.0.98"
|
||||||
axum = { version = "0.8.1", default-features = false, features = [
|
axum = { version = "0.8.4", default-features = false, features = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tokio",
|
"tokio",
|
||||||
"http1",
|
"http1",
|
||||||
|
|
@ -15,20 +15,21 @@ axum = { version = "0.8.1", default-features = false, features = [
|
||||||
axum-msgpack = "0.5.0"
|
axum-msgpack = "0.5.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
sqlx = { version = "0.8.3", default-features = false, features = [
|
sqlx = { version = "0.8.6", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
"migrate",
|
"migrate",
|
||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
"tls-rustls",
|
"tls-rustls",
|
||||||
] }
|
] }
|
||||||
tokio = { version = "1.43.0", default-features = false, features = [
|
tokio = { version = "1.45.1", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"signal",
|
"signal",
|
||||||
] }
|
] }
|
||||||
tower-http = { version = "0.6.2", default-features = false, features = ["fs"] }
|
thiserror = "2.0.12"
|
||||||
|
tower-http = { version = "0.6.6", default-features = false, features = ["fs"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.19", default-features = false, features = [
|
tracing-subscriber = { version = "0.3.19", default-features = false, features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
|
|
@ -38,4 +39,4 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = { version = "17.2.0", features = ["msgpack"] }
|
axum-test = { version = "17.3.0", features = ["msgpack"] }
|
||||||
|
|
|
||||||
26
src/db.rs
Normal file
26
src/db.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
//! Database handling.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
/// Creates a Database Pool
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function may return an error if the `DATABASE_URL` environment is not defined or if the
|
||||||
|
/// database that URL points to is not reachable for some reason.
|
||||||
|
pub async fn pool() -> Result<SqlitePool> {
|
||||||
|
let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not defined")?;
|
||||||
|
let pool = SqlitePool::connect(&db_url)
|
||||||
|
.await
|
||||||
|
.context("Couldn't connect to database")?;
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run migrations on the database `pool` is connected to.
|
||||||
|
pub async fn run_migrations(pool: &SqlitePool) -> Result<()> {
|
||||||
|
sqlx::migrate!()
|
||||||
|
.run(pool)
|
||||||
|
.await
|
||||||
|
.context("Couldn't run migrations")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
20
src/logging.rs
Normal file
20
src/logging.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//! Service logging.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
/// Setups logging.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function can return an error if called repeatedly or if logging/tracing was already setup
|
||||||
|
/// by another means.
|
||||||
|
pub fn setup() -> Result<()> {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| "huellas=debug".into()),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.try_init()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
61
src/main.rs
61
src/main.rs
|
|
@ -1,61 +1,22 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use axum::Router;
|
|
||||||
use axum::serve::ListenerExt;
|
|
||||||
use sqlx::sqlite::SqlitePool;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
use tracing::trace;
|
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|
||||||
|
|
||||||
mod place;
|
mod db;
|
||||||
mod routes;
|
mod logging;
|
||||||
|
mod places;
|
||||||
|
mod server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenvy::dotenv().unwrap_or_default();
|
dotenvy::dotenv().unwrap_or_default();
|
||||||
tracing_subscriber::registry()
|
logging::setup()?;
|
||||||
.with(
|
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| "huellas=debug".into()),
|
|
||||||
)
|
|
||||||
.with(tracing_subscriber::fmt::layer())
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let db_url = dotenvy::var("DATABASE_URL").context("DATABASE_URL not defined")?;
|
let pool = db::pool().await?;
|
||||||
let pool = SqlitePool::connect(&db_url)
|
db::run_migrations(&pool).await?;
|
||||||
.await
|
|
||||||
.context("Couldn't connect to database")?;
|
|
||||||
|
|
||||||
sqlx::migrate!()
|
let places_repository = places::db_repository::DbPlacesRepository::new(pool);
|
||||||
.run(&pool)
|
let places_routes = places::routes::places_routes(places_repository);
|
||||||
.await
|
|
||||||
.context("Couldn't run migrations")?;
|
|
||||||
|
|
||||||
let app = Router::new()
|
server::serve(places_routes).await?;
|
||||||
.nest("/places", routes::places_routes(pool))
|
|
||||||
.nest_service("/", ServeDir::new("static"));
|
|
||||||
|
|
||||||
let port = dotenvy::var("PORT").unwrap_or_default();
|
|
||||||
let port = str::parse(&port).unwrap_or(3000);
|
|
||||||
let address = SocketAddr::from(([0, 0, 0, 0], port));
|
|
||||||
tracing::debug!("listening on {}", address);
|
|
||||||
let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
.await?
|
|
||||||
.tap_io(|tcp_stream| {
|
|
||||||
if let Err(err) = tcp_stream.set_nodelay(true) {
|
|
||||||
trace!("failed to set TCP_NODELAY on incoming connection: {err:#}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
axum::serve(listener, app.into_make_service())
|
|
||||||
.with_graceful_shutdown(shutdown_signal())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
|
||||||
tokio::signal::ctrl_c()
|
|
||||||
.await
|
|
||||||
.expect("failed to listen for ctrl-c");
|
|
||||||
tracing::debug!("Received shutdown signal");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
14
src/place.rs
14
src/place.rs
|
|
@ -1,14 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
||||||
pub struct Place {
|
|
||||||
pub id: Option<i64>,
|
|
||||||
pub name: String,
|
|
||||||
pub address: String,
|
|
||||||
pub open_hours: String,
|
|
||||||
pub icon: String,
|
|
||||||
pub description: String,
|
|
||||||
pub longitude: f64,
|
|
||||||
pub latitude: f64,
|
|
||||||
pub url: Option<String>,
|
|
||||||
}
|
|
||||||
287
src/places/db_repository.rs
Normal file
287
src/places/db_repository.rs
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
//! `PlacesRepository` that is backed by a DB.
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::places::models::PlaceInsert;
|
||||||
|
|
||||||
|
use super::models::Place;
|
||||||
|
use super::repository::{PlacesError, PlacesRepository};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DbPlacesRepository {
|
||||||
|
db_pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbPlacesRepository {
|
||||||
|
pub fn new(db_pool: SqlitePool) -> Self {
|
||||||
|
Self { db_pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlacesRepository for DbPlacesRepository {
|
||||||
|
async fn get_places(&self) -> Result<Vec<super::models::Place>, PlacesError> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"SELECT id, name, address, open_hours, icon, description, url,
|
||||||
|
longitude as "longitude: f64", latitude as "latitude: f64"
|
||||||
|
FROM places
|
||||||
|
WHERE active = TRUE"#
|
||||||
|
)
|
||||||
|
.fetch(&self.db_pool)
|
||||||
|
.map_ok(|p| Place {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
address: p.address,
|
||||||
|
open_hours: p.open_hours,
|
||||||
|
icon: p.icon,
|
||||||
|
description: p.description,
|
||||||
|
latitude: p.latitude,
|
||||||
|
longitude: p.longitude,
|
||||||
|
url: p.url,
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.map_err(|err| PlacesError::FailToGet(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_place(&self, place: PlaceInsert) -> Result<Place, PlacesError> {
|
||||||
|
let id = sqlx::query_scalar!(
|
||||||
|
r#"INSERT INTO places
|
||||||
|
(name, address, open_hours, icon, description, longitude, latitude, url)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
RETURNING id"#,
|
||||||
|
place.name,
|
||||||
|
place.address,
|
||||||
|
place.open_hours,
|
||||||
|
place.icon,
|
||||||
|
place.description,
|
||||||
|
place.longitude,
|
||||||
|
place.latitude,
|
||||||
|
place.url
|
||||||
|
)
|
||||||
|
.fetch_one(&self.db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PlacesError::FailToUpsert(err.to_string()))?;
|
||||||
|
|
||||||
|
Ok((place, id).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_place(&self, place: Place) -> Result<Place, PlacesError> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"UPDATE places
|
||||||
|
SET (name, address, open_hours, icon, description, longitude, latitude, url)
|
||||||
|
= (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
WHERE id = ?"#,
|
||||||
|
place.name,
|
||||||
|
place.address,
|
||||||
|
place.open_hours,
|
||||||
|
place.icon,
|
||||||
|
place.description,
|
||||||
|
place.longitude,
|
||||||
|
place.latitude,
|
||||||
|
place.url,
|
||||||
|
place.id,
|
||||||
|
)
|
||||||
|
.execute(&self.db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PlacesError::FailToUpsert(err.to_string()))?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 1 {
|
||||||
|
Ok(place)
|
||||||
|
} else {
|
||||||
|
Err(PlacesError::NotFound(place.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_place(&self, id: i64) -> Result<(), PlacesError> {
|
||||||
|
let result = ::sqlx::query!("UPDATE places SET active = FALSE WHERE id = ?", id)
|
||||||
|
.execute(&self.db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PlacesError::FailToDelete(err.to_string()))?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 1 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PlacesError::NotFound(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
#![cfg(test)]
|
||||||
|
use super::DbPlacesRepository;
|
||||||
|
use crate::places::models::PlaceInsert;
|
||||||
|
use crate::places::repository::{PlacesError, PlacesRepository};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use futures::future::try_join_all;
|
||||||
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_add_place(pool: SqlitePool) -> Result<()> {
|
||||||
|
let repository = DbPlacesRepository::new(pool);
|
||||||
|
let place = PlaceInsert {
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
};
|
||||||
|
// Insert the place
|
||||||
|
let res_place = repository.insert_place(place.clone()).await?;
|
||||||
|
let (res_place, _) = res_place.into();
|
||||||
|
// And now they should be equal
|
||||||
|
assert_eq!(place, res_place);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_get_places(pool: SqlitePool) -> Result<()> {
|
||||||
|
let repository = DbPlacesRepository::new(pool);
|
||||||
|
let places = vec![
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
},
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Museo Nacional de Historia Natural".to_owned(),
|
||||||
|
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
||||||
|
description: "Museo".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: -70.681838888889,
|
||||||
|
longitude: -33.4421694444449,
|
||||||
|
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
||||||
|
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// insert the places
|
||||||
|
for place in &places {
|
||||||
|
let _res_place = repository.insert_place(place.clone()).await?;
|
||||||
|
}
|
||||||
|
// and fetch them
|
||||||
|
let mut res_places = repository.get_places().await?;
|
||||||
|
// and they should be equal
|
||||||
|
res_places.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
let res_places = res_places
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| {
|
||||||
|
let (p, _id): (PlaceInsert, i64) = p.into();
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(places, res_places);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_delete(pool: SqlitePool) -> Result<()> {
|
||||||
|
let repository = DbPlacesRepository::new(pool);
|
||||||
|
let places = vec![
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
},
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Museo Nacional de Historia Natural".to_owned(),
|
||||||
|
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
||||||
|
description: "Museo".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: -70.681838888889,
|
||||||
|
longitude: -33.4421694444449,
|
||||||
|
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
||||||
|
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// insert the places
|
||||||
|
let ids = try_join_all(places.iter().map(|place| async {
|
||||||
|
let res_place = repository.insert_place(place.clone()).await?;
|
||||||
|
Ok::<_, PlacesError>(res_place.id)
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
// delete the first one
|
||||||
|
repository.delete_place(ids[0]).await?;
|
||||||
|
|
||||||
|
// fetch the remaining places
|
||||||
|
let res_places = repository.get_places().await?;
|
||||||
|
let res_places = res_places
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| {
|
||||||
|
let (p, _id) = p.into();
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// we should only get the second place
|
||||||
|
assert_eq!(&places[1..], res_places.as_slice());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_delete_not_existing(pool: SqlitePool) -> Result<()> {
|
||||||
|
let repository = DbPlacesRepository::new(pool);
|
||||||
|
// Try to delete a non-existing place
|
||||||
|
let res = repository.delete_place(33).await;
|
||||||
|
assert!(res.is_err_and(|err| err == PlacesError::NotFound(33)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_update(pool: SqlitePool) -> Result<()> {
|
||||||
|
let repository = DbPlacesRepository::new(pool);
|
||||||
|
let places = vec![
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
},
|
||||||
|
PlaceInsert {
|
||||||
|
name: "Museo Nacional de Historia Natural".to_owned(),
|
||||||
|
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
||||||
|
description: "Museo".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: -70.681838888889,
|
||||||
|
longitude: -33.4421694444449,
|
||||||
|
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
||||||
|
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// insert original place
|
||||||
|
let res = repository.insert_place(places[0].clone()).await?;
|
||||||
|
// Add the returned ID to the new place so we can do the update
|
||||||
|
let place = (places[1].clone(), res.id).into();
|
||||||
|
// update the place
|
||||||
|
let _res = repository.update_place(place).await?;
|
||||||
|
|
||||||
|
// fetch the places
|
||||||
|
let res_places = repository.get_places().await?;
|
||||||
|
let res_places = res_places
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| {
|
||||||
|
let (p, _id) = p.into();
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// we should get the updated place
|
||||||
|
assert_eq!(&places[1..], res_places.as_slice());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/places/mod.rs
Normal file
4
src/places/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod db_repository;
|
||||||
|
pub mod models;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod routes;
|
||||||
119
src/places/models.rs
Normal file
119
src/places/models.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/// Models
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Place can be any place of interest we want to mark in a map
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct Place {
|
||||||
|
pub id: i64,
|
||||||
|
/// Name
|
||||||
|
pub name: String,
|
||||||
|
/// Address
|
||||||
|
pub address: String,
|
||||||
|
/// Opening Hours
|
||||||
|
pub open_hours: String,
|
||||||
|
/// Icon name
|
||||||
|
pub icon: String,
|
||||||
|
/// Description
|
||||||
|
pub description: String,
|
||||||
|
/// Longitude of the place
|
||||||
|
pub longitude: f64,
|
||||||
|
/// latitude of the place
|
||||||
|
pub latitude: f64,
|
||||||
|
/// URL for the place website
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert Place payload
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct PlaceInsert {
|
||||||
|
/// Name
|
||||||
|
pub name: String,
|
||||||
|
/// Address
|
||||||
|
pub address: String,
|
||||||
|
/// Opening Hours
|
||||||
|
pub open_hours: String,
|
||||||
|
/// Icon name
|
||||||
|
pub icon: String,
|
||||||
|
/// Description
|
||||||
|
pub description: String,
|
||||||
|
/// Longitude of the place
|
||||||
|
pub longitude: f64,
|
||||||
|
/// latitude of the place
|
||||||
|
pub latitude: f64,
|
||||||
|
/// URL for the place website
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UpsertPlace payload
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct PlaceUpsert {
|
||||||
|
pub id: Option<i64>,
|
||||||
|
/// Name
|
||||||
|
pub name: String,
|
||||||
|
/// Address
|
||||||
|
pub address: String,
|
||||||
|
/// Opening Hours
|
||||||
|
pub open_hours: String,
|
||||||
|
/// Icon name
|
||||||
|
pub icon: String,
|
||||||
|
/// Description
|
||||||
|
pub description: String,
|
||||||
|
/// Longitude of the place
|
||||||
|
pub longitude: f64,
|
||||||
|
/// latitude of the place
|
||||||
|
pub latitude: f64,
|
||||||
|
/// URL for the place website
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(PlaceInsert, i64)> for Place {
|
||||||
|
fn from((place, id): (PlaceInsert, i64)) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name: place.name,
|
||||||
|
address: place.address,
|
||||||
|
open_hours: place.open_hours,
|
||||||
|
icon: place.icon,
|
||||||
|
description: place.description,
|
||||||
|
longitude: place.longitude,
|
||||||
|
latitude: place.latitude,
|
||||||
|
url: place.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaceUpsert> for (PlaceInsert, Option<i64>) {
|
||||||
|
fn from(place: PlaceUpsert) -> Self {
|
||||||
|
(
|
||||||
|
PlaceInsert {
|
||||||
|
name: place.name,
|
||||||
|
address: place.address,
|
||||||
|
open_hours: place.open_hours,
|
||||||
|
icon: place.icon,
|
||||||
|
description: place.description,
|
||||||
|
longitude: place.longitude,
|
||||||
|
latitude: place.latitude,
|
||||||
|
url: place.url,
|
||||||
|
},
|
||||||
|
place.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Place> for (PlaceInsert, i64) {
|
||||||
|
fn from(place: Place) -> Self {
|
||||||
|
(
|
||||||
|
PlaceInsert {
|
||||||
|
name: place.name,
|
||||||
|
address: place.address,
|
||||||
|
open_hours: place.open_hours,
|
||||||
|
icon: place.icon,
|
||||||
|
description: place.description,
|
||||||
|
longitude: place.longitude,
|
||||||
|
latitude: place.latitude,
|
||||||
|
url: place.url,
|
||||||
|
},
|
||||||
|
place.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/places/repository.rs
Normal file
105
src/places/repository.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
//! Places Repository
|
||||||
|
#[cfg(test)]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
#[cfg(test)]
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use super::models::{Place, PlaceInsert};
|
||||||
|
|
||||||
|
/// Trait to handle Places.
|
||||||
|
pub trait PlacesRepository: Clone + Send + Sync + 'static {
|
||||||
|
/// Get all of the Places
|
||||||
|
fn get_places(&self) -> impl Future<Output = Result<Vec<Place>, PlacesError>> + Send;
|
||||||
|
|
||||||
|
/// Inserts a Place.
|
||||||
|
fn insert_place(
|
||||||
|
&self,
|
||||||
|
place: PlaceInsert,
|
||||||
|
) -> impl Future<Output = Result<Place, PlacesError>> + Send;
|
||||||
|
|
||||||
|
/// Updates a Place.
|
||||||
|
fn update_place(&self, place: Place)
|
||||||
|
-> impl Future<Output = Result<Place, PlacesError>> + Send;
|
||||||
|
|
||||||
|
/// Deletes the place for the given `id`.
|
||||||
|
fn delete_place(&self, id: i64) -> impl Future<Output = Result<(), PlacesError>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq)]
|
||||||
|
pub enum PlacesError {
|
||||||
|
#[error("Couldn't retrieve places: {0}")]
|
||||||
|
FailToGet(String),
|
||||||
|
#[error("Couldn't upsert place: {0}")]
|
||||||
|
FailToUpsert(String),
|
||||||
|
#[error("Couldn't delete place: {0}")]
|
||||||
|
FailToDelete(String),
|
||||||
|
#[error("Place with id {0} not found")]
|
||||||
|
NotFound(i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MockPlacesRepository {
|
||||||
|
get_places_count: Arc<RwLock<usize>>,
|
||||||
|
insert_place_count: Arc<RwLock<usize>>,
|
||||||
|
update_place_count: Arc<RwLock<usize>>,
|
||||||
|
delete_place_count: Arc<RwLock<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl MockPlacesRepository {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
get_places_count: Arc::new(RwLock::new(0)),
|
||||||
|
insert_place_count: Arc::new(RwLock::new(0)),
|
||||||
|
update_place_count: Arc::new(RwLock::new(0)),
|
||||||
|
delete_place_count: Arc::new(RwLock::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_places_count(&self) -> usize {
|
||||||
|
*self.get_places_count.read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_place_count(&self) -> usize {
|
||||||
|
*self.insert_place_count.read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_place_count(&self) -> usize {
|
||||||
|
*self.update_place_count.read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_place_count(&self) -> usize {
|
||||||
|
*self.delete_place_count.read().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl PlacesRepository for MockPlacesRepository {
|
||||||
|
async fn get_places(&self) -> Result<Vec<Place>, PlacesError> {
|
||||||
|
let mut get_places_count = self.get_places_count.write().await;
|
||||||
|
*get_places_count += 1;
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_place(&self, place: super::models::PlaceInsert) -> Result<Place, PlacesError> {
|
||||||
|
let mut insert_place_count = self.insert_place_count.write().await;
|
||||||
|
*insert_place_count += 1;
|
||||||
|
let place: Place = (place, 0).into();
|
||||||
|
Ok(place)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_place(&self, place: Place) -> Result<Place, PlacesError> {
|
||||||
|
let mut update_place_count = self.update_place_count.write().await;
|
||||||
|
*update_place_count += 1;
|
||||||
|
Ok(place)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_place(&self, _id: i64) -> Result<(), PlacesError> {
|
||||||
|
let mut delete_place_count = self.delete_place_count.write().await;
|
||||||
|
*delete_place_count += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/places/routes.rs
Normal file
145
src/places/routes.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::{delete, get, put};
|
||||||
|
use axum_msgpack::MsgPack;
|
||||||
|
|
||||||
|
use super::models::{Place, PlaceUpsert};
|
||||||
|
use super::repository::{PlacesError, PlacesRepository};
|
||||||
|
|
||||||
|
type Result<T, E = (StatusCode, String)> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
fn internal_error(err: PlacesError) -> (StatusCode, String) {
|
||||||
|
match err {
|
||||||
|
PlacesError::FailToGet(_) | PlacesError::FailToUpsert(_) | PlacesError::FailToDelete(_) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||||
|
}
|
||||||
|
PlacesError::NotFound(_) => (StatusCode::NOT_FOUND, err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_places<PR: PlacesRepository>(
|
||||||
|
State(repository): State<PR>,
|
||||||
|
) -> Result<MsgPack<Vec<Place>>> {
|
||||||
|
let places = repository.get_places().await.map_err(internal_error)?;
|
||||||
|
Ok(MsgPack(places))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upsert_place<PR: PlacesRepository>(
|
||||||
|
State(repository): State<PR>,
|
||||||
|
MsgPack(place): MsgPack<PlaceUpsert>,
|
||||||
|
) -> Result<MsgPack<Place>> {
|
||||||
|
let place = match place.into() {
|
||||||
|
(place, Some(id)) => repository.update_place((place, id).into()).await,
|
||||||
|
(place, None) => repository.insert_place(place).await,
|
||||||
|
}
|
||||||
|
.map_err(internal_error)?;
|
||||||
|
Ok(MsgPack(place))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_place<PR: PlacesRepository>(
|
||||||
|
State(repository): State<PR>,
|
||||||
|
Path(id): Path<i64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
repository.delete_place(id).await.map_err(internal_error)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn places_routes<PR: PlacesRepository>(repository: PR) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_places::<PR>))
|
||||||
|
.route("/", put(upsert_place::<PR>))
|
||||||
|
.route("/{id}", delete(delete_place::<PR>))
|
||||||
|
.with_state(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
#![cfg(test)]
|
||||||
|
use super::places_routes;
|
||||||
|
use crate::places::models::{Place, PlaceUpsert};
|
||||||
|
use crate::places::repository::MockPlacesRepository;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
|
||||||
|
fn setup_server() -> Result<(TestServer, MockPlacesRepository)> {
|
||||||
|
let places_repository = MockPlacesRepository::new();
|
||||||
|
let router = Router::new().nest("/places", places_routes(places_repository.clone()));
|
||||||
|
Ok((TestServer::new(router)?, places_repository))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_add_place() -> Result<()> {
|
||||||
|
let (server, mock_repository) = setup_server()?;
|
||||||
|
let place = PlaceUpsert {
|
||||||
|
id: None,
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
};
|
||||||
|
// Insert the place
|
||||||
|
let res = server.put("/places").msgpack(&place).await;
|
||||||
|
// We should get a success on the request
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
let _res_place: Place = res.msgpack();
|
||||||
|
// The correct function should be called
|
||||||
|
assert_eq!(mock_repository.insert_place_count().await, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_places() -> Result<()> {
|
||||||
|
let (server, mock_repository) = setup_server()?;
|
||||||
|
// Get the places
|
||||||
|
let res = server.get("/places").await;
|
||||||
|
// We should get a success on the request
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
let _res_places: Vec<Place> = res.msgpack();
|
||||||
|
// and the correct function should be called
|
||||||
|
assert_eq!(mock_repository.get_places_count().await, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete() -> Result<()> {
|
||||||
|
let (server, mock_repository) = setup_server()?;
|
||||||
|
// Call delete
|
||||||
|
let res = server.delete("/places/0").await;
|
||||||
|
// We should get a success on the request
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
// The correct function should be called
|
||||||
|
assert_eq!(mock_repository.delete_place_count().await, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update() -> Result<()> {
|
||||||
|
let (server, mock_repository) = setup_server()?;
|
||||||
|
let places = PlaceUpsert {
|
||||||
|
id: Some(1),
|
||||||
|
name: "Sherlock Holmes".to_owned(),
|
||||||
|
address: "221 B Baker Street, London".to_owned(),
|
||||||
|
description: "Museum and Gift Shop".to_owned(),
|
||||||
|
icon: "museum".to_owned(),
|
||||||
|
latitude: 51.5237669,
|
||||||
|
longitude: -0.1627829,
|
||||||
|
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
||||||
|
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
||||||
|
};
|
||||||
|
// upsert the place
|
||||||
|
let res = server.put("/places").msgpack(&places).await;
|
||||||
|
// We should get a success on the request
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
let _res_place: Place = res.msgpack();
|
||||||
|
// The correct function should be called
|
||||||
|
assert_eq!(mock_repository.update_place_count().await, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
352
src/routes.rs
352
src/routes.rs
|
|
@ -1,352 +0,0 @@
|
||||||
use axum::Router;
|
|
||||||
use axum::extract::{Path, State};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::routing::{delete, get};
|
|
||||||
use axum_msgpack::MsgPack;
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
use sqlx::sqlite::SqlitePool;
|
|
||||||
|
|
||||||
use crate::place::Place;
|
|
||||||
type Result<T, E = (StatusCode, String)> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
fn internal_error<E>(err: E) -> (StatusCode, String)
|
|
||||||
where
|
|
||||||
E: std::error::Error,
|
|
||||||
{
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_places(State(pool): State<SqlitePool>) -> Result<MsgPack<Vec<Place>>> {
|
|
||||||
let places = sqlx::query!(
|
|
||||||
r#"SELECT id, name, address, open_hours, icon, description, url,
|
|
||||||
longitude as "longitude: f64", latitude as "latitude: f64"
|
|
||||||
FROM places
|
|
||||||
WHERE active = TRUE"#
|
|
||||||
)
|
|
||||||
.fetch(&pool)
|
|
||||||
.map_ok(|p| Place {
|
|
||||||
id: Some(p.id),
|
|
||||||
name: p.name,
|
|
||||||
address: p.address,
|
|
||||||
open_hours: p.open_hours,
|
|
||||||
icon: p.icon,
|
|
||||||
description: p.description,
|
|
||||||
latitude: p.latitude,
|
|
||||||
longitude: p.longitude,
|
|
||||||
url: p.url,
|
|
||||||
})
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.map_err(internal_error)?;
|
|
||||||
|
|
||||||
Ok(MsgPack(places))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn upsert_place(
|
|
||||||
State(pool): State<SqlitePool>,
|
|
||||||
MsgPack(place): MsgPack<Place>,
|
|
||||||
) -> Result<MsgPack<Place>> {
|
|
||||||
if place.id.is_some() {
|
|
||||||
update_place(pool, place).await
|
|
||||||
} else {
|
|
||||||
insert_place(pool, place).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_place(pool: SqlitePool, mut place: Place) -> Result<MsgPack<Place>> {
|
|
||||||
let id = sqlx::query_scalar!(
|
|
||||||
r#"INSERT INTO places
|
|
||||||
(name, address, open_hours, icon, description, longitude, latitude, url)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
RETURNING id"#,
|
|
||||||
place.name,
|
|
||||||
place.address,
|
|
||||||
place.open_hours,
|
|
||||||
place.icon,
|
|
||||||
place.description,
|
|
||||||
place.longitude,
|
|
||||||
place.latitude,
|
|
||||||
place.url
|
|
||||||
)
|
|
||||||
.fetch_one(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(internal_error)?;
|
|
||||||
|
|
||||||
place.id = Some(id);
|
|
||||||
Ok(MsgPack(place))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_place(pool: SqlitePool, place: Place) -> Result<MsgPack<Place>> {
|
|
||||||
let result = sqlx::query!(
|
|
||||||
r#"UPDATE places
|
|
||||||
SET (name, address, open_hours, icon, description, longitude, latitude, url)
|
|
||||||
= (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
WHERE id = ?"#,
|
|
||||||
place.name,
|
|
||||||
place.address,
|
|
||||||
place.open_hours,
|
|
||||||
place.icon,
|
|
||||||
place.description,
|
|
||||||
place.longitude,
|
|
||||||
place.latitude,
|
|
||||||
place.url,
|
|
||||||
place.id,
|
|
||||||
)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(internal_error)?;
|
|
||||||
|
|
||||||
if result.rows_affected() == 1 {
|
|
||||||
Ok(MsgPack(place))
|
|
||||||
} else {
|
|
||||||
Err((StatusCode::NOT_FOUND, "".to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_place(State(pool): State<SqlitePool>, Path(id): Path<i64>) -> Result<()> {
|
|
||||||
let result = ::sqlx::query!("UPDATE places SET active = FALSE WHERE id = ?", id)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(internal_error)?;
|
|
||||||
|
|
||||||
if result.rows_affected() == 1 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err((StatusCode::NOT_FOUND, "".to_owned()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn places_routes(pool: SqlitePool) -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/", get(get_places).put(upsert_place))
|
|
||||||
.route("/{id}", delete(delete_place))
|
|
||||||
.with_state(pool)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod tests {
|
|
||||||
#![cfg(test)]
|
|
||||||
use super::places_routes;
|
|
||||||
use crate::place::Place;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use axum::Router;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum_test::TestServer;
|
|
||||||
use sqlx::sqlite::SqlitePool;
|
|
||||||
|
|
||||||
fn server(pool: &SqlitePool) -> Result<TestServer> {
|
|
||||||
let router = Router::new().nest("/places", places_routes(pool.clone()));
|
|
||||||
TestServer::new(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_from_db(pool: &SqlitePool, id: i64) -> Result<Place> {
|
|
||||||
sqlx::query_as!(
|
|
||||||
Place,
|
|
||||||
r#"SELECT id, name, address, open_hours, icon, description, url,
|
|
||||||
longitude as "longitude: f64", latitude as "latitude: f64"
|
|
||||||
FROM places
|
|
||||||
WHERE active = ?"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.context("Couldn't get from DB")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test]
|
|
||||||
async fn test_add_place(pool: SqlitePool) -> Result<()> {
|
|
||||||
let server = server(&pool)?;
|
|
||||||
let mut place = Place {
|
|
||||||
id: None,
|
|
||||||
name: "Sherlock Holmes".to_owned(),
|
|
||||||
address: "221 B Baker Street, London".to_owned(),
|
|
||||||
description: "Museum and Gift Shop".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: 51.5237669,
|
|
||||||
longitude: -0.1627829,
|
|
||||||
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
|
||||||
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
|
||||||
};
|
|
||||||
// Insert the place
|
|
||||||
let res = server.put("/places").msgpack(&place).await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_place: Place = res.msgpack();
|
|
||||||
// The inserted place should have an ID
|
|
||||||
assert!(res_place.id.is_some());
|
|
||||||
// Add the returned ID to the original place
|
|
||||||
place.id = res_place.id;
|
|
||||||
// And now they should be equal
|
|
||||||
assert_eq!(place, res_place);
|
|
||||||
// Check against the place stored in the DB
|
|
||||||
let db_place = get_from_db(&pool, place.id.unwrap()).await?;
|
|
||||||
assert_eq!(place, db_place);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test]
|
|
||||||
async fn test_get_places(pool: SqlitePool) -> Result<()> {
|
|
||||||
let server = server(&pool)?;
|
|
||||||
let mut places = vec![
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Sherlock Holmes".to_owned(),
|
|
||||||
address: "221 B Baker Street, London".to_owned(),
|
|
||||||
description: "Museum and Gift Shop".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: 51.5237669,
|
|
||||||
longitude: -0.1627829,
|
|
||||||
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
|
||||||
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
|
||||||
},
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Museo Nacional de Historia Natural".to_owned(),
|
|
||||||
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
|
||||||
description: "Museo".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: -70.681838888889,
|
|
||||||
longitude: -33.4421694444449,
|
|
||||||
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
|
||||||
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// insert the places
|
|
||||||
for p in &mut places {
|
|
||||||
let res = server.put("/places").msgpack(&p).await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_place: Place = res.msgpack();
|
|
||||||
// The inserted place should have an ID
|
|
||||||
assert!(res_place.id.is_some());
|
|
||||||
// Add the returned ID to the original place
|
|
||||||
p.id = res_place.id;
|
|
||||||
}
|
|
||||||
// and fetch them
|
|
||||||
let res = server.get("/places").await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let mut res_places: Vec<Place> = res.msgpack();
|
|
||||||
// and they should be equal
|
|
||||||
places.sort_by(|a, b| a.id.cmp(&b.id));
|
|
||||||
res_places.sort_by(|a, b| a.id.cmp(&b.id));
|
|
||||||
assert_eq!(places, res_places);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test]
|
|
||||||
async fn test_delete(pool: SqlitePool) -> Result<()> {
|
|
||||||
let server = server(&pool)?;
|
|
||||||
let mut places = vec![
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Sherlock Holmes".to_owned(),
|
|
||||||
address: "221 B Baker Street, London".to_owned(),
|
|
||||||
description: "Museum and Gift Shop".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: 51.5237669,
|
|
||||||
longitude: -0.1627829,
|
|
||||||
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
|
||||||
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
|
||||||
},
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Museo Nacional de Historia Natural".to_owned(),
|
|
||||||
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
|
||||||
description: "Museo".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: -70.681838888889,
|
|
||||||
longitude: -33.4421694444449,
|
|
||||||
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
|
||||||
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// insert the places
|
|
||||||
for p in &mut places {
|
|
||||||
let res = server.put("/places").msgpack(&p).await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_place: Place = res.msgpack();
|
|
||||||
// The inserted place should have an ID
|
|
||||||
assert!(res_place.id.is_some());
|
|
||||||
// Add the returned ID to the original place
|
|
||||||
p.id = res_place.id;
|
|
||||||
}
|
|
||||||
// delete the first one
|
|
||||||
let res = server
|
|
||||||
.delete(&format!("/places/{}", places[0].id.unwrap()))
|
|
||||||
.await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
|
|
||||||
// fetch the remaining places
|
|
||||||
let res = server.get("/places").await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_places: Vec<Place> = res.msgpack();
|
|
||||||
// we should only get the second place
|
|
||||||
assert_eq!(&places[1..], res_places.as_slice());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test]
|
|
||||||
async fn test_delete_not_existing(pool: SqlitePool) -> Result<()> {
|
|
||||||
let server = server(&pool)?;
|
|
||||||
// Try to delete a non-existing place
|
|
||||||
let res = server.delete("/places/33").await;
|
|
||||||
// We should get the corresponding status code
|
|
||||||
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test]
|
|
||||||
async fn test_update(pool: SqlitePool) -> Result<()> {
|
|
||||||
let server = server(&pool)?;
|
|
||||||
let mut places = vec![
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Sherlock Holmes".to_owned(),
|
|
||||||
address: "221 B Baker Street, London".to_owned(),
|
|
||||||
description: "Museum and Gift Shop".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: 51.5237669,
|
|
||||||
longitude: -0.1627829,
|
|
||||||
open_hours: "Tu-Su 09:30-18:00".to_owned(),
|
|
||||||
url: Some("https://www.sherlock-holmes.co.uk/".to_owned()),
|
|
||||||
},
|
|
||||||
Place {
|
|
||||||
id: None,
|
|
||||||
name: "Museo Nacional de Historia Natural".to_owned(),
|
|
||||||
address: "Parque Quinta Normal S/N, Santiago".to_owned(),
|
|
||||||
description: "Museo".to_owned(),
|
|
||||||
icon: "museum".to_owned(),
|
|
||||||
latitude: -70.681838888889,
|
|
||||||
longitude: -33.4421694444449,
|
|
||||||
open_hours: "Tu-Su 10:00-18:00".to_owned(),
|
|
||||||
url: Some("https://www.mnhn.gob.cl/".to_owned()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// insert original place
|
|
||||||
let res = server.put("/places").msgpack(&places[0]).await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_place: Place = res.msgpack();
|
|
||||||
// The inserted place should have an ID
|
|
||||||
assert!(res_place.id.is_some());
|
|
||||||
// Add the returned ID to the new place so we can do the update
|
|
||||||
places[1].id = res_place.id;
|
|
||||||
// update the place
|
|
||||||
let res = server.put("/places").msgpack(&places[1]).await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
|
|
||||||
// fetch the places
|
|
||||||
let res = server.get("/places").await;
|
|
||||||
// We should get a success on the request
|
|
||||||
assert_eq!(res.status_code(), StatusCode::OK);
|
|
||||||
let res_places: Vec<Place> = res.msgpack();
|
|
||||||
// we should get the updated place
|
|
||||||
assert_eq!(&places[1..], res_places.as_slice());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
src/server.rs
Normal file
37
src/server.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
//! HTTP Server
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::serve::ListenerExt;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
|
pub async fn serve(place_routes: Router) -> Result<()> {
|
||||||
|
let port = std::env::var("PORT").unwrap_or_default();
|
||||||
|
let port = str::parse(&port).unwrap_or(3000);
|
||||||
|
let address = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
||||||
|
let routes = Router::new()
|
||||||
|
.nest("/places", place_routes)
|
||||||
|
.fallback_service(ServeDir::new("static"));
|
||||||
|
|
||||||
|
tracing::debug!("listening on {}", address);
|
||||||
|
let listener = tokio::net::TcpListener::bind(address)
|
||||||
|
.await?
|
||||||
|
.tap_io(|tcp_stream| {
|
||||||
|
if let Err(err) = tcp_stream.set_nodelay(true) {
|
||||||
|
tracing::trace!("failed to set TCP_NODELAY on incoming connection: {err:#}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
axum::serve(listener, routes.into_make_service())
|
||||||
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("failed to listen for ctrl-c");
|
||||||
|
tracing::debug!("Received shutdown signal");
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-contextmenu/1.4.0/leaflet.contextmenu.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-contextmenu/1.4.0/leaflet.contextmenu.min.js"
|
||||||
integrity="sha512-8sfQf8cr0KjCeN32YPfjvLU2cMvyY1lhCXTMfpTZ16CvwIzeVQtwtKlxeSqFs/TpXjKhp1Dcv77LQmn1VFaOZg=="
|
integrity="sha512-8sfQf8cr0KjCeN32YPfjvLU2cMvyY1lhCXTMfpTZ16CvwIzeVQtwtKlxeSqFs/TpXjKhp1Dcv77LQmn1VFaOZg=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
<script crossorigin src="https://unpkg.com/@msgpack/msgpack"
|
<script crossorigin src="https://unpkg.com/@msgpack/msgpack@3.1.2/dist.umd/msgpack.min.js"
|
||||||
integrity="sha512-t/LymXW9iw9p0bciQ6uASEj+8XE/p+07CCmCyNwn06F4FK2s80IuHCY62bfSorcD4ktOJavXApp5rqtIVlg3+g=="
|
integrity="sha512-B9xeVWeBMLLUlFALrj2/h3IY/N7MJSkzBrwIltslJSlfWdPsQsQinFJ3X9PuAsz695c5qy5U0194ZqZTg8H3yg=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|
||||||
<style type="text/css" media="screen">
|
<style type="text/css" media="screen">
|
||||||
|
|
@ -26,12 +26,10 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, #map {
|
html, body, #map {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-popup-content h3 {
|
.leaflet-popup-content h3 {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
|
@ -41,20 +39,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#modal {
|
dialog {
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 400;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: rgb(0,0,0);
|
|
||||||
background-color: rgba(0,0,0,0.4);
|
|
||||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
}
|
|
||||||
#modal-form {
|
|
||||||
margin: 15vh auto;
|
margin: 15vh auto;
|
||||||
color: #333;
|
color: #333;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -63,6 +49,9 @@
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
#close {
|
#close {
|
||||||
color: #333;
|
color: #333;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
@ -94,8 +83,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div id="modal">
|
<dialog id="dialog">
|
||||||
<div id="modal-form">
|
|
||||||
<span id="close">×</span>
|
<span id="close">×</span>
|
||||||
<h1>Título</h1>
|
<h1>Título</h1>
|
||||||
<form>
|
<form>
|
||||||
|
|
@ -152,8 +140,7 @@
|
||||||
<button type="button" id="button">Enviar </button>
|
<button type="button" id="button">Enviar </button>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
<script src="client.js" onload="setupMap()"></script>
|
<script src="client.js" onload="setupMap()"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ function clearForm(): void {
|
||||||
document.getElementById("button").onclick = null;
|
document.getElementById("button").onclick = null;
|
||||||
|
|
||||||
/* Now you see it, now you don't*/
|
/* Now you see it, now you don't*/
|
||||||
document.getElementById("modal").style.display = "none";
|
(document.getElementById("dialog") as HTMLDialogElement).close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPlace(): Promise<void> {
|
async function createPlace(): Promise<void> {
|
||||||
|
|
@ -190,22 +190,16 @@ function toLink(url: string): string {
|
||||||
|
|
||||||
async function setupMap(): Promise<void> {
|
async function setupMap(): Promise<void> {
|
||||||
/* Create/Edit form */
|
/* Create/Edit form */
|
||||||
const modal = document.getElementById("modal");
|
const dialog = document.getElementById("dialog") as HTMLDialogElement;
|
||||||
const closeButton = document.getElementById("close");
|
const closeButton = document.getElementById("close");
|
||||||
|
|
||||||
closeButton.onclick = function() {
|
closeButton.onclick = function() {
|
||||||
modal.style.display = "none";
|
dialog.close();
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(e: Event) {
|
|
||||||
if (e.target == modal) {
|
|
||||||
modal.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openForm(op: Operation, lat: number, long: number): Promise<void> {
|
async function openForm(op: Operation, lat: number, long: number): Promise<void> {
|
||||||
/* Fill the form for us */
|
/* Fill the form for us */
|
||||||
const h1 = modal.getElementsByTagName("h1")[0];
|
const h1 = dialog.getElementsByTagName("h1")[0];
|
||||||
if (op == Operation.Create) {
|
if (op == Operation.Create) {
|
||||||
clearForm()
|
clearForm()
|
||||||
h1.innerText = "Añadir lugar nuevo";
|
h1.innerText = "Añadir lugar nuevo";
|
||||||
|
|
@ -255,7 +249,7 @@ async function setupMap(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make it appear */
|
/* Make it appear */
|
||||||
modal.style.display = "block";
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateForm(e: MapEvent) {
|
function openCreateForm(e: MapEvent) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue