#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_go_identifier::is_valid_ascii_go_identifier;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoPackageError {
EmptyName,
InvalidName,
EmptyPath,
EmptyPathSegment,
InvalidPathSegment,
UnknownLabel,
}
impl fmt::Display for GoPackageError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyName => formatter.write_str("Go package name cannot be empty"),
Self::InvalidName => formatter.write_str("invalid Go package name"),
Self::EmptyPath => formatter.write_str("Go package path cannot be empty"),
Self::EmptyPathSegment => {
formatter.write_str("Go package path contains an empty segment")
}
Self::InvalidPathSegment => formatter.write_str("invalid Go package path segment"),
Self::UnknownLabel => formatter.write_str("unknown Go package metadata label"),
}
}
}
impl Error for GoPackageError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoPackageName(String);
impl GoPackageName {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(GoPackageError::EmptyName);
}
if !is_valid_ascii_go_identifier(trimmed) {
return Err(GoPackageError::InvalidName);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for GoPackageName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GoPackageName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoPackageName {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for GoPackageName {
type Error = GoPackageError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoPackagePath(String);
impl GoPackagePath {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
let trimmed = value.as_ref().trim();
validate_path(trimmed)?;
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.0.split('/')
}
}
impl AsRef<str> for GoPackagePath {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GoPackagePath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoPackagePath {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for GoPackagePath {
type Error = GoPackageError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoPackageDocName(String);
impl GoPackageDocName {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(GoPackageError::EmptyName)
} else {
Ok(Self(trimmed.to_string()))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for GoPackageDocName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoPackageDocName {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoPackageVisibility {
Internal,
Public,
}
impl GoPackageVisibility {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Internal => "internal",
Self::Public => "public",
}
}
}
impl fmt::Display for GoPackageVisibility {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoPackageVisibility {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"internal" => Ok(Self::Internal),
"public" => Ok(Self::Public),
_ => Err(GoPackageError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoPackageLayout {
SinglePackage,
MultiPackage,
InternalPackage,
CmdPackage,
}
impl GoPackageLayout {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SinglePackage => "single-package",
Self::MultiPackage => "multi-package",
Self::InternalPackage => "internal-package",
Self::CmdPackage => "cmd-package",
}
}
}
impl fmt::Display for GoPackageLayout {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoPackageLayout {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"single-package" | "single_package" | "single package" => Ok(Self::SinglePackage),
"multi-package" | "multi_package" | "multi package" => Ok(Self::MultiPackage),
"internal-package" | "internal_package" | "internal package" => {
Ok(Self::InternalPackage)
}
"cmd-package" | "cmd_package" | "cmd package" => Ok(Self::CmdPackage),
_ => Err(GoPackageError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoFileKind {
Source,
Test,
Generated,
BuildTagged,
Cgo,
ModuleConfig,
WorkspaceConfig,
}
impl GoFileKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Source => "source",
Self::Test => "test",
Self::Generated => "generated",
Self::BuildTagged => "build-tagged",
Self::Cgo => "cgo",
Self::ModuleConfig => "module-config",
Self::WorkspaceConfig => "workspace-config",
}
}
}
impl fmt::Display for GoFileKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoFileKind {
type Err = GoPackageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"source" => Ok(Self::Source),
"test" => Ok(Self::Test),
"generated" => Ok(Self::Generated),
"build-tagged" | "build_tagged" | "build tagged" => Ok(Self::BuildTagged),
"cgo" => Ok(Self::Cgo),
"module-config" | "module_config" | "module config" => Ok(Self::ModuleConfig),
"workspace-config" | "workspace_config" | "workspace config" => {
Ok(Self::WorkspaceConfig)
}
_ => Err(GoPackageError::UnknownLabel),
}
}
}
fn validate_path(value: &str) -> Result<(), GoPackageError> {
if value.is_empty() {
return Err(GoPackageError::EmptyPath);
}
for segment in value.split('/') {
if segment.is_empty() {
return Err(GoPackageError::EmptyPathSegment);
}
if segment.trim() != segment
|| segment.chars().any(char::is_whitespace)
|| segment.contains('\\')
{
return Err(GoPackageError::InvalidPathSegment);
}
}
Ok(())
}
fn normalized_label(value: &str) -> Result<String, GoPackageError> {
let trimmed = value.trim();
if trimmed.is_empty() {
Err(GoPackageError::UnknownLabel)
} else {
Ok(trimmed.to_ascii_lowercase())
}
}
#[cfg(test)]
mod tests {
use super::{
GoFileKind, GoPackageDocName, GoPackageError, GoPackageLayout, GoPackageName,
GoPackagePath, GoPackageVisibility,
};
#[test]
fn validates_package_names() -> Result<(), GoPackageError> {
let name = GoPackageName::new("http")?;
assert_eq!(name.as_str(), "http");
assert_eq!(GoPackageName::new(""), Err(GoPackageError::EmptyName));
assert_eq!(
GoPackageName::new("net/http"),
Err(GoPackageError::InvalidName)
);
Ok(())
}
#[test]
fn validates_package_paths() -> Result<(), GoPackageError> {
let path = GoPackagePath::new("net/http")?;
assert_eq!(path.segments().collect::<Vec<_>>(), vec!["net", "http"]);
assert_eq!(GoPackagePath::new(""), Err(GoPackageError::EmptyPath));
assert_eq!(
GoPackagePath::new("net//http"),
Err(GoPackageError::EmptyPathSegment)
);
assert_eq!(
GoPackagePath::new("net/http server"),
Err(GoPackageError::InvalidPathSegment)
);
Ok(())
}
#[test]
fn stores_doc_names() -> Result<(), GoPackageError> {
let doc_name = GoPackageDocName::new("Package http")?;
assert_eq!(doc_name.to_string(), "Package http");
Ok(())
}
#[test]
fn parses_package_enums() -> Result<(), GoPackageError> {
assert_eq!(
"internal".parse::<GoPackageVisibility>()?,
GoPackageVisibility::Internal
);
assert_eq!(
"cmd package".parse::<GoPackageLayout>()?,
GoPackageLayout::CmdPackage
);
assert_eq!(
"build_tagged".parse::<GoFileKind>()?,
GoFileKind::BuildTagged
);
assert_eq!(GoFileKind::ModuleConfig.to_string(), "module-config");
Ok(())
}
}