diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f069717..ec2abe9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest environment: test steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - - name: Run tests - env: - DEVID: ${{ secrets.DEVID }} - KEY: ${{ secrets.KEY }} - QUIET: true - run: cargo test -- --nocapture + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + env: + DEVID: ${{ secrets.DEVID }} + KEY: ${{ secrets.KEY }} + QUIET: true + run: cargo test -- --nocapture diff --git a/Cargo.lock b/Cargo.lock index 356d4ae..dc6cf80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.4", ] @@ -134,6 +135,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.4" @@ -169,6 +176,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -186,6 +206,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -247,6 +273,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -254,6 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -262,6 +304,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -280,10 +350,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -499,6 +575,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -745,9 +830,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -759,23 +844,37 @@ dependencies = [ "anyhow", "chrono", "colored", + "derive_more", "dotenv", + "futures", "hex", "hmac", + "itertools", "once_cell", + "ptvrs-macros", "reqwest", "serde", "serde_json", "sha1", "to_and_fro", "tokio", + "url-escape", +] + +[[package]] +name = "ptvrs-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -837,6 +936,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.31" @@ -903,6 +1011,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.197" @@ -999,9 +1113,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.52" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", @@ -1221,6 +1335,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "url-escape" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218" +dependencies = [ + "percent-encoding", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 13471d1..3cdc14f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,13 @@ repository = "https://github.com/tascord/ptvrs" [dependencies] anyhow = "1.0.81" -chrono = "0.4.35" +chrono = { version = "0.4.35", features = ["serde"] } colored = "2.1.0" +derive_more = "0.99.18" dotenv = "0.15.0" hex = "0.4.3" hmac = "0.12.1" +itertools = "0.13.0" once_cell = "1.19.0" reqwest = { version = "0.12.0", features = ["json"] } serde = { version = "1.0.197", features = ["derive"] } @@ -22,3 +24,12 @@ serde_json = "1.0.114" sha1 = "0.10.6" to_and_fro = "0.5.0" tokio = { version = "1.36.0", features = ["full"] } +url-escape = "0.1.1" + +[dev-dependencies] +futures = "0.3.30" +ptvrs-macros = { path = "ptvrs-macros" } + + +[workspace] +members = ["ptvrs-macros"] diff --git a/README.md b/README.md index e0d83c9..daef6c0 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,13 @@ | **Outlets** | [/outlets](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.outlets) | 🟦 | | | | [/outlets/location/{}/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.outlets_lat_long) | 🟦 | | | **Patterns** | [/pattern/run/{}/route_type/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.patterns_run_route) | 🟦 | | +| **Search** | /search/{} | 🟦 | | | **Routes** | [/routes](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.routes) | 🟨 | Types not yet concrete. See docs. | | | [/routes/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.routes_id) | 🟨 | " | | **Runs** | [/runs/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.runs_ref) | 🟨 | " | | | [/runs/route/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.runs_id) | 🟨 | " | | | [/runs/{}/route_type/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.runs_ref_type) | 🟨 | " | | | [/runs/route/{}/route_type/{}](https://docs.rs/ptv/latest/ptv/struct.Client.html#method.runs_id_type) | 🟨 | " | -| **Search** | /search/{} | ❌ | Not implemented | -| **Stops** | /stops/{}/route_type/{} | ❌ | " | -| | /stops/route/{}/route_type/{} | ❌ | " | +| **Stops** | /stops/{}/route_type/{} | 🟨 | " | +| | /stops/route/{}/route_type/{} | ❌ | Not Implemented | | | /stops/location/{}/{} | ❌ | " | diff --git a/ptvrs-macros/Cargo.toml b/ptvrs-macros/Cargo.toml new file mode 100644 index 0000000..78887bc --- /dev/null +++ b/ptvrs-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ptvrs-macros" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.36" +syn = { version = "2.0.70", features = ["full"] } + +[lib] +proc-macro = true diff --git a/ptvrs-macros/src/lib.rs b/ptvrs-macros/src/lib.rs new file mode 100644 index 0000000..714adea --- /dev/null +++ b/ptvrs-macros/src/lib.rs @@ -0,0 +1,315 @@ +use std::fmt::format; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{quote, ToTokens}; + +use syn::{ + bracketed, ext, parse::{self, discouraged::Speculative, Parse}, parse_macro_input, parse_quote, parse_quote_spanned, punctuated::{self, Punctuated}, spanned::Spanned, token, Expr, ExprStruct, FieldValue, Ident, LitStr, PatLit, Path, Stmt, Token +}; + +struct Bracketed { + bracket: token::Bracket, + inner: T, +} + +impl Parse for Bracketed { + fn parse(input: parse::ParseStream) -> syn::Result { + let content; + Ok(Self { + bracket: bracketed!(content in input), + inner: content.parse()?, + }) + } +} + +enum Grouped2 { + SingleSet(T), + Set(Bracketed, Token![,]>>), +} +impl Parse for Grouped2 { + fn parse(input: parse::ParseStream) -> syn::Result { + if input.peek(token::Bracket) { + let content; + Ok(Self::Set(Bracketed { + bracket: bracketed!(content in input ), + inner: Punctuated::parse_separated_nonempty(&content)?, + })) + } else { + Ok(Self::SingleSet(input.parse()?)) + } + } +} + +struct ExtraParams { + _comma: Token![,], + expr: Punctuated, +} + +impl Parse for ExtraParams { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let _comma = input.parse()?; + let expr = Punctuated::parse_separated_nonempty(input)?; + Ok(Self { _comma, expr }) + } +} + +struct Field { + field_name: Ident, + value: Option, +} + +impl Parse for Field { + fn parse(input: parse::ParseStream) -> syn::Result { + Ok(Self { + field_name: input.parse()?, + value: if input.peek(Token![:]) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }, + }) + } +} + +enum Grouped { + Single(T), + Set { + bracket: token::Bracket, + inner: Punctuated, + }, +} + +impl Parse for Grouped { + fn parse(input: parse::ParseStream) -> syn::Result { + if input.peek(token::Bracket) { + let content; + Ok(Grouped::Set { + bracket: bracketed!(content in input), + inner: Punctuated::parse_separated_nonempty(&content)?, + }) + } else { + Ok(Grouped::Single(input.parse()?)) + } + } +} + +struct Struct { + _comma2: Token![,], + struc: Path, + _arrow: Token![=>], + _bracket: token::Bracket, + idents: Punctuated, Token![,]>, +} +impl Parse for Struct { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(Self { + _comma2: input.parse()?, + struc: input.parse()?, + _arrow: input.parse()?, + _bracket: bracketed!(content in input), + idents: if content.is_empty() { + Punctuated::new() + } else { + Punctuated::parse_separated_nonempty(&content)? + }, + }) + } +} + +struct Params { + struc: Option, + extraparams: Option, +} +impl Parse for Params { + fn parse(input: parse::ParseStream) -> syn::Result { + Ok(Self { + struc: { + let fork = input.fork(); + if let Ok(struc) = fork.parse::() { + input.advance_to(&fork); + Some(struc) + } else { + None + } + }, + extraparams: { + let fork = input.fork(); + if let Ok(extraparams) = fork.parse() { + input.advance_to(&fork); + Some(extraparams) + } else { + None + } + }, + }) + } +} + +struct MacroInput { + map: Expr, + _comma: Token![,], + name: Ident, + params: Grouped2, +} + +impl Parse for MacroInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Self { + map: input.parse()?, + _comma: input.parse()?, + name: input.parse()?, + params: input.parse()?, + }) + } +} + +#[proc_macro] +pub fn make_test(_input: TokenStream) -> TokenStream { + // parse the input of the macro, which is (Expr, Ident, Ident => [Ident+], Expr+) + let MacroInput { + map, name, params, .. + } = parse_macro_input!(_input as MacroInput); + let stmts = match params { + Grouped2::SingleSet(Params { extraparams, struc }) => { + create_statement_vec(struc, &map, &name, &name.to_string(), extraparams) + } + Grouped2::Set(Bracketed { bracket, inner }) => inner + .into_iter() + .flat_map( + |Bracketed { + inner: Params { struc, extraparams }, + .. + }| { + let test_name = if let Some(ExtraParams { ref expr, .. }) = extraparams { + format!("{}({})", name, expr.to_token_stream()) + } else { + name.to_string() + }; + create_statement_vec(struc, &map, &name, &test_name, extraparams) + }, + ) + .collect(), + }; + + quote! { + #(#stmts);* + } + .into() +} + +fn create_statement_vec( + struc: Option, + map: &Expr, + name: &Ident, + test_name: &str, + extraparams: Option, +) -> Vec { + let mut stmts: Vec = if let Some(Struct { idents, struc, .. }) = &struc { + idents + .iter() + .map(|group| { + let map = ↦ + let struc = struc; + let extraparams = extraparams.as_ref().map(|x| &x.expr); + let (test_name, parsed_struc, span) = match group { + Grouped::Single(Field { field_name, value }) => { + let test_name = format!("{}::{}", test_name, field_name); + let value: Expr = if let Some(value) = value { + parse_quote_spanned!{ value.span() => + Some(#value) + } + } else { + parse_quote_spanned! { field_name.span() => + Some(true) + } + }; + let parsed_struc: Expr = parse_quote_spanned! { field_name.span() => + #struc { + #field_name: #value, + ..Default::default() + } + }; + (test_name, parsed_struc, field_name.span()) + } + Grouped::Set { bracket, inner } => { + let mut test_name_vec = vec![format!("{}::",test_name)]; + let fields = inner + .iter() + .map(|Field { field_name, value } | -> FieldValue { match value { + Some(value) => { + test_name_vec.push(format!( + "{}({})", + field_name.to_string(), + value.to_token_stream() + )); + parse_quote_spanned! { value.span() => #field_name: Some(#value) } + } + None => { + test_name_vec.push(field_name.to_string()); + parse_quote_spanned! { field_name.span() => #field_name: Some(true)} + } + }}) + .collect::>(); + let test_name = test_name_vec.join(","); + + let parsed_struc: Expr = parse_quote_spanned! { bracket.span => + #struc { + #fields , + ..Default::default() + }}; + (test_name, parsed_struc, bracket.span.span()) + } + }; + + let params: Punctuated = if let Some(expr) = extraparams { + parse_quote!(#expr, #parsed_struc) + } else { + parse_quote!(#parsed_struc ) + }; + let test_name: LitStr = parse_quote! { + #test_name + }; + + + let wow: Stmt = parse_quote_spanned! { span => + #map.insert(#test_name, Arc::new(|| { + Box::pin(async { + let res = (CLIENT.#name(#params)).await?; + Ok(format!("{:?}", res)) + }) + }));}; + + wow + }) + .collect() + } else { + vec![] + }; + let default_params: Punctuated = match (extraparams, struc) { + (Some(ExtraParams { expr, .. }), Some(Struct { struc, .. })) => { + parse_quote!(#expr, #struc::default()) + } + (Some(ExtraParams { expr, .. }), None) => { + parse_quote!(#expr) + } + (None, Some(Struct { struc, .. })) => { + parse_quote!(#struc::default()) + } + (None, None) => { + parse_quote!() + } + }; + + stmts.push(parse_quote! { + #map.insert(stringify!(#name), Arc::new(|| { + Box::pin(async { + let res = (CLIENT.#name(#default_params)).await?; + Ok(format!("{:?}", res)) + }) + })); + }); + stmts +} diff --git a/src/helpers.rs b/src/helpers.rs index 25f675f..a8edd5b 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,5 +1,8 @@ use chrono::NaiveDateTime; -use serde::{Deserialize, Deserializer, Serialize}; +use itertools::Itertools; +use serde::{de::SeqAccess, ser, Deserialize, Deserializer, Serialize}; + +use crate::DisruptionModes; pub fn clean(s: String) -> String { let mut s = s; @@ -21,7 +24,13 @@ pub fn to_query(s: T) -> String { v.as_array() .unwrap() .iter() - .map(|v| format!("{}={}", k, clean(v.to_string()))) + .map(|v| { + format!( + "{}={}", + k, + url_escape::encode_query(&clean(v.to_string())).into_owned() + ) + }) .collect::>() .join("&") } else { @@ -37,8 +46,8 @@ where D: Deserializer<'de>, { let s: String = String::deserialize(deserializer)?; - Ok(NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") - .map_err(|e| serde::de::Error::custom(format!("Error deser iso_8601 '{s}': {e:?}")))?) + NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") + .map_err(|e| serde::de::Error::custom(format!("Error deser iso_8601 '{s}': {e:?}"))) } pub fn ser_iso_8601(date: &Option, serializer: S) -> Result @@ -89,8 +98,8 @@ where D: Deserializer<'de>, { let s: String = String::deserialize(deserializer)?; - Ok(NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.3fZ") - .map_err(|e| serde::de::Error::custom(format!("Error deser rfc3339 '{s}': {e:?}")))?) + NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.3fZ") + .map_err(|e| serde::de::Error::custom(format!("Error deser rfc3339 '{s}': {e:?}"))) } pub fn opt_de_rfc3339<'de, D>(deserializer: D) -> Result, D::Error> @@ -107,3 +116,94 @@ where None => Ok(None), } } + +pub fn deserialize_path<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct PathVisitor; + impl<'de> serde::de::Visitor<'de> for PathVisitor { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a list of lists of tuples of f64") + } + // primarily deserializing [ + // "-37.8683203000288, 145.079655599963 -37.8655618999753, 145.080159200029 -37.8643671999759, 145.080394800045 -37.8637750999515, 145.080557999986 -37.8635422999975, 145.080654900021 -37.8582984000176, 145.082927900024 -37.8579842999768, 145.083004200024 -37.8572559000092, 145.083102600027 -37.8562641000235, 145.083071499962 -37.8556125999982, 145.082906500037 -37.8554588999607, 145.082876400034 -37.8545596999964, 145.082445099949 -37.8437658999819, 145.075417500015 -37.8425731999696, 145.074675700034 -37.8364900000393, 145.070811399995 -37.8348502000055, 145.069729200021 -37.8342419999945, 145.069460999968 -37.8338166999764, 145.069358400021 -37.8330147999865, 145.069368000002 -37.832817199959, 145.069407200039 -37.832754099991, 145.069408900034 -37.8310293000002, 145.069737900034 -37.8283767000385, 145.070102400024 -37.8280790999898, 145.070087399984 -37.8277532999815, 145.070005000044 -37.8271535999531, 145.069702499947 -37.8267788000005, 145.06938279996 -37.8265577999701, 145.069093199963 -37.8263718999837, 145.068745799971 -37.8261712000388, 145.068046599958 -37.8261099999648, 145.067616499972 -37.8261036999724, 145.06723039997 -37.8262540999595, 145.065965300042 -37.8262642999546, 145.0654879 -37.8262822000221, 145.06493069998 -37.8263466999918, 145.06337249997 -37.8264930000373, 145.061857600011 -37.8265761000108, 145.058696899958", + // "-37.8265761000108, 145.058696899958 -37.8265754000173, 145.058651500023 -37.8265763000059, 145.057617500001 -37.826581799995, 145.057401500045 -37.8265383999863, 145.056959599996 -37.8264497000363, 145.056496099966 -37.8262976999717, 145.056022899954 -37.8259514000122, 145.055247999985 -37.8256736999505, 145.054800899955 -37.8244995000169, 145.053547899972 -37.824084899963, 145.053002099991 -37.8238341999955, 145.052554200003 -37.8236089999777, 145.052014799968 -37.8233684000126, 145.051089500046 -37.8230483000229, 145.049177900008 -37.8229320999804, 145.048692500033 -37.8225329000186, 145.04689660003 -37.8223983000171, 145.045843600042 -37.8223874000308, 145.045730300034 -37.8223666999572, 145.045571800029 -37.8223513000041, 145.045185899959 -37.8223313999499, 145.045072800009 -37.8221798999673, 145.0424638 -37.821867500024, 145.039938600033 -37.8217714999782, 145.039043700026 -37.8213715000086, 145.035589300009 -37.8213602000302, 145.035453200002 -37.8213348000065, 145.033931600034 -37.8212586999663, 145.032070400043 -37.8210688000277, 145.030416799984 -37.8208471000355, 145.029013999973 -37.8206689999712, 145.027530499997 -37.8206078000258, 145.026566500027 -37.8206462000339, 145.026167899985 -37.8208369999824, 145.025185800001 -37.8208875999509, 145.024979899976 -37.8211090000076, 145.024212899959 -37.8212744999776, 145.023867599988 -37.8215449999596, 145.023337799954 -37.8218259999648, 145.022898600037 -37.8221793999933, 145.022480200043 -37.8226675999865, 145.02203539995 -37.8242175999786, 145.019892300033 -37.8247437999656, 145.019026099987 -37.8255391999616, 145.017561899967 -37.8257320000204, 145.017238700006 -37.8258198000133, 145.017099999966 -37.8259512999542, 145.016880599982 -37.8260128000115, 145.016788099958 -37.826949699979, 145.015160999955 -37.8272185999868, 145.014540299957 -37.8274428000346, 145.013943500001 -37.8275291999788, 145.013725299965 -37.8276387000278, 145.013267899969 -37.8278159999823, 145.012558699971 -37.8279087000195, 145.011647299991 -37.8279017999988, 145.010704400053 -37.82756019996, 145.007555100033 -37.8274864999539, 145.006932200036 -37.8261322000209, 144.994573400054 -37.8260058000082, 144.994031500047 -37.8256500000113, 144.993245800019 -37.825529800031, 144.993067300004 -37.8252169000015, 144.992689499967 -37.8250692999908, 144.992489000002 -37.8249492999501, 144.992321799946 -37.8247286999589, 144.992066499959 -37.8244776999975, 144.991618799972 -37.8245284999882, 144.991424300051 -37.8243490000252, 144.990940700042 -37.8241955999787, 144.990399499978 -37.8241011999923, 144.99015210001 -37.8236761000357, 144.989004800006 -37.8228364999981, 144.985755700012 -37.822291599961, 144.984452700014 -37.8218150999527, 144.983465899969 -37.8201561999773, 144.98079590002 -37.8192841999558, 144.979342799971 -37.8184652000271, 144.977831600038 -37.8178993000183, 144.976892799959 -37.8173584000181, 144.975828300025 -37.8170090999653, 144.974906399995 -37.8168934000328, 144.974466499945 -37.8168523000383, 144.974172200015 -37.8167973000098, 144.973594399995 -37.8167826000027, 144.973265400031 -37.8167958999745, 144.972992400029 -37.8168180000008, 144.972707799979 -37.8169317999936, 144.97198899997 -37.8173818000015, 144.970386299946 -37.8177335000255, 144.968831700008 -37.8180060999924, 144.96791540002 -37.8180839999503, 144.967731500016 -37.8183051000073, 144.966964299956", + // "-37.8201561999773, 144.98079590002 -37.8177107999522, 144.97693200003 -37.8171955999625, 144.97632120001 -37.8166556000173, 144.975836200011 -37.8162349999771, 144.975484099993 -37.8159246000112, 144.975254099958 -37.8156684999692, 144.975033899985 -37.8155869000169, 144.975002000052 -37.8152870999753, 144.974862500026 -37.8150237000143, 144.974744800009 -37.8087737999705, 144.97184869999 -37.8086363000065, 144.971716200048 -37.8085259000299, 144.971582900021 -37.8083964000279, 144.971393299951 -37.8082289999986, 144.971091199971 -37.808172399957, 144.970945099968 -37.8080764999841, 144.970606999995 -37.808025400012, 144.970256300053 -37.8080015000372, 144.969916199985 -37.8079953999606, 144.96956419998 -37.808034999982, 144.969245100018 -37.8080848999907, 144.969005199993 -37.8099386999852, 144.962593500014 -37.8103137999581, 144.961356399982 -37.8117517000372, 144.956443799957 -37.8133308999864, 144.950868300016 -37.8134615000047, 144.950603399969 -37.8135931000251, 144.950395299996 -37.8138054999956, 144.950173599965 -37.81398329996, 144.950032400054 -37.81418889995, 144.949935799977 -37.8145199999943, 144.949801699966 -37.8147624000128, 144.949749500022 -37.8149150999922, 144.949722600005 -37.8151223000106, 144.949716899987 -37.8153748999829, 144.949732599976 -37.8154472999846, 144.949753299957 -37.8156823000166, 144.949792299978 -37.8161265999766, 144.949950400014 -37.8163266000045, 144.950047100001 -37.8172413000239, 144.950828299953 -37.8174253999886, 144.951050400047 -37.8194719000155, 144.952652399974 -37.820146400037, 144.953088200015 -37.8203663000234, 144.953297999947 -37.820983800037, 144.954087499969 -37.8211138999842, 144.95431110001 -37.8212360999886, 144.954603099955 -37.8213497000187, 144.954918100029 -37.8214091000209, 144.955223199949 -37.8214777999948, 144.955550700002 -37.8214793999859, 144.955641600043 -37.8214930000136, 144.95590249995 -37.8214808999926, 144.956243599998 -37.821366399953, 144.956917099953 -37.8210132000064, 144.957858399986 -37.8199780000159, 144.960522499975 -37.8198747999727, 144.960809400014 -37.8197300000066, 144.961290500009 -37.8196731999909, 144.961655599964 -37.819358000036, 144.962709400051 -37.8190125000142, 144.964093500022 -37.8188720999518, 144.964835699955 -37.8183051000073, 144.966964299956" + //] + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut paths = Vec::new(); + while let Some(path) = seq.next_element::()? { + // take a + // "-37.8201561999773, 144.98079590002 -37.8177107999522, 144.97693200003 -37.8171955999625, 144.97632120001 -37.8166556000173, 144.975836200011 -37.8162349999771, 144.975484099993 -37.8159246000112, 144.975254099958 -37.8156684999692, 144.975033899985 -37.8155869000169, 144.975002000052 -37.8152870999753, 144.974862500026 -37.8150237000143, 144.974744800009 -37.8087737999705, 144.97184869999 -37.8086363000065, 144.971716200048 -37.8085259000299, 144.971582900021 -37.8083964000279, 144.971393299951 -37.8082289999986, 144.971091199971 -37.808172399957, 144.970945099968 -37.8080764999841, 144.970606999995 -37.808025400012, 144.970256300053 -37.8080015000372, 144.969916199985 -37.8079953999606, 144.96956419998 -37.808034999982, 144.969245100018 -37.8080848999907, 144.969005199993 -37.8099386999852, 144.962593500014 -37.8103137999581, 144.961356399982 -37.8117517000372, 144.956443799957 -37.8133308999864, 144.950868300016 -37.8134615000047, 144.950603399969 -37.8135931000251, 144.950395299996 -37.8138054999956, 144.950173599965 -37.81398329996, 144.950032400054 -37.81418889995, 144.949935799977 -37.8145199999943, 144.949801699966 -37.8147624000128, 144.949749500022 -37.8149150999922, 144.949722600005 -37.8151223000106, 144.949716899987 -37.8153748999829, 144.949732599976 -37.8154472999846, 144.949753299957 -37.8156823000166, 144.949792299978 -37.8161265999766, 144.949950400014 -37.8163266000045, 144.950047100001 -37.8172413000239, 144.950828299953 -37.8174253999886, 144.951050400047 -37.8194719000155, 144.952652399974 -37.820146400037, 144.953088200015 -37.8203663000234, 144.953297999947 -37.820983800037, 144.954087499969 -37.8211138999842, 144.95431110001 -37.8212360999886, 144.954603099955 -37.8213497000187, 144.954918100029 -37.8214091000209, 144.955223199949 -37.8214777999948, 144.955550700002 -37.8214793999859, 144.955641600043 -37.8214930000136, 144.95590249995 -37.8214808999926, 144.956243599998 -37.821366399953, 144.956917099953 -37.8210132000064, 144.957858399986 -37.8199780000159, 144.960522499975 -37.8198747999727, 144.960809400014 -37.8197300000066, 144.961290500009 -37.8196731999909, 144.961655599964 -37.819358000036, 144.962709400051 -37.8190125000142, 144.964093500022 -37.8188720999518, 144.964835699955 -37.8183051000073, 144.966964299956" + // and convert it into a Vec<(f64, f64)> like + // [(-37.8201561999773, 144.98079590002)] + paths.push( + path.split(' ') + .chunks(2) + .into_iter() + .map(|mut chunk| { + let lat = chunk + .next() + .and_then(|x| x.split(',').next()) + .ok_or_else(|| serde::de::Error::missing_field("latitude"))? + .parse::() + .map_err(|e| { + serde::de::Error::custom(format!("could not parse f64: {}", e)) + })?; + let lon = chunk + .next() + .and_then(|x| x.split(',').next()) + .ok_or_else(|| serde::de::Error::missing_field("longitude"))? + .parse::() + .map_err(|e| { + serde::de::Error::custom(format!("could not parse f64: {}", e)) + })?; + Ok((lat, lon)) + }) + .collect::, A::Error>>()?, + ) + } + Ok(paths) + } + } + deserializer.deserialize_any(PathVisitor) +} + +pub fn ser_disruption_query( + disruption: &Option>, + serializer: S, +) -> Result +where + S: ser::Serializer, +{ + match disruption { + Some(disruption) => serializer.collect_seq(disruption.iter().map(|d| d.as_number())), + None => serializer.serialize_none(), + } +} + +pub fn de_string_as_i32<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = i32; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an integer") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse::().map_err(serde::de::Error::custom) + } + } + deserializer.deserialize_i32(Visitor) +} diff --git a/src/lib.rs b/src/lib.rs index fcec78a..ba37151 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,12 +28,10 @@ impl Client { { if !path.contains('?') { "?" + } else if path.ends_with('?') { + "" } else { - if path.ends_with('?') { - "" - } else { - "&" - } + "&" } }, self.devid @@ -51,7 +49,11 @@ impl Client { let res = reqwest::get(&url).await?; if !res.status().is_success() { - return Err(anyhow::anyhow!("Request failed: {}", res.status())); + let status = res.status(); + if let Ok(ApiError { message, .. }) = res.json().await { + return Err(anyhow::anyhow!("Request failed: {} - {}", status, message)); + } + return Err(anyhow::anyhow!("Request failed: {}", status)); } Ok(res.json().await?) @@ -63,7 +65,7 @@ impl Client { pub async fn departures_stop( &self, route_type: RouteType, - stop_id: i32, + stop_id: StopId, options: DeparturesStopOpts, ) -> Result { self.rq(format!( @@ -79,8 +81,8 @@ impl Client { pub async fn departures_stop_route( &self, route_type: RouteType, - route_id: i32, - stop_id: i32, + route_id: RouteId, + stop_id: StopId, options: DeparturesStopRouteOpts, ) -> Result { self.rq(format!( @@ -96,19 +98,19 @@ impl Client { /* > Directions */ /// View all routes for a direction of travel - pub async fn directions_id(&self, direction_id: i32) -> Result { + pub async fn directions_id(&self, direction_id: DirectionId) -> Result { self.rq(format!("v3/directions/{}", direction_id)).await } /// View directions that a route travels in - pub async fn directions_route(&self, route_id: i32) -> Result { + pub async fn directions_route(&self, route_id: RouteId) -> Result { self.rq(format!("v3/directions/route/{}", route_id)).await } /// View all routes of a particular type for a direction of travel pub async fn directions_id_route( &self, - direction_id: i32, + direction_id: DirectionId, route_type: RouteType, ) -> Result { self.rq(format!( @@ -129,7 +131,7 @@ impl Client { /// View all disruptions for a particular route pub async fn disruptions_route( &self, - route_id: i32, + route_id: RouteId, options: DisruptionsSpecificOpts, ) -> Result { self.rq(format!( @@ -143,8 +145,8 @@ impl Client { /// View all disruptions for a particular route and stop pub async fn disruptions_route_stop( &self, - route_id: i32, - stop_id: i32, + route_id: RouteId, + stop_id: StopId, options: DisruptionsSpecificOpts, ) -> Result { self.rq(format!( @@ -159,7 +161,7 @@ impl Client { /// View all disruptions for a particular stop pub async fn disruptions_stop( &self, - stop_id: i32, + stop_id: StopId, options: DisruptionsSpecificOpts, ) -> Result { self.rq(format!( @@ -171,7 +173,7 @@ impl Client { } /// View a specific disruption - pub async fn disruptions_id(&self, disruption_id: i32) -> Result { + pub async fn disruptions_id(&self, disruption_id: DisruptionId) -> Result { // TODO: Technically this has Status too but I dont want to // dupe the struct 17 times self.rq(format!("v3/disruptions/{}", disruption_id)).await @@ -223,7 +225,7 @@ impl Client { /// View the stopping pattern for a specific tip / service run pub async fn patterns_run_route( &self, - run_ref: String, + run_ref: &str, route_type: RouteType, options: PatternsRunRouteOpts, ) -> Result { @@ -244,7 +246,11 @@ impl Client { } // View route name and number for a specific route ID - pub async fn routes_id(&self, route_id: i32, options: RouteIdOpts) -> Result { + pub async fn routes_id( + &self, + route_id: RouteId, + options: RouteIdOpts, + ) -> Result { self.rq(format!("v3/routes/{}?{}", route_id, to_query(options))) .await } @@ -252,13 +258,13 @@ impl Client { /* > Runs */ /// View all trip/service runs for a specific run_ref - pub async fn runs_ref(&self, run_ref: String, options: RunsRefOpts) -> Result { + pub async fn runs_ref(&self, run_ref: &str, options: RunsRefOpts) -> Result { self.rq(format!("v3/runs/{}?{}", run_ref, to_query(options))) .await } /// View all trip/service runs for a specific route ID - pub async fn runs_id(&self, run_id: i32, options: RunsIdOpts) -> Result { + pub async fn runs_id(&self, run_id: RouteId, options: RunsIdOpts) -> Result { self.rq(format!("v3/runs/route/{}?{}", run_id, to_query(options))) .await } @@ -266,7 +272,7 @@ impl Client { /// View all trip/service runs for a specific run_ref and route type pub async fn runs_ref_type( &self, - run_ref: String, + run_ref: &str, route_type: RouteType, options: RunsRefOpts, ) -> Result { @@ -282,7 +288,7 @@ impl Client { /// View all trip/service runs for a specific run ID and route type pub async fn runs_id_type( &self, - run_id: i32, + run_id: RunId, route_type: RouteType, options: RunsIdOpts, ) -> Result { @@ -294,4 +300,28 @@ impl Client { )) .await } + // Search for stops, routes and myki outlets that match the input search term + pub async fn search(&self, search_term: &str, options: SearchOpts) -> Result { + self.rq(format!( + "v3/search/{}?{}", + url_escape::encode_path(&clean(search_term.to_owned())).to_owned(), + to_query(options) + )) + .await + } + // View facilities at a specific stop (Metro and VLine stations only) + pub async fn stops_id_route_type( + &self, + stop_id: StopId, + route_type: RouteType, + options: StopsIdRouteTypeOpts, + ) -> Result { + self.rq(format!( + "v3/stops/{}/route_type/{}?{}", + stop_id, + route_type, + to_query(options) + )) + .await + } } diff --git a/src/ty.rs b/src/ty.rs index f6ddeb1..0ae07dc 100644 --- a/src/ty.rs +++ b/src/ty.rs @@ -4,15 +4,19 @@ //! //! I appreciate any work done to fill in the TODO: T types. -use chrono::NaiveDateTime; +use chrono::{NaiveDate, NaiveDateTime}; +use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{collections::HashMap, fmt::Display, str::FromStr}; +use std::{collections::HashMap, str::FromStr}; use to_and_fro::{output_case, ToAndFro}; use crate::{ de_rfc3339, - helpers::{de_iso_8601, de_service_time, ser_iso_8601, ser_touch_utc}, + helpers::{ + de_iso_8601, de_service_time, de_string_as_i32, deserialize_path, ser_disruption_query, + ser_iso_8601, ser_touch_utc, + }, opt_de_rfc3339, }; @@ -29,105 +33,125 @@ impl<'de> Deserialize<'de> for I32ButSilly { } } -#[derive(Debug, Copy, Clone)] +macro_rules! newtype_i32 { + ($name:ident) => { + #[derive(Debug, Copy, Clone, Deserialize, Serialize, Display, PartialEq, Eq)] + #[serde(transparent)] + pub struct $name(pub i32); + }; + ($name:ident, $($extra:tt)*) => { + #[derive(Debug, Copy, Clone, Deserialize, Serialize, Display,PartialEq, Eq, $($extra)*)] + #[serde(transparent)] + pub struct $name(pub i32); + }; +} +newtype_i32!(DisruptionId); + +newtype_i32!(RunId); + +newtype_i32!(StopId); + +newtype_i32!(RouteId); + +newtype_i32!(DirectionId); + +/// Routepath (TODO) +#[derive(Debug, Deserialize)] +pub struct Geopath { + pub direction_id: DirectionId, + pub valid_from: NaiveDate, + pub valid_to: NaiveDate, + #[serde(deserialize_with = "deserialize_path")] + pub paths: Vec>, +} // TODO: T + +/// Types of routes +#[derive(Debug, Copy, Clone, Display, From)] #[repr(i8)] pub enum RouteType { + /// Metropolitan train service Train = 0, + /// Metropolitan tram service Tram = 1, + /// Bus Service Bus = 2, + /// V/Line regional train service VLine = 3, + /// Night Bus service NightBus = 4, + /// Other Route Type Other(i8), } -impl From for RouteType { - fn from(value: i8) -> Self { - match value { - 0 => RouteType::Train, - 1 => RouteType::Tram, - 2 => RouteType::Bus, - 3 => RouteType::VLine, - 4 => RouteType::NightBus, - _ => RouteType::Other(value), - } - } -} - -impl Into for RouteType { - fn into(self) -> i8 { - match self { - RouteType::Train => 0, - RouteType::Tram => 1, - RouteType::Bus => 2, - RouteType::VLine => 3, - RouteType::NightBus => 4, - RouteType::Other(value) => value, - } - } -} - impl Serialize for RouteType { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - serializer.serialize_i8((*self).into()) + i8::from(*self).serialize(serializer) } } +//imp deserialize impl<'de> Deserialize<'de> for RouteType { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - let value = i8::deserialize(deserializer)?; - Ok(value.into()) - } -} - -impl Display for RouteType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Into::::into(*self).fmt(f) - } -} - -#[derive(Debug, Copy, Clone)] -pub enum DisruptionModes {} - -impl From for DisruptionModes { - fn from(_value: i8) -> Self { - todo!(); + Ok(i8::deserialize(deserializer)?.into()) } } -impl Into for DisruptionModes { - fn into(self) -> i8 { - todo!(); - } -} - -impl Serialize for DisruptionModes { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_i8((*self).into()) +impl From for i8 { + fn from(value: RouteType) -> Self { + match value { + RouteType::Train => 0, + RouteType::Tram => 1, + RouteType::Bus => 2, + RouteType::VLine => 3, + RouteType::NightBus => 4, + RouteType::Other(x) => x, + } } } - -impl<'de> Deserialize<'de> for DisruptionModes { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = i8::deserialize(deserializer)?; - Ok(value.into()) - } +/// Modes of disruption +#[derive(Debug, Serialize, Deserialize, Clone, From, Copy)] +#[serde(tag = "disruption_mode_name", content = "disruption_mode")] +#[repr(i8)] +pub enum DisruptionModes { + #[serde(rename = "metro_train")] + MetroTrain = 1, // { + #[serde(rename = "metro_bus")] + MetroBus = 2, + #[serde(rename = "metro_tram")] + MetroTram = 3, + #[serde(rename = "regional_coach")] + RegionalCoach = 4, + #[serde(rename = "regional_train")] + RegionalTrain = 5, + #[serde(rename = "regional_bus")] + RegionalBus = 7, + #[serde(rename = "school_bus")] + SchoolBus = 8, + #[serde(rename = "telebus")] + Telebus = 9, + #[serde(rename = "night_bus")] + NightBus = 10, + #[serde(rename = "ferry")] + Ferry = 11, + #[serde(rename = "interstate_train")] + InterstateTrain = 12, + #[serde(rename = "skybus")] + Skybus = 13, + #[serde(rename = "taxi")] + Taxi = 14, + #[serde(rename = "general")] + General = 100, } impl DisruptionModes { pub fn as_number(&self) -> i8 { - Into::::into(*self) + *self as i8 } } @@ -150,7 +174,7 @@ pub struct DeparturesStopOpts { pub platform_numbers: Option>, /// Filter by identifier of direction of travel #[serde(skip_serializing_if = "Option::is_none")] - pub direction_id: Option, + pub direction_id: Option, /// Indicates that stop_id parameter will accept "GTFS stop_id" data #[serde(skip_serializing_if = "Option::is_none")] pub gtfs: Option, @@ -178,11 +202,17 @@ pub struct DeparturesStopOpts { pub include_geopath: Option, } +#[derive(Debug, Deserialize)] +pub struct ApiError { + pub message: String, + pub status: Status, +} + #[derive(Serialize, Default)] pub struct DeparturesStopRouteOpts { /// Filter by identifier of direction of travel #[serde(skip_serializing_if = "Option::is_none")] - pub direction_id: Option, + pub direction_id: Option, /// Indicates that stop_id parameter will accept "GTFS stop_id" data #[serde(skip_serializing_if = "Option::is_none")] pub gtfs: Option, @@ -210,11 +240,11 @@ pub struct DeparturesStopRouteOpts { pub include_geopath: Option, } -#[derive(Debug, ToAndFro)] +#[derive(Debug, ToAndFro, Serialize)] pub enum ExpandOptions { All, Stop, - Router, + Route, Run, Direction, Disruption, @@ -223,22 +253,13 @@ pub enum ExpandOptions { None, } -impl Serialize for ExpandOptions { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - impl<'de> Deserialize<'de> for ExpandOptions { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?; - Self::from_str(&value).map_err(|e| serde::de::Error::custom(e)) + Self::from_str(&value).map_err(serde::de::Error::custom) } } #[derive(Deserialize, Debug)] @@ -246,9 +267,9 @@ pub struct DeparturesResponse { /// Timetabled and real-time service departures pub departures: Vec, /// A train station, tram stop, bus stop, regional coach stop or Night Bus stop - pub stops: HashMap, + pub stops: HashMap, /// Train lines, tram routes, bus routes, regional coach routes, Night Bus routes - pub routes: HashMap, + pub routes: HashMap, /// Individual trips/services of a route pub runs: HashMap, /// Directions of travel of route @@ -259,20 +280,48 @@ pub struct DeparturesResponse { pub status: Status, } +#[derive(Deserialize, Debug)] +pub struct StoppingPatternsStop { + #[serde(flatten)] + pub stop: Stop, + pub stop_ticket: StopTicket, +} + +#[derive(Deserialize, Debug)] +pub struct Stop { + #[serde(rename = "stop_distance")] + pub distance: f32, + #[serde(rename = "stop_suburb")] + pub suburb: String, + #[serde(rename = "stop_name")] + pub name: String, + #[serde(rename = "stop_id")] + pub id: StopId, + pub route_type: RouteType, + #[serde(rename = "stop_latitude")] + pub latitude: f64, + #[serde(rename = "stop_longitude")] + pub longitude: f64, + #[serde(rename = "stop_landmark")] + pub landmark: String, + #[serde(rename = "stop_sequence")] + pub sequence: i32, +} + #[derive(Deserialize, Debug)] pub struct Departure { /// Stop identifier - pub stop_id: i32, + pub stop_id: StopId, /// Route identifier - pub route_id: i32, + pub route_id: RouteId, /// Numeric trip/service run identifier. Defaults to -1 when run identifier is Alphanumeric - pub run_id: i32, + pub run_id: RunId, /// Alphanumeric trip/service run identifier pub run_ref: String, /// Direction of travel identifier - pub direction_id: i32, + pub direction_id: DirectionId, /// Disruption information identifier(s) - pub disruption_ids: Vec, + pub disruption_ids: Vec, /// Scheduled (i.e. timetabled) departure time and date #[serde(deserialize_with = "opt_de_rfc3339")] #[serde(rename = "scheduled_departure_utc")] @@ -291,54 +340,49 @@ pub struct Departure { pub flags: String, /// Chronological sequence for the departures in a run. pub departure_sequence: i32, + + pub skipped_stops: Option>, } -/// TODO: Should we rename fields here? #[derive(Deserialize, Debug)] -pub struct DepartureStop { - #[serde(rename = "stop_distance")] - pub distance: f32, - #[serde(rename = "stop_suburb")] - pub suburb: String, - #[serde(rename = "stop_name")] - pub name: String, - #[serde(rename = "stop_id")] - pub id: i32, - pub route_type: RouteType, - #[serde(rename = "stop_latitude")] - pub latitude: f64, - #[serde(rename = "stop_longitude")] - pub longitude: f64, - /// Seems to sometimes be empty - #[serde(rename = "stop_landmark")] - pub landmark: String, - #[serde(rename = "stop_sequence")] - pub sequence: i32, +pub struct StopTicket { + pub ticket_type: String, + pub zone: String, + pub is_free_fare_zone: bool, + pub ticket_machine: bool, + pub ticket_checks: bool, + pub vline_reservation: bool, + pub ticket_zones: Vec, } #[derive(Deserialize, Debug)] -pub struct DepartureRoute { - #[serde(rename = "route_type")] +pub struct Route { pub route_type: RouteType, #[serde(rename = "route_id")] - pub id: i32, + pub id: RouteId, #[serde(rename = "route_name")] pub name: String, #[serde(rename = "route_number")] pub number: String, #[serde(rename = "route_gtfs_id")] pub gtfs_id: String, +} + +#[derive(Deserialize, Debug)] +pub struct RouteWithGeoPath { + #[serde(flatten)] + pub route: Route, /// TODO: T - pub geopath: Vec, + pub geopath: Option>, } #[derive(Deserialize, Debug)] pub struct Direction { #[serde(rename = "direction_id")] - pub id: i32, + pub id: DirectionId, #[serde(rename = "direction_name")] pub name: String, - pub route_id: i32, + pub route_id: RouteId, pub route_type: RouteType, } @@ -354,19 +398,12 @@ pub struct DirectionsResponse { #[derive(Deserialize, Debug)] pub struct DirectionWithDescription { + // Direction of travel identifier + #[serde(flatten)] + pub direction: Direction, /// Description #[serde(rename = "route_direction_description")] pub description: String, - /// Direction of travel identifier - #[serde(rename = "direction_id")] - pub id: i32, - /// Name of direction of travel - #[serde(rename = "direction_name")] - pub name: String, - /// Route identifier - pub route_id: i32, - /// Transport mode identifier - pub route_type: RouteType, } // @@ -379,6 +416,7 @@ pub struct DisruptionsOpts { /// Filter by disruption_modes #[serde(rename = "disruption_modes")] #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "ser_disruption_query")] pub modes: Option>, /// Filter by status of disruption #[serde(rename = "disruption_status")] @@ -416,7 +454,7 @@ impl<'de> Deserialize<'de> for DisruptionStatus { D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?; - Self::from_str(&value).map_err(|e| serde::de::Error::custom(e)) + Self::from_str(&value).map_err(serde::de::Error::custom) } } @@ -463,7 +501,7 @@ pub struct Disruptions { #[derive(Deserialize, Debug)] pub struct Disruption { /// Disruption information identifier - pub disruption_id: i32, + pub disruption_id: DisruptionId, /// Headline title summarising disruption information pub title: String, /// URL of relevant article on PTV website @@ -498,23 +536,15 @@ pub struct Disruption { #[derive(Deserialize, Debug)] pub struct DisruptionStop { #[serde(rename = "stop_id")] - pub id: i32, + pub id: StopId, #[serde(rename = "stop_name")] pub name: String, } #[derive(Deserialize, Debug)] pub struct DisruptionRoute { - /// Transport mode identifier - pub route_type: RouteType, - /// Route identifier - pub route_id: i32, - /// Name of route - pub route_name: String, - /// Route number presented to public (i.e not route_id) - pub route_number: String, - /// Route GTFS identifier - pub route_gtfs_id: String, + #[serde(flatten)] + pub route: Route, /// Direction of travel relevant to disruption pub direction: Option, } @@ -526,7 +556,7 @@ pub struct DisruptionDirection { pub combination_id: i32, /// Direction of travel identifier #[serde(rename = "direction_id")] - pub id: i32, + pub id: DirectionId, /// Name of direction of travel #[serde(rename = "direction_name")] pub name: String, @@ -559,11 +589,53 @@ pub struct FareEstimateOpts { } #[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] pub struct FareEstimateResponse { - // TODO: This is undefined on the API documentation + pub fare_estimate_result: FareEstimate, + // API Status / Metadata + pub fare_estimate_status: Status, } -// +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ZoneInfo { + pub min_zone: i32, + pub max_zone: i32, + pub unique_zones: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub enum PassengerType { + Senior, + Concession, + FullFare, +} +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct PassengerFare { + pub passenger_type: PassengerType, + pub fare2_hour_off_peak: f32, + pub fare2_hour_peak: f32, + pub fare_daily_peak: f32, + pub fare_daily_off_peak: f32, + pub pass7_days: f32, + pub pass28_to69_day_per_day: f32, + pub pass70_plus_day_per_day: f32, + pub weekend_cap: f32, + pub holiday_cap: f32, +} + +// TODO: This is undefined on the API documentation +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct FareEstimate { + pub is_early_bird: bool, + pub is_journey_in_free_tram_zone: Option, + pub is_this_weekend_journey: Option, + pub zone_info: ZoneInfo, + pub passenger_fares: Vec, +} #[derive(Serialize, Default)] pub struct OutletsOpts { @@ -598,7 +670,7 @@ pub struct OutletsResponse { pub struct Outlet { /// The SLID / SPID #[serde(rename = "outlet_slid_spid")] - pub id: i32, + pub id: String, /// The location name of the outlet #[serde(rename = "outlet_name")] pub name: String, @@ -619,29 +691,29 @@ pub struct Outlet { pub postcode: usize, /// The business hours on Monday #[serde(rename = "outlet_business_hour_mon")] - pub hours_monday: String, + pub hours_monday: Option, /// The business hours on Tuesday #[serde(rename = "outlet_business_hour_tue")] - pub hours_tuesday: String, + pub hours_tuesday: Option, /// The business hours on Wednesday #[serde(rename = "outlet_business_hour_wed")] - pub hours_wednesday: String, + pub hours_wednesday: Option, /// The business hours on Thursday #[serde(rename = "outlet_business_hour_thu")] - pub hours_thursday: String, + pub hours_thursday: Option, /// The business hours on Friday #[serde(rename = "outlet_business_hour_fri")] - pub hours_friday: String, + pub hours_friday: Option, /// The business hours on Saturday #[serde(rename = "outlet_business_hour_sat")] - pub hours_saturday: String, + pub hours_saturday: Option, /// The business hours on Sunday #[serde(rename = "outlet_business_hour_sun")] - pub hours_sunday: String, + pub hours_sunday: Option, /// Any additional notes for the outlet such as /// 'Buy pre-loaded myki cards only' - #[serde(rename = "outlet_note")] - pub note: String, + #[serde(rename = "outlet_notes")] + pub note: Option, } // @@ -653,7 +725,7 @@ pub struct PatternsRunRouteOpts { pub expand: Option>, /// Filter by stop_id #[serde(skip_serializing_if = "Option::is_none")] - pub stop_id: Option, + pub stop_id: Option, /// Filter by the date and time of the request #[serde(serialize_with = "ser_iso_8601")] #[serde(rename = "date_utc")] @@ -661,7 +733,7 @@ pub struct PatternsRunRouteOpts { pub date: Option, /// Include any skipped stops in a stopping pattern /// (default = false) - #[serde(rename = "include_skipped")] + #[serde(rename = "include_skipped_stops")] #[serde(skip_serializing_if = "Option::is_none")] pub include_skipped: Option, /// Incidates if the route geopath should be returned @@ -677,13 +749,13 @@ pub struct PatternResponse { /// Timetabled and real-time service departures pub departures: Vec, /// A train station, tram stop, bus stop, regional coach stop or Night Bus stop - pub stops: Value, // TODO: T + pub stops: HashMap, /// Train lines, tram routes, bus routes, regional coach routes, Night Bus routes - pub routes: Value, // TODO: T + pub routes: HashMap, // TODO needs to be more specific /// Individual trips/services of a route - pub runs: Value, // TODO: T + pub runs: HashMap, /// Directions of travel of route - pub directions: Value, // TODO: T + pub directions: HashMap, /// API Status / Metadata pub status: Status, } @@ -733,18 +805,8 @@ pub struct RouteWithStatus { /// Service status for the route (indicates disruptions) #[serde(rename = "route_service_status")] pub service_status: RouteServiceStatus, - /// Transport mode identifier - pub route_type: RouteType, - /// Route identifier - pub route_id: i32, - /// Name of route - pub route_name: String, - /// Route number presented to public (i.e not route_id) - pub route_number: String, - /// Route GTFS identifier - pub route_gtfs_id: String, - /// Geopath of the route - pub geopath: Value, // TODO: T + #[serde(flatten)] + pub route: RouteWithGeoPath, } #[derive(Deserialize, Debug)] @@ -792,21 +854,21 @@ pub struct RunsResponse { pub struct Run { /// Numeric trip/service run identifier. /// Defaults to -1 when run identifier is Alphanumeric - pub run_id: i32, + pub run_id: RunId, /// Alphanumeric trip/service run identifier pub run_ref: String, /// Route identifier - pub route_id: i32, + pub route_id: RouteId, /// Transport mode identifier pub route_type: RouteType, /// stop_id of final stop of run - pub final_stop_id: i32, + pub final_stop_id: StopId, /// Name of destination of run pub destination_name: String, /// Status of metropolitan train run; returns "scheduled" for other modes pub status: String, /// Direction of travel identifier - pub direction_id: i32, + pub direction_id: DirectionId, /// Chronological sequence of the trip/service run on the route in direction /// Order ascendingly by this field to get chronological order (earliest first) of runs with the same route_id and direction_id /// @@ -819,7 +881,7 @@ pub struct Run { // Descriptor of the trip/service run. Only available for some runs. pub vehicle_descriptor: Option, /// Geopath of the route - pub geopath: Value, // TODO: T + pub geopath: Vec, } #[derive(Deserialize, Debug)] @@ -847,11 +909,22 @@ pub struct VehiclePosition { pub expiry_time: Option, // TODO: Add a deser. No information in docs. } +#[derive(Deserialize, Debug)] +pub enum ServiceOperator { + #[serde(rename = "Metro Trains Melbourne")] + MetroTrainsMelbourne, + #[serde(rename = "Yarra Trams")] + YarraTrams, + #[serde(rename = "Ventura Bus Line")] + VenturaBusLine, + Other(String), +} + #[derive(Deserialize, Debug)] pub struct VehicleDescriptor { /// Operator name of the vehicle such as "Metro Trains Melbourne", "Yarra Trams", "Ventura Bus Line", etc. /// Only available for some runs. - pub operator: Option, + pub operator: Option, /// Operator identifier of the vehicle. Only available for some runs. pub id: Option, /// Indicator if the vehicle has a low floor. Only available for some tram runs. @@ -866,3 +939,179 @@ pub struct VehicleDescriptor { /// Meters? Feet? Who knows. pub length: Option, } + +#[derive(Serialize, Default)] +pub struct SearchOpts { + #[serde(skip_serializing_if = "Option::is_none")] + pub route_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub latitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub longitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_distance: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_addresses: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_outlets: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub match_stop_by_suburb: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub match_stop_by_gtfs_stop_id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct ResultStop { + #[serde(flatten)] + pub stop: Stop, + pub routes: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct SearchResponse { + pub stops: Vec, + pub routes: Vec, + pub outlets: Vec, + pub status: Status, +} + +#[derive(Serialize, Default)] +pub struct StopsIdRouteTypeOpts { + /// Indicates if stop locaiton information should be returned + #[serde(rename = "stop_location")] + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + /// Indicates if stop amenities information should be returned + #[serde(rename = "stop_amenities")] + #[serde(skip_serializing_if = "Option::is_none")] + pub amenities: Option, + #[serde(rename = "stop_accessibility")] + #[serde(skip_serializing_if = "Option::is_none")] + pub accessibility: Option, + #[serde(rename = "stop_contact")] + #[serde(skip_serializing_if = "Option::is_none")] + pub contact: Option, + #[serde(rename = "stop_ticket")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ticket: Option, + #[serde(rename = "stop_staffing")] + #[serde(skip_serializing_if = "Option::is_none")] + pub staffing: Option, + #[serde(rename = "stop_disruptions")] + #[serde(skip_serializing_if = "Option::is_none")] + pub disruptions: Option, +} + +#[derive(Deserialize, Debug)] +pub struct StopGps { + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Deserialize, Debug)] +pub struct StopLocation { + pub gps: StopGps, +} + +#[derive(Deserialize, Debug)] +pub struct StopAmenityDetails { + pub toilet: bool, + pub taxi_rank: bool, + pub car_parking: bool, + pub cctv: bool, +} + +#[derive(Deserialize, Debug)] +pub struct StopAccessibilityWheelchair { + pub accessible_ramp: bool, + pub parking: bool, + pub telephone: bool, + pub toilet: bool, + pub low_ticket_counter: bool, + pub manouvering: bool, + pub raised_platform: bool, + pub ramp: bool, + pub secondary_path: bool, + pub raised_platform_shelter: bool, + pub steep_ramp: bool, +} + +#[derive(Deserialize, Debug)] +pub struct StopAccessibility { + pub lighting: bool, + pub platform_number: bool, + pub escalator: bool, + pub lift: bool, + pub stairs: bool, + pub stop_accessibility: bool, + pub tactile_ground_surface_indicator: bool, + pub waiting_room: bool, + pub wheelchair: StopAccessibilityWheelchair, +} + +#[derive(Deserialize, Debug)] +pub struct StopStaffing { + pub fri_am_from: String, + pub fri_am_to: String, + pub fri_pm_from: String, + pub fri_pm_to: String, + pub mon_am_from: String, + pub mon_am_to: String, + pub mon_pm_from: String, + pub mon_pm_to: String, + pub ph_additional_text: String, + pub ph_from: String, + pub ph_to: String, + pub sat_am_from: String, + pub sat_am_to: String, + pub sat_pm_from: String, + pub sat_pm_to: String, + pub sun_am_from: String, + pub sun_am_to: String, + pub sun_pm_from: String, + pub sun_pm_to: String, + pub thu_am_from: String, + pub thu_am_to: String, + pub thu_pm_from: String, + pub thu_pm_to: String, + pub tue_am_from: String, + pub tue_am_to: String, + pub tue_pm_from: String, + pub tue_pm_to: String, + pub wed_am_from: String, + pub wed_am_to: String, + pub wed_pm_from: String, + #[allow(non_snake_case)] + pub wed_pm_To: String, +} + +#[derive(Debug, Deserialize)] +pub struct StopDetails { + pub disruption_ids: Vec, + #[serde(rename = "stop_id")] + pub id: StopId, + pub station_type: String, + pub station_description: String, + pub route_type: RouteType, + // assumption, because it just says object + pub routes: Vec, + #[serde(rename = "stop_landmark")] + pub landmark: String, + #[serde(rename = "stop_name")] + pub name: String, + #[serde(rename = "stop_amenities")] + pub amenities: Option, + #[serde(rename = "stop_accessibility")] + pub accessibility: Option, + #[serde(rename = "stop_staffing")] + pub staffing: Option, + #[serde(rename = "stop_location")] + pub location: Option, +} + +#[derive(Debug, Deserialize)] +pub struct StopResponse { + pub stop: StopDetails, + pub disruptions: Vec, + pub status: Status, +} diff --git a/tests/main.rs b/tests/main.rs index a64fc62..62ee6b9 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,41 +1,36 @@ #[allow(dead_code)] #[cfg(test)] pub mod test { - use std::{collections::{BTreeMap, HashMap}, future::Future, pin::Pin, sync::Arc}; + use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc}; use colored::Colorize; use dotenv::dotenv; + use futures::{stream::FuturesUnordered, StreamExt}; + use itertools::Itertools; use once_cell::sync::Lazy; use ptv::*; + use ptvrs_macros::make_test; + use tokio::io::AsyncWriteExt; - macro_rules! make_test { - ($m:expr, $name:literal, {$e:expr}) => { - $m.insert($name, Arc::new(|| { - Box::pin(async { - let res = $e.await?; - Ok(format!("{:?}", res)) - }) - })); - }; - } + static DEVID: &str = "DEVID"; + static KEY: &str = "KEY"; static CLIENT: Lazy = Lazy::new(|| { // Load .env file if DEVID and KEY are not set - if std::env::var("DEVID").is_err() || std::env::var("KEY").is_err() { + if let (Ok(devid), Ok(key)) = (std::env::var(DEVID), std::env::var(KEY)) { + Client::new(devid, key) + } else { dotenv().ok(); + Client::new(std::env::var(DEVID).unwrap(), std::env::var(KEY).unwrap()) } - - Client::new( - std::env::var("DEVID").unwrap(), - std::env::var("KEY").unwrap(), - ) }); // TODO: Find sensible constants static ROUTE_TYPE: RouteType = RouteType::Train; // Train - static ROUTE_ID: i32 = 1; // Alamein (Line) - static STOP_ID: i32 = 1002; // Alamein (Station) - static DIRECTION_ID: i32 = 1; // Towards Flinders Street + static ROUTE_ID: RouteId = RouteId(1); // Alamein (Line) + static STOP_ID: StopId = StopId(1002); // Alamein (Station) + static DIRECTION_ID: DirectionId = DirectionId(1); // Towards Flinders Street + static RUN_REF: &str = "1"; // Alamein something type Task = Arc Pin>>> + Send + Sync>; @@ -43,99 +38,144 @@ pub mod test { let mut map = BTreeMap::<&str, Task>::new(); // > Departures - make_test!(map, "departures_stop", { - CLIENT.departures_stop(ROUTE_TYPE, STOP_ID, DeparturesStopOpts::default()) - }); - - make_test!(map, "departures_stop_route", { - CLIENT.departures_stop_route( - ROUTE_TYPE, - ROUTE_ID, - STOP_ID, - DeparturesStopRouteOpts::default() - ) - }); + make_test!( + map, + departures_stop, + DeparturesStopOpts => [gtfs,include_cancelled], + ROUTE_TYPE, + STOP_ID + ); + + make_test!( + map, + departures_stop_route, + DeparturesStopRouteOpts => [[max_results: 10, look_backwards: true],gtfs,include_cancelled], + ROUTE_TYPE, + ROUTE_ID, + STOP_ID + ); + // > Routes + make_test!( + map, + routes, + RouteOpts => [route_types: vec![RouteType::Train]] + ); + + make_test!(map, routes_id, RouteIdOpts => [include_geopath], ROUTE_ID); + + // > Patterns + make_test!(map, patterns_run_route, PatternsRunRouteOpts => [stop_id: STOP_ID, expand: vec![ExpandOptions::All], include_skipped, include_geopath], RUN_REF, ROUTE_TYPE); // > Directions - make_test!(map, "directions_id", { - CLIENT.directions_id(DIRECTION_ID) - }); + make_test!(map, directions_id, DIRECTION_ID); - make_test!(map, "directions_route", { - CLIENT.directions_route(ROUTE_ID) - }); + make_test!(map, directions_route, ROUTE_ID); - make_test!(map, "directions_id_route", { - CLIENT.directions_id_route(DIRECTION_ID, ROUTE_TYPE) - }); + make_test!(map, directions_id_route, DIRECTION_ID, ROUTE_TYPE); // > Disruptions - make_test!(map, "disruptions", { - CLIENT.disruptions(DisruptionsOpts::default()) - }); - - make_test!(map, "disruptions_route", { - CLIENT.disruptions_route(ROUTE_ID, DisruptionsSpecificOpts::default()) - }); - - make_test!(map, "disruptions_route_stop", { - CLIENT.disruptions_route_stop(ROUTE_ID, STOP_ID, DisruptionsSpecificOpts::default()) - }); - - make_test!(map, "disruptions_stop", { - CLIENT.disruptions_stop(STOP_ID, DisruptionsSpecificOpts::default()) - }); + make_test!(map, disruptions, DisruptionsOpts => [modes: vec![DisruptionModes::MetroTrain], modes: vec![DisruptionModes::MetroBus]]); + + make_test!( + map, + disruptions_route, + DisruptionsSpecificOpts => [ + status: DisruptionStatus::Current, + status: DisruptionStatus::Planned + ], + ROUTE_ID + ); + + make_test!( + map, + disruptions_route_stop, + DisruptionsSpecificOpts => [ + status: DisruptionStatus::Current, + status: DisruptionStatus::Planned + ], + ROUTE_ID, + STOP_ID + ); + + make_test!( + map, + disruptions_stop, + DisruptionsSpecificOpts => [ + status: DisruptionStatus::Current, + status: DisruptionStatus::Planned + ], + STOP_ID + ); + + // > Search + make_test!(map, search, SearchOpts => [include_outlets, include_addresses],"Flinders Street Station"); map }); // - #[tokio::test] - pub async fn test() { - let mut failed = 0; - for (i, (name, task)) in TASKS.iter().enumerate() { - println!("[{}] Running test: {}", "~".cyan(), name.yellow()); - let start = std::time::Instant::now(); - let res = task().await; - let elapsed = start.elapsed(); - match res { - Ok(res) => println!( - "[{}] {} {} in {:?}:{}", - "+".green(), - name.yellow(), - "passed".green(), - elapsed, - { - if std::env::var("QUIET").is_err() { - format!("\n{}", res.cyan()) - } else { - " ...".cyan().to_string() + #[test] + pub fn test() { + let failed = Arc::new(tokio::sync::Mutex::new(0usize)); + + tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .max_blocking_threads(4) + .enable_all() + .build() + .unwrap() + .block_on(async { + let mut tasks = TASKS + .iter() + .map(|(name, task)| { + let task = Arc::clone(&task); + let failed = Arc::clone(&failed); + async move { + println!("[{}] Running test: {}", "~".cyan(), name.yellow()); + let start = std::time::Instant::now(); + let res = task().await; + let elapsed = start.elapsed(); + match res { + Ok(res) => println!( + "[{}] {} {} in {:?}:{}", + "+".green(), + name.yellow(), + "passed".green(), + elapsed, + { + if std::env::var("QUIET").is_err() { + format!("\n{}", res.cyan()) + } else { + " ...".cyan().to_string() + } + } + ), + Err(e) => { + { + let mut failed = failed.lock().await; + *failed += 1; + } + println!( + "[{}] {} {} in {:?}:\n{}", + "-".red(), + name.yellow(), + "failed".red(), + elapsed, + e.to_string().cyan() + ) + } + } } - } - ), - Err(e) => { - failed += 1; - println!( - "[{}] {} {} in {:?}:\n{}", - "-".red(), - name.yellow(), - "failed".red(), - elapsed, - e.to_string().cyan() - ) - } - } - - if i < TASKS.len() - 1 { - println!("\n[{}] Waiting 5 seconds to avoid limiting.\n", "~".cyan()); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - } - } + }) + .collect::>(); + while let Some(_) = tasks.next().await {} + }); - if failed > 0 { + let failed = failed.blocking_lock(); + if *failed > 0 { panic!("{} tests failed", failed); }