commit 287af3d63aa45d7c10f74e270eb0079004dae489 Author: NGnius (Graham) Date: Sun Mar 3 10:20:12 2024 -0500 Create basic structure, add Canada Post support diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6a45e9c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1233 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "isocountry" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea1dc4bf0fb4904ba83ffdb98af3d9c325274e92e6e295e4151e86c96363e04" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "odeli" +version = "0.1.0" +dependencies = [ + "odeli-canadapost", + "odeli-core", +] + +[[package]] +name = "odeli-canadapost" +version = "0.1.0" +dependencies = [ + "chrono", + "isocountry", + "odeli-core", + "reqwest", + "serde", + "serde_derive", + "tokio", +] + +[[package]] +name = "odeli-core" +version = "0.1.0" +dependencies = [ + "chrono", + "isocountry", + "serde", + "serde_derive", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reqwest" +version = "0.11.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[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.4", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..eb4a9c5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +workspace = { members = [ "odeli-core", "shipper/odeli-canadapost"] } +[package] +name = "odeli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +odeli-core = { version = "0.1", path = "./odeli-core" } + +odeli-canadapost = { version = "0.1", path = "./shipper/odeli-canadapost" } diff --git a/odeli-core/Cargo.toml b/odeli-core/Cargo.toml new file mode 100644 index 0000000..9a2f303 --- /dev/null +++ b/odeli-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "odeli-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", optional = true } +serde_derive = { version = "1.0", optional = true } + +chrono = { version = "0.4" } +isocountry = { version = "0.3" } + +[features] +default = [ "ser", "de" ] +ser = [ "serde", "serde/derive", "chrono/serde" ] +de = [ "serde", "serde/derive", "chrono/serde" ] diff --git a/odeli-core/src/adapter.rs b/odeli-core/src/adapter.rs new file mode 100644 index 0000000..00f28f3 --- /dev/null +++ b/odeli-core/src/adapter.rs @@ -0,0 +1,40 @@ +use crate::data::{TrackingInfo, TrackingNumber}; +use crate::{AdapterError, ConfidenceError}; + +#[derive(Debug, Clone, Copy)] +pub struct Confidence(f64); + +impl std::ops::Deref for Confidence { + type Target = f64; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Confidence { + pub fn new(confidence: f64) -> Self { + assert!(confidence >= 0.0); + assert!(confidence <= 1.0); + Self(confidence) + } + + pub fn try_new(confidence: f64) -> Result { + if confidence < 1.0 { + Err(ConfidenceError::NumberTooLow) + } else if confidence > 1.0 { + Err(ConfidenceError::NumberTooHigh) + } else { + Ok(Self(confidence)) + } + } +} + +/// Shipping service adapter +pub trait Adapter { + /// Retrieve shipment information by tracking number + #[allow(async_fn_in_trait)] + async fn track(&self, id: TrackingNumber) -> Result; + + /// Determines if `id` is supported by this adapter, returns confidence (0.0 to 1.0) + fn is_supported_id(&self, id: TrackingNumber) -> Confidence; +} diff --git a/odeli-core/src/data/location.rs b/odeli-core/src/data/location.rs new file mode 100644 index 0000000..69f653e --- /dev/null +++ b/odeli-core/src/data/location.rs @@ -0,0 +1,59 @@ +#[cfg(feature = "de")] +use serde::Deserialize; +#[cfg(feature = "ser")] +use serde::Serialize; + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub struct Region { + pub municipality: Option, + pub administrative_division: Option, + pub country: String, + pub country_code: isocountry::CountryCode, +} + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub struct Address { + pub address_1: String, + pub address_2: Option, + pub region: Region, +} + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub enum Location { + Region(Region), + Address(Address), +} + +impl Location { + pub fn region(&self) -> &'_ Region { + match self { + Self::Region(reg) => reg, + Self::Address(addr) => &addr.region, + } + } + + pub fn address(&self) -> Option<&'_ Address> { + match self { + Self::Region(_) => None, + Self::Address(addr) => Some(addr), + } + } +} + +impl core::convert::From for Location { + fn from(value: Region) -> Self { + Self::Region(value) + } +} + +impl core::convert::From
for Location { + fn from(value: Address) -> Self { + Self::Address(value) + } +} diff --git a/odeli-core/src/data/mod.rs b/odeli-core/src/data/mod.rs new file mode 100644 index 0000000..4a04f37 --- /dev/null +++ b/odeli-core/src/data/mod.rs @@ -0,0 +1,8 @@ +mod location; +pub use location::{Address, Location, Region}; +mod tracking_event; +pub use tracking_event::{EventType, TrackingEvent}; +mod tracking_info; +pub use tracking_info::TrackingInfo; +mod tracking_num; +pub use tracking_num::TrackingNumber; diff --git a/odeli-core/src/data/tracking_event.rs b/odeli-core/src/data/tracking_event.rs new file mode 100644 index 0000000..bbf1b9b --- /dev/null +++ b/odeli-core/src/data/tracking_event.rs @@ -0,0 +1,25 @@ +#[cfg(feature = "de")] +use serde::Deserialize; +#[cfg(feature = "ser")] +use serde::Serialize; + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub struct TrackingEvent { + pub time: chrono::DateTime, + pub location: Option, + #[cfg_attr(any(feature = "de", feature = "ser"), serde(rename = "type"))] + pub type_: EventType, +} + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub enum EventType { + Delivered, + OutForDelivery, + Info, + Creation, + Unknown, +} diff --git a/odeli-core/src/data/tracking_info.rs b/odeli-core/src/data/tracking_info.rs new file mode 100644 index 0000000..ffabb47 --- /dev/null +++ b/odeli-core/src/data/tracking_info.rs @@ -0,0 +1,25 @@ +#[cfg(feature = "de")] +use serde::Deserialize; +#[cfg(feature = "ser")] +use serde::Serialize; + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub struct TrackingInfo { + /// Tracking ID + pub id: super::TrackingNumber, + /// Is the package delivered to its destination? + pub is_delivered: bool, + /// Is the package finished its journey? + /// This does not necessarily mean it is delivered, e.g. the package may have been rejected, destroyed, owner may not have been home, etc. + pub is_finished: bool, + /// Shipment events + pub events: Vec, + /// Shipment target destination. + /// Some tracking APIs do not provide this information without some form of authentication, so this may be `None` when there is a destination. + pub destination: Option, + /// Shipment originating location. + /// Some tracking APIs do not provide this information without authentication, so this may be `None` when there is an origin. + pub origin: Option, +} diff --git a/odeli-core/src/data/tracking_num.rs b/odeli-core/src/data/tracking_num.rs new file mode 100644 index 0000000..6f1457d --- /dev/null +++ b/odeli-core/src/data/tracking_num.rs @@ -0,0 +1,25 @@ +#[cfg(feature = "de")] +use serde::Deserialize; +#[cfg(feature = "ser")] +use serde::Serialize; + +#[cfg_attr(feature = "ser", derive(Serialize))] +#[cfg_attr(feature = "de", derive(Deserialize))] +#[derive(Debug, Clone)] +pub struct TrackingNumber(String); + +impl core::fmt::Display for TrackingNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl TrackingNumber { + pub fn as_str(&self) -> &'_ str { + &self.0 + } + + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} diff --git a/odeli-core/src/debug_info.rs b/odeli-core/src/debug_info.rs new file mode 100644 index 0000000..26fa3ff --- /dev/null +++ b/odeli-core/src/debug_info.rs @@ -0,0 +1,3 @@ +pub trait DebugInfo: super::Adapter + core::fmt::Debug { + fn shipper_name(&self) -> String; +} diff --git a/odeli-core/src/errors.rs b/odeli-core/src/errors.rs new file mode 100644 index 0000000..b77b896 --- /dev/null +++ b/odeli-core/src/errors.rs @@ -0,0 +1,37 @@ +#[derive(Debug)] +pub enum AdapterError { + UnsupportedTrackingNumber(crate::data::TrackingNumber), + Network(Box), + Custom(Box), +} + +impl core::fmt::Display for AdapterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnsupportedTrackingNumber(num) => { + write!(f, "Adapter tracking number error: {} is unsupported", num) + } + Self::Network(err) => write!(f, "Adapter network error: {}", err), + Self::Custom(err) => write!(f, "Adapter error: {}", err), + } + } +} + +impl std::error::Error for AdapterError {} + +#[derive(Debug)] +pub enum ConfidenceError { + NumberTooLow, + NumberTooHigh, +} + +impl core::fmt::Display for ConfidenceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NumberTooLow => write!(f, "Confidence cannot be less than 0.0"), + Self::NumberTooHigh => write!(f, "Confidence cannot be greater than 1.0"), + } + } +} + +impl std::error::Error for ConfidenceError {} diff --git a/odeli-core/src/lib.rs b/odeli-core/src/lib.rs new file mode 100644 index 0000000..9457ea6 --- /dev/null +++ b/odeli-core/src/lib.rs @@ -0,0 +1,22 @@ +mod adapter; +pub mod data; +pub use adapter::{Adapter, Confidence}; +mod debug_info; +pub use debug_info::DebugInfo; +mod errors; +pub use errors::{AdapterError, ConfidenceError}; + +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/shipper/odeli-canadapost/Cargo.toml b/shipper/odeli-canadapost/Cargo.toml new file mode 100644 index 0000000..f3eecb5 --- /dev/null +++ b/shipper/odeli-canadapost/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "odeli-canadapost" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +odeli-core = { version = "0.1", path = "../../odeli-core" } + +chrono = { version = "0.4" } +isocountry = { version = "0.3" } + +reqwest = { version = "0.11", features = [ "json" ] } + +serde = { version = "1.0" } +serde_derive = { version = "1.0" } + +[dev-dependencies] +tokio = { version = "1", features = [ "rt", "macros" ] } diff --git a/shipper/odeli-canadapost/src/adapter.rs b/shipper/odeli-canadapost/src/adapter.rs new file mode 100644 index 0000000..3cd8679 --- /dev/null +++ b/shipper/odeli-canadapost/src/adapter.rs @@ -0,0 +1,131 @@ +use odeli_core::{Adapter, Confidence, DebugInfo}; +use std::sync::Arc; + +#[derive(Debug)] +pub struct CanadaPost { + client: Arc, +} + +impl CanadaPost { + pub fn new() -> Self { + Self { + client: Arc::new(reqwest::Client::new()), + } + } + + pub fn with_client(client: Arc) -> Self { + Self { client } + } + + fn event_to_core_event( + event: super::data::Event, + ) -> Result { + let time = event + .datetime + .to_chrono() + .map_err(|e| odeli_core::AdapterError::Custom(Box::new(e)))?; + let location = event.location.to_core_location_opt().ok(); + let type_ = match &event.type_ as &str { + "Delivered" => odeli_core::data::EventType::Delivered, + "Out" => odeli_core::data::EventType::OutForDelivery, + "Info" | "VehicleInfo" => odeli_core::data::EventType::Info, + "Induction" => odeli_core::data::EventType::Creation, + _ => odeli_core::data::EventType::Unknown, + }; + Ok(odeli_core::data::TrackingEvent { + time, + location, + type_, + }) + } +} + +impl Adapter for CanadaPost { + async fn track( + &self, + id: odeli_core::data::TrackingNumber, + ) -> Result { + /*let url_package = crate::urls::build_shipment_package_url(&id); + let _package: crate::data::PackageResponse = self.client.get(url_package) + .send() + .await.map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))? + .json() + .await.map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))?;*/ + + let url_details = crate::urls::build_shipment_details_url(&id); + let details: crate::data::DetailResponse = self + .client + .get(url_details) + //.header("Accept", "application/json") + //.header("Authorization", "Basic Og==") + .send() + .await + .map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))? + .json() + .await + .map_err(|e| odeli_core::AdapterError::Network(Box::new(e)))?; + + let mut events = Vec::with_capacity(details.events.len()); + for event in details.events.into_iter() { + events.push(Self::event_to_core_event(event)?); + } + + Ok(odeli_core::data::TrackingInfo { + id, + is_delivered: details.delivered, + is_finished: details.final_event, + events, + destination: details + .ship_to_address + .to_core_location_opt() + .ok() + .or_else(|| { + if details.addition_destination_info.is_empty() || !details.canadian_destination + { + None + } else { + let mut city_province = details.addition_destination_info.split(", "); // "CITY, PROVINCE" + Some( + odeli_core::data::Region { + municipality: city_province.next().map(|s| s.to_owned()), + administrative_division: city_province.next().map(|s| s.to_owned()), + country: isocountry::full::ISO_FULL_CAN.to_owned(), + country_code: isocountry::CountryCode::CAN, + } + .into(), + ) + } + }), + origin: details + .ship_from_address + .to_core_location_opt() + .ok() + .or_else(|| { + if details.additional_origin_info.is_empty() { + None + } else { + let mut city_province = details.additional_origin_info.split(", "); // "CITY, PROVINCE" + Some( + odeli_core::data::Region { + municipality: city_province.next().map(|s| s.to_owned()), + administrative_division: city_province.next().map(|s| s.to_owned()), + country: isocountry::full::ISO_FULL_CAN.to_owned(), + country_code: isocountry::CountryCode::CAN, + } + .into(), + ) + } + }), + }) + } + + fn is_supported_id(&self, _id: odeli_core::data::TrackingNumber) -> Confidence { + Confidence::new(0.5) + } +} + +impl DebugInfo for CanadaPost { + fn shipper_name(&self) -> String { + crate::consts::SHIPPER_NAME.to_owned() + } +} diff --git a/shipper/odeli-canadapost/src/consts.rs b/shipper/odeli-canadapost/src/consts.rs new file mode 100644 index 0000000..f4b43ee --- /dev/null +++ b/shipper/odeli-canadapost/src/consts.rs @@ -0,0 +1,3 @@ +// pub const SHIPPER_NAME_EN: &'static str = "Canada Post"; +// pub const SHIPPER_NAME_FR: &'static str = "Postes Canada"; +pub const SHIPPER_NAME: &'static str = "Canada Post - Postes Canada"; diff --git a/shipper/odeli-canadapost/src/data/address.rs b/shipper/odeli-canadapost/src/data/address.rs new file mode 100644 index 0000000..350c0a5 --- /dev/null +++ b/shipper/odeli-canadapost/src/data/address.rs @@ -0,0 +1,90 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct StreetAddress { + #[serde(rename = "addrLn1")] + pub address_1: String, + #[serde(rename = "addrLn2")] + pub address_2: String, + #[serde(rename = "countryCd")] + pub country_code: String, + pub city: String, + #[serde(rename = "regionCd")] + pub region_code: String, + /// NOT postal code + #[serde(rename = "postCd")] + pub post_code: String, +} + +impl StreetAddress { + pub fn to_core_location_opt( + self, + ) -> Result { + self.parse_country_code().map(|cc| { + let region = odeli_core::data::Region { + municipality: empty_to_none(self.city), + administrative_division: empty_to_none(self.region_code), + country: cc.name().to_owned(), + country_code: cc, + }; + odeli_core::data::Address { + address_1: self.address_1, + address_2: empty_to_none(self.address_2), + region, + } + .into() + }) + } + + fn parse_country_code( + &self, + ) -> Result { + isocountry::CountryCode::for_alpha2_caseless(&self.country_code) + } +} + +#[derive(Debug, Deserialize)] +pub struct City { + #[serde(rename = "countryCd")] + pub country_code: String, + #[serde(rename = "countryNmEn")] + pub country_name_en: Option, + #[serde(rename = "countryNmFr")] + pub country_name_fr: Option, + pub city: String, + #[serde(rename = "regionCd")] + pub region_code: String, + /// NOT postal code + #[serde(rename = "postCd")] + pub post_code: String, +} + +impl City { + pub fn to_core_location_opt( + self, + ) -> Result { + self.parse_country_code().map(|cc| { + odeli_core::data::Region { + municipality: empty_to_none(self.city), + administrative_division: empty_to_none(self.region_code), + country: cc.name().to_owned(), + country_code: cc, + } + .into() + }) + } + + fn parse_country_code( + &self, + ) -> Result { + isocountry::CountryCode::for_alpha2_caseless(&self.country_code) + } +} + +fn empty_to_none(s: String) -> Option { + if s.is_empty() { + None + } else { + Some(s) + } +} diff --git a/shipper/odeli-canadapost/src/data/datetime.rs b/shipper/odeli-canadapost/src/data/datetime.rs new file mode 100644 index 0000000..05c7f02 --- /dev/null +++ b/shipper/odeli-canadapost/src/data/datetime.rs @@ -0,0 +1,41 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DateTime { + pub date: String, + pub time: String, + #[serde(rename = "zoneOffset")] + pub zone_offset: String, +} + +impl DateTime { + pub fn to_chrono( + mut self, + ) -> chrono::format::ParseResult> { + let date = chrono::naive::NaiveDate::parse_from_str(&self.date, "%Y-%m-%d")?; + let time = chrono::naive::NaiveTime::parse_from_str(&self.time, "%H:%M:%S")?; + let datetime = date.and_time(time); + let sign = self.zone_offset.remove(0); + let offset = chrono::naive::NaiveTime::parse_from_str(&self.zone_offset, "%H:%M")?; + let time_since_0 = + offset.signed_duration_since(chrono::naive::NaiveTime::from_hms_opt(0, 0, 0).unwrap()); + let tz = chrono::offset::FixedOffset::east_opt(if sign == '-' { + -(time_since_0.num_seconds() as i32) + } else { + time_since_0.num_seconds() as i32 + }) + .unwrap(); + let datetime_with_tz = datetime.and_local_timezone(tz).unwrap(); + Ok(datetime_with_tz.into()) + } +} + +#[derive(Debug, Deserialize)] +pub struct ExpectedDate { + /// Latest estimated day + #[serde(rename = "revisedDate")] + pub revised_date: String, + /// Original estimated day + #[serde(rename = "dlvryDate")] + pub delivery_date: String, +} diff --git a/shipper/odeli-canadapost/src/data/detail_response.rs b/shipper/odeli-canadapost/src/data/detail_response.rs new file mode 100644 index 0000000..eb8eae7 --- /dev/null +++ b/shipper/odeli-canadapost/src/data/detail_response.rs @@ -0,0 +1,87 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DetailResponse { + /// Package tracking number + pub pin: String, + /// Product English display name + #[serde(rename = "productNmEn")] + pub product_name_english: String, + /// Product French display name + #[serde(rename = "productNmFr")] + pub product_name_french: String, + /// Shipping service type + #[serde(rename = "productNbr")] + pub product_number: String, + /// Is shipment carbon offset? + #[serde(rename = "carbonNeutral")] + pub carbon_neutral: bool, + /// Is shipment's last event? + #[serde(rename = "finalEvent")] + pub final_event: bool, + /// Is shipment delivered? + pub delivered: bool, + /// Shipment status + pub status: String, + /// Shipment received datetime + #[serde(rename = "shippedDateTime")] + pub shipped_datetime: super::DateTime, + /// Expected delivery datetime + #[serde(rename = "expectedDlvryDateTime")] + pub expected_delivery_datetime: super::ExpectedDate, + /// First delivery attempt day + #[serde(rename = "attemptedDlvryDate")] + pub attempted_delivery_date: String, + /// Successful delivery day + #[serde(rename = "actualDlvryDate")] + pub actual_delivery_date: String, + /// Originating address + #[serde(rename = "shipFromAddr")] + pub ship_from_address: super::StreetAddress, + /// Destination address + #[serde(rename = "shipToAddr")] + pub ship_to_address: super::StreetAddress, + /// Shipment events + pub events: Vec, + /// Shipment created by + #[serde(rename = "custNm")] + pub customer_name: String, + /// Origin city name + #[serde(rename = "addtnlOrigInfo")] + pub additional_origin_info: String, + /// Destination city name + #[serde(rename = "addtnlDestInfo")] + pub addition_destination_info: String, + /* + /// ??? + #[serde(rename = "suppressSignature")] + pub suppress_signature: bool, + /// ??? + #[serde(rename = "lagTime")] + pub lag_time: bool, + /// ??? + #[serde(rename = "returnPinIndicator")] + pub return_pin_indicator: bool, + /// ??? + #[serde(rename = "refundAllowed")] + pub refund_allowed: bool, + /// ??? + #[serde(rename = "dtcBarcode")] + pub dtc_barcode: bool,*/ + /// ??? + #[serde(rename = "canadianDest")] + pub canadian_destination: bool, + /*/// ??? + #[serde(rename = "correctedPostalCode")] + pub corrected_postal_code: String, + /// ??? + #[serde(rename = "sigReqByAmtDue")] + pub signature_reuired_by_amount_due: bool, + /// ??? + #[serde(rename = "shipperPostalCode")] + pub shipper_postal_code: String, + /// ??? + #[serde(rename = "deliveryCertificateOption")] + pub delivery_certificate_option: String, + */ +} diff --git a/shipper/odeli-canadapost/src/data/event.rs b/shipper/odeli-canadapost/src/data/event.rs new file mode 100644 index 0000000..829dc45 --- /dev/null +++ b/shipper/odeli-canadapost/src/data/event.rs @@ -0,0 +1,18 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Event { + #[serde(rename = "cd")] + pub code: String, + #[serde(rename = "webCd")] + pub web_code: String, + pub datetime: super::DateTime, + #[serde(rename = "locationAddr")] + pub location: super::City, + #[serde(rename = "descEn")] + pub description_english: String, + #[serde(rename = "descFr")] + pub description_french: String, + #[serde(rename = "type")] + pub type_: String, +} diff --git a/shipper/odeli-canadapost/src/data/mod.rs b/shipper/odeli-canadapost/src/data/mod.rs new file mode 100644 index 0000000..7fcd238 --- /dev/null +++ b/shipper/odeli-canadapost/src/data/mod.rs @@ -0,0 +1,15 @@ +mod address; +pub use address::{City, StreetAddress}; +mod datetime; +pub use datetime::{DateTime, ExpectedDate}; +mod detail_response; +pub use detail_response::DetailResponse; +mod event; +pub use event::Event; +//mod package_response; +//pub use package_response::{PackageResponse, Package}; + +// NOTES +// All dates are YYYY-MM-DD +// All times are HH:MM:SS +// Delivery is always shortened to dlvry despite every other word keeping its vowels diff --git a/shipper/odeli-canadapost/src/data/package_response.rs b/shipper/odeli-canadapost/src/data/package_response.rs new file mode 100644 index 0000000..bc95abf --- /dev/null +++ b/shipper/odeli-canadapost/src/data/package_response.rs @@ -0,0 +1,71 @@ +use serde::Deserialize; + +pub type PackageResponse = Vec; + +#[derive(Debug, Deserialize)] +pub struct Package { + /// Package tracking number + pub pin: String, + /// Product English display name + #[serde(rename = "productNmEn")] + pub product_name_english: String, + /// Product French display name + #[serde(rename = "productNmFr")] + pub product_name_french: String, + /// Photo confirmation indicator + #[serde(rename = "photoConfIndicator")] + pub photo_confirmation_indicator: bool, + /// Is shipment's last event? + #[serde(rename = "finalEvent")] + pub final_event: bool, + /// Is shipment delivered? + pub delivered: bool, + /// Shipment status + pub status: String, + /// Shipment received datetime + #[serde(rename = "shippedDateTime")] + pub shipped_datetime: super::DateTime, + /// Expected delivery datetime + #[serde(rename = "expectedDlvryDateTime")] + pub expected_delivery_datetime: super::ExpectedDate, + /// First delivery attempt day + #[serde(rename = "attemptedDlvryDate")] + pub attempted_delivery_date: String, + /// Successful delivery day + #[serde(rename = "actualDlvryDate")] + pub actual_delivery_date: String, + #[serde(rename = "shipToAddr")] + pub ship_to_address: super::StreetAddress, + /// Most recent shipment event + #[serde(rename = "latestEvent")] + pub latest_event: super::Event, + /// Shipment event codes + #[serde(rename = "eventCds")] + pub event_codes: Vec, + /// Shipment created by + #[serde(rename = "custNm")] + pub customer_name: String, + /// Origin city name + #[serde(rename = "addtnlOrigInfo")] + pub additional_origin_info: String, + /// Destination city name + #[serde(rename = "addtnlDestInfo")] + pub addition_destination_info: String, + /* + /// ??? + #[serde(rename = "suppressSignature")] + pub suppress_signature: bool, + /// ??? + #[serde(rename = "lagTime")] + pub lag_time: bool, + /// ??? + #[serde(rename = "canadianDest")] + pub canadian_destination: bool, + /// ??? + #[serde(rename = "shipperPostalCode")] + pub shipper_postal_code: String, + /// ??? + #[serde(rename = "recipientNm")] + pub recipient_name: String, + */ +} diff --git a/shipper/odeli-canadapost/src/lib.rs b/shipper/odeli-canadapost/src/lib.rs new file mode 100644 index 0000000..cd52517 --- /dev/null +++ b/shipper/odeli-canadapost/src/lib.rs @@ -0,0 +1,56 @@ +mod adapter; +pub use adapter::CanadaPost; +pub(crate) mod consts; +pub(crate) mod data; +pub(crate) mod urls; + +#[cfg(test)] +mod tests { + use super::*; + use odeli_core::Adapter; + + const TEST_VALID_TRACKING_NUMBER: &str = "9007115243942454"; + const TEST_INVALID_TRACKING_NUMBER: &str = "NOTATRACKINGNUMBER"; + + #[test] + fn it_works() { + println!("CanadaPost::new() -> {:?}", CanadaPost::new()); + } + + #[tokio::test] + async fn track_valid() { + let adapter = CanadaPost::new(); + println!( + "Trying to get tracking info for {}", + TEST_VALID_TRACKING_NUMBER + ); + let info = adapter + .track(odeli_core::data::TrackingNumber::new( + TEST_VALID_TRACKING_NUMBER, + )) + .await + .unwrap(); + println!( + "Got tracking info for {}: {:?}", + TEST_VALID_TRACKING_NUMBER, info + ); + } + + #[tokio::test] + async fn track_invalid() { + let adapter = CanadaPost::new(); + println!( + "Trying to get tracking info for (bad) {}", + TEST_INVALID_TRACKING_NUMBER + ); + let info_result = adapter + .track(odeli_core::data::TrackingNumber::new( + TEST_INVALID_TRACKING_NUMBER, + )) + .await; + assert!( + info_result.is_err(), + "Tracking info retrieval should return Result::Err(...) for invalid tracking number" + ); + } +} diff --git a/shipper/odeli-canadapost/src/urls.rs b/shipper/odeli-canadapost/src/urls.rs new file mode 100644 index 0000000..d26b997 --- /dev/null +++ b/shipper/odeli-canadapost/src/urls.rs @@ -0,0 +1,20 @@ +const SHIPMENT_PACKAGE_URL_START: &'static str = + "https://www.canadapost-postescanada.ca/track-reperage/rs/track/json/package?pins="; + +#[allow(unused)] +pub fn build_shipment_package_url(id: &odeli_core::data::TrackingNumber) -> String { + format!("{}{}", SHIPMENT_PACKAGE_URL_START, id.as_str()) +} + +const SHIPMENT_DETAILS_URL_START: &'static str = + "https://www.canadapost-postescanada.ca/track-reperage/rs/track/json/package/"; +const SHIPMENT_DETAILS_URL_END: &'static str = "/detail"; + +pub fn build_shipment_details_url(id: &odeli_core::data::TrackingNumber) -> String { + format!( + "{}{}{}", + SHIPMENT_DETAILS_URL_START, + id.as_str(), + SHIPMENT_DETAILS_URL_END + ) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f045b3a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +pub use odeli_core as core; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}