Skip to content

Commit

Permalink
refactor: remove unicode dep and simplify UntaggedEither impl
Browse files Browse the repository at this point in the history
  • Loading branch information
robjtede committed Aug 7, 2024
1 parent 62891bb commit 5a215eb
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 48 deletions.
17 changes: 0 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cadence = "1"
crates-index = { version = "2", default-features = false, features = ["git"] }
derive_more = "0.99"
dotenvy = "0.15"
either = "1.12.0"
either = "1.12"
font-awesome-as-a-crate = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
hyper = { version = "0.14.10", features = ["full"] }
Expand All @@ -34,15 +34,14 @@ relative-path = { version = "1", features = ["serde"] }
reqwest = { version = "0.12", features = ["json"] }
route-recognizer = "0.3"
rustsec = "0.29"
semver = { version = "1.0", features = ["serde"] }
semver = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
serde_with = "3.8.1"
serde_with = "3"
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"] }
Expand Down
26 changes: 21 additions & 5 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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;
Expand All @@ -25,9 +24,11 @@ use crate::{
repo::RepoPath,
SubjectPath,
},
utils::common::{UntaggedEither, WrappedBool},
utils::common::{safe_truncate, UntaggedEither, WrappedBool},
};

const MAX_SUBJECT_WIDTH: usize = 100;

#[derive(Debug, Clone, Copy, PartialEq)]
enum StatusFormat {
Html,
Expand Down Expand Up @@ -430,10 +431,13 @@ static SELF_BASE_URL: Lazy<String> =
pub struct ExtraConfig {
/// Badge style to show
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<String>,

/// Path in which the crate resides within the repository
path: Option<String>,
}
Expand All @@ -447,7 +451,7 @@ impl ExtraConfig {

impl<T> QueryParam<T> {
fn opt(self) -> Option<T> {
self.0.into_either().left()
either::Either::from(self.0).left()
}
}

Expand All @@ -459,7 +463,6 @@ impl ExtraConfig {
path: Option<String>,
}

const MAX_WIDTH: usize = 100;
let extra_config = qs
.and_then(|qs| serde_urlencoded::from_str::<ExtraConfigPartial>(qs).ok())
.unwrap_or_default();
Expand All @@ -477,8 +480,21 @@ impl ExtraConfig {
subject: extra_config
.subject
.filter(|t| !t.is_empty())
.map(|t| truncate_str(&t, MAX_WIDTH).into()),
.map(|subject| safe_truncate(&subject, MAX_SUBJECT_WIDTH).to_owned()),
path: extra_config.path,
}
}

/// Returns subject for badge.
///
/// Returns `subject` if set, or "dependencies" / "deps" depending on value of `compact`.
pub(crate) fn subject(&self) -> &str {
if let Some(subject) = &self.subject {
subject
} else if self.compact {
"deps"
} else {
"dependencies"
}
}
}
8 changes: 1 addition & 7 deletions src/server/views/badge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ pub fn badge(
analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
badge_knobs: ExtraConfig,
) -> Badge {
let subject = if let Some(subject) = badge_knobs.subject {
subject
} else if badge_knobs.compact {
"deps".into()
} else {
"dependencies".into()
};
let subject = badge_knobs.subject().to_owned();

let opts = match analysis_outcome {
Some(outcome) => {
Expand Down
95 changes: 80 additions & 15 deletions src/utils/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,39 +41,27 @@ impl<L, R> From<Either<L, R>> for UntaggedEither<L, R> {
}
}

impl<L, R> UntaggedEither<L, R> {
pub fn into_either(self) -> Either<L, R> {
self.into()
}
}

/// A generic newtype which serialized using `Display` and deserialized using `FromStr`.
#[derive(Default, Clone, DeserializeFromStr, SerializeDisplay)]
pub struct SerdeDisplayFromStr<T>(pub T);

impl<T> From<T> for SerdeDisplayFromStr<T> {
fn from(value: T) -> Self {
Self(value)
}
}

impl<T: Debug> Debug for SerdeDisplayFromStr<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
Debug::fmt(&self.0, f)
}
}

impl<T: Display> Display for SerdeDisplayFromStr<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
Display::fmt(&self.0, f)
}
}

impl<T: FromStr> FromStr for SerdeDisplayFromStr<T> {
type Err = T::Err;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.parse::<T>()?.into())
s.parse::<T>().map(Self)
}
}

Expand All @@ -83,3 +71,80 @@ impl<T: FromStr> FromStr for SerdeDisplayFromStr<T> {
/// are used. The Wrap type here forces the deserialization process to
/// be delegated to `FromStr`.
pub type WrappedBool = SerdeDisplayFromStr<bool>;

/// Returns truncated string accounting for multi-byte characters.
pub(crate) fn safe_truncate(s: &str, len: usize) -> &str {
if len == 0 {
return "";
}

if s.len() <= len {
return s;
}

if s.is_char_boundary(len) {
return &s[0..len];
}

// Only 3 cases possible: 1, 2, or 3 bytes need to be removed for a new,
// valid UTF-8 string to appear when truncated, just enumerate them,
// Underflow is not possible since position 0 is always a valid boundary.

if let Some((slice, _rest)) = s.split_at_checked(len - 1) {
return slice;
}

if let Some((slice, _rest)) = s.split_at_checked(len - 2) {
return slice;
}

if let Some((slice, _rest)) = s.split_at_checked(len - 3) {
return slice;
}

unreachable!("all branches covered");
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn safe_truncation() {
assert_eq!(safe_truncate("", 0), "");
assert_eq!(safe_truncate("", 1), "");
assert_eq!(safe_truncate("", 9), "");

assert_eq!(safe_truncate("a", 0), "");
assert_eq!(safe_truncate("a", 1), "a");
assert_eq!(safe_truncate("a", 9), "a");

assert_eq!(safe_truncate("lorem\nipsum", 0), "");
assert_eq!(safe_truncate("lorem\nipsum", 5), "lorem");
assert_eq!(safe_truncate("lorem\nipsum", usize::MAX), "lorem\nipsum");

assert_eq!(safe_truncate("café", 1), "c");
assert_eq!(safe_truncate("café", 2), "ca");
assert_eq!(safe_truncate("café", 3), "caf");
assert_eq!(safe_truncate("café", 4), "caf");
assert_eq!(safe_truncate("café", 5), "café");

// 2-byte char
assert_eq!(safe_truncate("é", 0), "");
assert_eq!(safe_truncate("é", 1), "");
assert_eq!(safe_truncate("é", 2), "é");

// 3-byte char
assert_eq!(safe_truncate("⊕", 0), "");
assert_eq!(safe_truncate("⊕", 1), "");
assert_eq!(safe_truncate("⊕", 2), "");
assert_eq!(safe_truncate("⊕", 3), "⊕");

// 4-byte char
assert_eq!(safe_truncate("🦊", 0), "");
assert_eq!(safe_truncate("🦊", 1), "");
assert_eq!(safe_truncate("🦊", 2), "");
assert_eq!(safe_truncate("🦊", 3), "");
assert_eq!(safe_truncate("🦊", 4), "🦊");
}
}

0 comments on commit 5a215eb

Please sign in to comment.