diff --git a/Cargo.lock b/Cargo.lock index a3e1954..5416445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[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 = "anyhow" version = "1.0.86" @@ -208,6 +223,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.5", +] + [[package]] name = "clru" version = "0.6.2" @@ -355,6 +383,41 @@ dependencies = [ "serde", ] +[[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.66", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.66", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1384,7 +1447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f39216c1843182f78541276fec96f88406861f16aa19cc9f8add70f8e67b7577" dependencies = [ "codemap", - "indexmap", + "indexmap 2.2.6", "lasso", "once_cell", "phf", @@ -1402,7 +1465,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1421,13 +1484,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.13.2" @@ -1636,6 +1705,35 @@ dependencies = [ "tracing", ] +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1646,6 +1744,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1873,6 +1982,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -2567,6 +2685,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -2609,13 +2757,14 @@ dependencies = [ "crates-index", "derive_more", "dotenvy", + "either", "error_reporter", "font-awesome-as-a-crate", "futures-util", "gix", "grass", "hyper 0.14.28", - "indexmap", + "indexmap 2.2.6", "lru_time_cache", "maud", "once_cell", @@ -2628,11 +2777,13 @@ dependencies = [ "semver", "serde", "serde_urlencoded", + "serde_with", "sha-1", "tokio", "toml 0.8.13", "tracing", "tracing-subscriber", + "unicode-ellipsis", ] [[package]] @@ -2687,6 +2838,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2967,7 +3124,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2980,7 +3137,7 @@ version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -3133,6 +3290,16 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" +[[package]] +name = "unicode-ellipsis" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ab5b8c3fed9966b8cde2dc7169146331cba3dacba97cbd0e8866e7cfd4dff" +dependencies = [ + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3148,6 +3315,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.12" @@ -3331,6 +3504,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[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.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 607ec65..63f216a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ cadence = "1" crates-index = { version = "2", default-features = false, features = ["git"] } derive_more = "0.99" dotenvy = "0.15" +either = "1.12.0" font-awesome-as-a-crate = "0.3" futures-util = { version = "0.3", default-features = false, features = ["std"] } hyper = { version = "0.14.10", features = ["full"] } @@ -36,10 +37,12 @@ rustsec = "0.29" semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_urlencoded = "0.7" +serde_with = "3.8.1" tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros", "sync", "time"] } toml = "0.8" tracing = "0.1.30" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +unicode-ellipsis = "0.2.0" [target.'cfg(any())'.dependencies] gix = { version = "0.63", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls"] } diff --git a/README.md b/README.md index e332ba4..bb038cf 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,15 @@ To analyze the state of your dependencies you can use the following URLs: On the analysis page, you will also find the markdown code to include a fancy badge in your project README so visitors (and you) can see at a glance if your dependencies are still up to date! -Badges have a few style options, specified with query parameters, that match the styles from `shields.io`: -- `?style=flat` (default) -- `?style=flat-square` -- `?style=for-the-badge` +Badges have a few options, specified with query parameters: +- `style`: which matches the styles from `shields.io`: + - `?style=flat` (default) + - `?style=flat-square` + - `?style=for-the-badge` +- `subject`: customize the text on the left (which is the same concept as `label` in `shields.io`, and [URL-Encoding](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) is needed for spaces or special characters!). e.g.: + - `?subject=yourdeps` + - `?subject=git%20deps` + - `?subject=deps%3Acore` ## Contributing diff --git a/src/server/mod.rs b/src/server/mod.rs index 3355066..a6651c2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -10,6 +10,7 @@ use once_cell::sync::Lazy; use route_recognizer::{Params, Router}; use semver::VersionReq; use serde::Deserialize; +use unicode_ellipsis::truncate_str; mod assets; mod views; @@ -24,6 +25,7 @@ use crate::{ repo::RepoPath, SubjectPath, }, + utils::common::{UntaggedEither, WrappedBool}, }; #[derive(Debug, Clone, Copy, PartialEq)] @@ -430,26 +432,52 @@ pub struct ExtraConfig { style: BadgeStyle, /// Whether the inscription _"dependencies"_ should be abbreviated as _"deps"_ in the badge. compact: bool, + /// Custom text on the left (it's the same concept as `label` in shields.io). + subject: Option, /// Path in which the crate resides within the repository path: Option, } impl ExtraConfig { fn from_query_string(qs: Option<&str>) -> Self { + /// This wrapper can make the deserialization process infallible. + #[derive(Debug, Clone, Deserialize)] + #[serde(transparent)] + struct QueryParam(UntaggedEither); + + impl QueryParam { + fn opt(self) -> Option { + self.0.into_either().left() + } + } + #[derive(Debug, Clone, Default, Deserialize)] struct ExtraConfigPartial { - style: Option, - compact: Option, + style: Option>, + compact: Option>, + subject: Option, path: Option, } + const MAX_WIDTH: usize = 100; let extra_config = qs .and_then(|qs| serde_urlencoded::from_str::(qs).ok()) .unwrap_or_default(); Self { - style: extra_config.style.unwrap_or_default(), - compact: extra_config.compact.unwrap_or_default(), + style: extra_config + .style + .and_then(|qp| qp.opt()) + .unwrap_or_default(), + compact: extra_config + .compact + .and_then(|qp| qp.opt()) + .unwrap_or_default() + .0, + subject: extra_config + .subject + .filter(|t| !t.is_empty()) + .map(|t| truncate_str(&t, MAX_WIDTH).into()), path: extra_config.path, } } diff --git a/src/server/views/badge.rs b/src/server/views/badge.rs index beba072..f97f829 100644 --- a/src/server/views/badge.rs +++ b/src/server/views/badge.rs @@ -7,12 +7,13 @@ pub fn badge( analysis_outcome: Option<&AnalyzeDependenciesOutcome>, badge_knobs: ExtraConfig, ) -> Badge { - let subject = if badge_knobs.compact { - "deps" + let subject = if let Some(subject) = badge_knobs.subject { + subject + } else if badge_knobs.compact { + "deps".into() } else { - "dependencies" - } - .to_owned(); + "dependencies".into() + }; let opts = match analysis_outcome { Some(outcome) => { diff --git a/src/utils/common.rs b/src/utils/common.rs new file mode 100644 index 0000000..efd2c6f --- /dev/null +++ b/src/utils/common.rs @@ -0,0 +1,85 @@ +use either::Either; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{ + fmt::{self, Debug, Display, Formatter}, + str::FromStr, +}; + +/// An `untagged` version of `Either`. +/// +/// The reason this structure is needed is that `either::Either` is +/// by default an `Externally Tagged` enum, and it is possible to +/// implement `untagged` via `#[serde(with = "either::serde_untagged_optional")]` +/// as well. But this approach can cause problems with deserialization, +/// resulting in having to manually add the `#[serde(default)]` tag, +/// and this leads to less readable as well as less flexible code. +/// So it would be better if we manually implement this `UntaggedEither` here, +/// while providing a two-way conversion to `either::Either`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum UntaggedEither { + Left(L), + Right(R), +} + +impl From> for Either { + fn from(value: UntaggedEither) -> Self { + match value { + UntaggedEither::Left(l) => Self::Left(l), + UntaggedEither::Right(r) => Self::Right(r), + } + } +} + +impl From> for UntaggedEither { + fn from(value: Either) -> Self { + match value { + Either::Left(l) => UntaggedEither::Left(l), + Either::Right(r) => UntaggedEither::Right(r), + } + } +} + +impl UntaggedEither { + pub fn into_either(self) -> Either { + self.into() + } +} + +/// A generic newtype which serialized using `Display` and deserialized using `FromStr`. +#[derive(Default, Clone, DeserializeFromStr, SerializeDisplay)] +pub struct SerdeDisplayFromStr(pub T); + +impl From for SerdeDisplayFromStr { + fn from(value: T) -> Self { + Self(value) + } +} + +impl Debug for SerdeDisplayFromStr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Display for SerdeDisplayFromStr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl FromStr for SerdeDisplayFromStr { + type Err = T::Err; + + fn from_str(s: &str) -> Result { + Ok(s.parse::()?.into()) + } +} + +/// The reason it's needed here is that using `Deserialize` generated +/// by default by `serde` will cause deserialization to fail if +/// both untyped formats (such as `urlencoded`) and `untagged enum` +/// are used. The Wrap type here forces the deserialization process to +/// be delegated to `FromStr`. +pub type WrappedBool = SerdeDisplayFromStr; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bdf34de..dac05c0 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod cache; +pub mod common; pub mod index;