use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum DependencyKind {
Dependencies,
DevDependencies,
PeerDependencies,
OptionalDependencies,
}
impl DependencyKind {
pub const ALL: [Self; 4] = [
Self::Dependencies,
Self::DevDependencies,
Self::PeerDependencies,
Self::OptionalDependencies,
];
#[must_use]
pub const fn field(self) -> &'static str {
match self {
Self::Dependencies => "dependencies",
Self::DevDependencies => "devDependencies",
Self::PeerDependencies => "peerDependencies",
Self::OptionalDependencies => "optionalDependencies",
}
}
#[must_use]
pub const fn token(self) -> &'static str {
match self {
Self::Dependencies => "prod",
Self::DevDependencies => "dev",
Self::PeerDependencies => "peer",
Self::OptionalDependencies => "optional",
}
}
#[must_use]
pub fn from_field(field: &str) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.field() == field)
}
#[must_use]
pub fn from_token(token: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|kind| kind.token() == token || kind.field() == token)
}
}
const MAX_PACKAGE_NAME_LEN: usize = 214;
#[must_use]
pub fn is_valid_package_name(name: &str) -> bool {
fn segment(text: &str) -> bool {
let mut bytes = text.bytes();
bytes
.next()
.is_some_and(|first| first.is_ascii_alphanumeric() || matches!(first, b'-' | b'~'))
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~'))
}
if name.len() > MAX_PACKAGE_NAME_LEN {
return false;
}
match name.strip_prefix('@') {
Some(scoped) => scoped
.split_once('/')
.is_some_and(|(scope, rest)| segment(scope) && segment(rest)),
None => segment(name),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn names_round_trip_for_every_kind() {
for kind in DependencyKind::ALL {
assert_eq!(DependencyKind::from_field(kind.field()), Some(kind));
assert_eq!(DependencyKind::from_token(kind.token()), Some(kind));
assert_eq!(DependencyKind::from_token(kind.field()), Some(kind));
assert_eq!(serde_json::to_value(kind).unwrap(), kind.field());
}
}
#[test]
fn unknown_names_are_rejected() {
for name in ["", "Dev", "devDeps", "bundled", "!dev"] {
assert_eq!(DependencyKind::from_field(name), None, "{name}");
assert_eq!(DependencyKind::from_token(name), None, "{name}");
}
}
}