Compare commits

..

7 commits

Author SHA1 Message Date
Rob Ede
7c50788d59
refactor: migrate web server to Actix Web 2024-09-19 20:56:39 +01:00
dependabot[bot]
ff6d9e880f
Bump quinn-proto from 0.11.6 to 0.11.8 (#237)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 21:34:39 +01:00
Paolo Barbolini
b016ff1f8f
Bump dependencies (#236) 2024-08-31 17:57:25 +02:00
Rob Ede
5a215ebbfb
refactor: remove unicode dep and simplify UntaggedEither impl 2024-08-07 03:14:50 +01:00
29
62891bb2db
Support custom subject text (#231) 2024-08-07 03:14:37 +01:00
dependabot[bot]
e720a5f4b5
Bump gix-attributes from 0.22.2 to 0.22.3 (#234)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-26 11:10:39 +01:00
dependabot[bot]
9c5c5f88e0
Bump openssl from 0.10.64 to 0.10.66 (#233) 2024-07-22 19:23:22 +01:00
8 changed files with 1408 additions and 602 deletions

1765
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,10 @@ actix-web = "4"
actix-web-lab = "0.20" actix-web-lab = "0.20"
anyhow = "1" anyhow = "1"
cadence = "1" cadence = "1"
crates-index = { version = "2", default-features = false, features = ["git"] } crates-index = { version = "3", default-features = false, features = ["git"] }
derive_more = "0.99" derive_more = { version = "1", features = ["display", "error", "from"] }
dotenvy = "0.15" dotenvy = "0.15"
either = "1.12"
font-awesome-as-a-crate = "0.3" font-awesome-as-a-crate = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", default-features = false, features = ["std"] }
error_reporter = "1" error_reporter = "1"
@ -30,13 +31,14 @@ maud = "0.26"
mime = "0.3" mime = "0.3"
once_cell = "1" once_cell = "1"
parking_lot = "0.12" parking_lot = "0.12"
pulldown-cmark = "0.11" pulldown-cmark = "0.12"
relative-path = { version = "1", features = ["serde"] } relative-path = { version = "1", features = ["serde"] }
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
rustsec = "0.29" rustsec = "0.29"
semver = { version = "1.0", features = ["serde"] } semver = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
serde_with = "3"
tokio = { version = "1.24.2", features = ["rt", "macros", "sync", "time"] } tokio = { version = "1.24.2", features = ["rt", "macros", "sync", "time"] }
toml = "0.8" toml = "0.8"
tracing = "0.1.30" tracing = "0.1.30"

View file

@ -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! 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`: Badges have a few options, specified with query parameters:
- `style`: which matches the styles from `shields.io`:
- `?style=flat` (default) - `?style=flat` (default)
- `?style=flat-square` - `?style=flat-square`
- `?style=for-the-badge` - `?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 ## Contributing

View file

@ -9,22 +9,22 @@ use crate::server::views::html::error::{render, render_404};
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub(crate) enum ServerError { pub(crate) enum ServerError {
#[display(fmt = "Could not retrieve popular items")] #[display("Could not retrieve popular items")]
PopularItemsFailed, PopularItemsFailed,
#[display(fmt = "Crate not found")] #[display("Crate not found")]
CrateNotFound, CrateNotFound,
#[display(fmt = "Could not parse crate path")] #[display("Could not parse crate path")]
BadCratePath, BadCratePath,
#[display(fmt = "Could not fetch crate information")] #[display("Could not fetch crate information")]
CrateFetchFailed, CrateFetchFailed,
#[display(fmt = "Could not parse repository path")] #[display("Could not parse repository path")]
BadRepoPath, BadRepoPath,
#[display(fmt = "Crate/repo analysis failed")] #[display("Crate/repo analysis failed")]
AnalysisFailed(Markup), AnalysisFailed(Markup),
} }

View file

@ -38,8 +38,11 @@ use crate::{
repo::RepoPath, repo::RepoPath,
SubjectPath, SubjectPath,
}, },
utils::common::{safe_truncate, UntaggedEither, WrappedBool},
}; };
const MAX_SUBJECT_WIDTH: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
enum StatusFormat { enum StatusFormat {
Html, Html,
@ -310,18 +313,35 @@ static SELF_BASE_URL: Lazy<String> =
pub struct ExtraConfig { pub struct ExtraConfig {
/// Badge style to show /// Badge style to show
style: BadgeStyle, style: BadgeStyle,
/// Whether the inscription _"dependencies"_ should be abbreviated as _"deps"_ in the badge. /// Whether the inscription _"dependencies"_ should be abbreviated as _"deps"_ in the badge.
compact: bool, 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 in which the crate resides within the repository
path: Option<String>, path: Option<String>,
} }
impl ExtraConfig { impl ExtraConfig {
fn from_query_string(qs: Option<&str>) -> Self { fn from_query_string(qs: Option<&str>) -> Self {
/// This wrapper can make the deserialization process infallible.
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
struct QueryParam<T>(UntaggedEither<T, String>);
impl<T> QueryParam<T> {
fn opt(self) -> Option<T> {
either::Either::from(self.0).left()
}
}
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]
struct ExtraConfigPartial { struct ExtraConfigPartial {
style: Option<BadgeStyle>, style: Option<QueryParam<BadgeStyle>>,
compact: Option<bool>, compact: Option<QueryParam<WrappedBool>>,
subject: Option<String>,
path: Option<String>, path: Option<String>,
} }
@ -330,9 +350,33 @@ impl ExtraConfig {
.unwrap_or_default(); .unwrap_or_default();
Self { Self {
style: extra_config.style.unwrap_or_default(), style: extra_config
compact: extra_config.compact.unwrap_or_default(), .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(|subject| safe_truncate(&subject, MAX_SUBJECT_WIDTH).to_owned()),
path: extra_config.path, 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"
}
}
} }

View file

@ -7,12 +7,7 @@ pub fn badge(
analysis_outcome: Option<&AnalyzeDependenciesOutcome>, analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
badge_knobs: ExtraConfig, badge_knobs: ExtraConfig,
) -> Badge { ) -> Badge {
let subject = if badge_knobs.compact { let subject = badge_knobs.subject().to_owned();
"deps"
} else {
"dependencies"
}
.to_owned();
let opts = match analysis_outcome { let opts = match analysis_outcome {
Some(outcome) => { Some(outcome) => {

150
src/utils/common.rs Normal file
View file

@ -0,0 +1,150 @@
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<L, R> {
Left(L),
Right(R),
}
impl<L, R> From<UntaggedEither<L, R>> for Either<L, R> {
fn from(value: UntaggedEither<L, R>) -> Self {
match value {
UntaggedEither::Left(l) => Self::Left(l),
UntaggedEither::Right(r) => Self::Right(r),
}
}
}
impl<L, R> From<Either<L, R>> for UntaggedEither<L, R> {
fn from(value: Either<L, R>) -> Self {
match value {
Either::Left(l) => UntaggedEither::Left(l),
Either::Right(r) => UntaggedEither::Right(r),
}
}
}
/// A generic newtype which serialized using `Display` and deserialized using `FromStr`.
#[derive(Default, Clone, DeserializeFromStr, SerializeDisplay)]
pub struct SerdeDisplayFromStr<T>(pub T);
impl<T: Debug> Debug for SerdeDisplayFromStr<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.0, f)
}
}
impl<T: Display> Display for SerdeDisplayFromStr<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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> {
s.parse::<T>().map(Self)
}
}
/// 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<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), "🦊");
}
}

View file

@ -1,2 +1,3 @@
pub mod cache; pub mod cache;
pub mod common;
pub mod index; pub mod index;