Compare commits

..

1 commit

Author SHA1 Message Date
Rob Ede
13f5983c0a
refactor: migrate web server to Actix Web 2024-06-01 14:25:10 +01:00
8 changed files with 595 additions and 1401 deletions

1751
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,10 +18,9 @@ actix-web = "4"
actix-web-lab = "0.20" actix-web-lab = "0.20"
anyhow = "1" anyhow = "1"
cadence = "1" cadence = "1"
crates-index = { version = "3", default-features = false, features = ["git"] } crates-index = { version = "2", default-features = false, features = ["git"] }
derive_more = { version = "1", features = ["display", "error", "from"] } derive_more = "0.99"
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"
@ -31,14 +30,13 @@ 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.12" pulldown-cmark = "0.11"
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", features = ["serde"] } semver = { version = "1.0", 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,15 +19,10 @@ 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 options, specified with query parameters: Badges have a few style options, specified with query parameters, that match the styles from `shields.io`:
- `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("Could not retrieve popular items")] #[display(fmt = "Could not retrieve popular items")]
PopularItemsFailed, PopularItemsFailed,
#[display("Crate not found")] #[display(fmt = "Crate not found")]
CrateNotFound, CrateNotFound,
#[display("Could not parse crate path")] #[display(fmt = "Could not parse crate path")]
BadCratePath, BadCratePath,
#[display("Could not fetch crate information")] #[display(fmt = "Could not fetch crate information")]
CrateFetchFailed, CrateFetchFailed,
#[display("Could not parse repository path")] #[display(fmt = "Could not parse repository path")]
BadRepoPath, BadRepoPath,
#[display("Crate/repo analysis failed")] #[display(fmt = "Crate/repo analysis failed")]
AnalysisFailed(Markup), AnalysisFailed(Markup),
} }

View file

@ -38,11 +38,8 @@ 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,
@ -313,35 +310,18 @@ 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<QueryParam<BadgeStyle>>, style: Option<BadgeStyle>,
compact: Option<QueryParam<WrappedBool>>, compact: Option<bool>,
subject: Option<String>,
path: Option<String>, path: Option<String>,
} }
@ -350,33 +330,9 @@ impl ExtraConfig {
.unwrap_or_default(); .unwrap_or_default();
Self { Self {
style: extra_config style: extra_config.style.unwrap_or_default(),
.style compact: extra_config.compact.unwrap_or_default(),
.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,7 +7,12 @@ pub fn badge(
analysis_outcome: Option<&AnalyzeDependenciesOutcome>, analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
badge_knobs: ExtraConfig, badge_knobs: ExtraConfig,
) -> Badge { ) -> Badge {
let subject = badge_knobs.subject().to_owned(); let subject = if badge_knobs.compact {
"deps"
} else {
"dependencies"
}
.to_owned();
let opts = match analysis_outcome { let opts = match analysis_outcome {
Some(outcome) => { Some(outcome) => {

View file

@ -1,150 +0,0 @@
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,3 +1,2 @@
pub mod cache; pub mod cache;
pub mod common;
pub mod index; pub mod index;