use std::path::{Path, PathBuf};
use thiserror::Error;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum NumericParseError {
#[error("numeric input must be non-empty")]
Empty,
#[error("expected a non-negative integer")]
Invalid,
#[error("integer is out of range")]
OutOfRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PathNormalizeError {
#[error("path input must be non-empty")]
Empty,
#[error("path input must not contain surrounding whitespace")]
SurroundingWhitespace,
#[error("file URI is invalid")]
InvalidFileUri,
#[error("file URI must resolve to a local path")]
NonLocalFileUri,
}
pub fn parse_human_unsigned_u64(raw: &str) -> Result<u64, NumericParseError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(NumericParseError::Empty);
}
if let Ok(value) = trimmed.parse::<u64>() {
return Ok(value);
}
if !trimmed.contains('.') {
return Err(if trimmed.bytes().all(|byte| byte.is_ascii_digit()) {
NumericParseError::OutOfRange
} else {
NumericParseError::Invalid
});
}
let (integer, fraction) = trimmed.split_once('.').ok_or(NumericParseError::Invalid)?;
if integer.is_empty() || fraction.is_empty() || !fraction.bytes().all(|byte| byte == b'0') {
return Err(NumericParseError::Invalid);
}
integer
.parse::<u64>()
.map_err(|_| NumericParseError::OutOfRange)
}
pub fn checked_u64_to_usize(value: u64) -> Result<usize, NumericParseError> {
usize::try_from(value).map_err(|_| NumericParseError::OutOfRange)
}
#[must_use]
pub(crate) fn fold_ascii_token(raw: &str) -> String {
raw.chars()
.filter(|character| character.is_ascii_alphanumeric())
.map(|character| character.to_ascii_lowercase())
.collect()
}
pub fn normalize_local_path(
raw: &str,
workspace_root: Option<&Path>,
) -> Result<PathBuf, PathNormalizeError> {
if raw.is_empty() {
return Err(PathNormalizeError::Empty);
}
if raw.trim() != raw {
return Err(PathNormalizeError::SurroundingWhitespace);
}
let parsed = if raw.starts_with("file://") {
let file_url = Url::parse(raw).map_err(|_| PathNormalizeError::InvalidFileUri)?;
file_url
.to_file_path()
.map_err(|()| PathNormalizeError::NonLocalFileUri)?
} else {
PathBuf::from(raw)
};
Ok(if parsed.is_absolute() {
parsed
} else if let Some(workspace_root) = workspace_root {
workspace_root.join(parsed)
} else {
parsed
})
}
#[cfg(test)]
mod tests {
use super::{
NumericParseError, fold_ascii_token, normalize_local_path, parse_human_unsigned_u64,
};
use std::path::Path;
#[test]
fn parses_human_unsigned_integers() {
assert_eq!(parse_human_unsigned_u64("42"), Ok(42));
assert_eq!(parse_human_unsigned_u64("42.0"), Ok(42));
assert_eq!(parse_human_unsigned_u64(" 7 "), Ok(7));
assert_eq!(
parse_human_unsigned_u64("-1"),
Err(NumericParseError::Invalid)
);
assert_eq!(
parse_human_unsigned_u64("7.5"),
Err(NumericParseError::Invalid)
);
assert_eq!(
parse_human_unsigned_u64("9007199254740993.0"),
Ok(9_007_199_254_740_993)
);
assert_eq!(
parse_human_unsigned_u64("18446744073709551616"),
Err(NumericParseError::OutOfRange)
);
}
#[test]
fn normalizes_ascii_tokens() {
assert_eq!(
fold_ascii_token("textDocument/prepareRename"),
"textdocumentpreparerename"
);
assert_eq!(fold_ascii_token("prepare_rename"), "preparerename");
}
#[test]
fn resolves_relative_paths_against_workspace_root() {
let root = Path::new("/tmp/example-root");
let resolved = normalize_local_path("src/lib.rs", Some(root));
assert!(resolved.is_ok());
assert_eq!(
resolved.ok().as_deref(),
Some(root.join("src/lib.rs").as_path())
);
assert!(normalize_local_path(" src/lib.rs", Some(root)).is_err());
}
}