#![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 GoImportError {
EmptyPath,
InvalidPath,
EmptyAlias,
InvalidAlias,
UnknownLabel,
}
impl fmt::Display for GoImportError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyPath => formatter.write_str("Go import path cannot be empty"),
Self::InvalidPath => formatter.write_str("invalid Go import path"),
Self::EmptyAlias => formatter.write_str("Go import alias cannot be empty"),
Self::InvalidAlias => formatter.write_str("invalid Go import alias"),
Self::UnknownLabel => formatter.write_str("unknown Go import metadata label"),
}
}
}
impl Error for GoImportError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoImportPath(String);
impl GoImportPath {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(GoImportError::EmptyPath);
}
if trimmed.chars().any(char::is_whitespace) || trimmed.split('/').any(str::is_empty) {
return Err(GoImportError::InvalidPath);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_relative(&self) -> bool {
self.0.starts_with("./") || self.0.starts_with("../")
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for GoImportPath {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GoImportPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoImportPath {
type Err = GoImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for GoImportPath {
type Error = GoImportError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoImportAlias(String);
impl GoImportAlias {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(GoImportError::EmptyAlias);
}
if trimmed != "_" && trimmed != "." && !is_valid_ascii_go_identifier(trimmed) {
return Err(GoImportError::InvalidAlias);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_blank(&self) -> bool {
self.0 == "_"
}
#[must_use]
pub fn is_dot(&self) -> bool {
self.0 == "."
}
}
impl AsRef<str> for GoImportAlias {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GoImportAlias {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoImportAlias {
type Err = GoImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for GoImportAlias {
type Error = GoImportError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoImportKind {
StandardLibrary,
ThirdParty,
Internal,
Relative,
Blank,
Dot,
Aliased,
}
impl GoImportKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::StandardLibrary => "standard-library",
Self::ThirdParty => "third-party",
Self::Internal => "internal",
Self::Relative => "relative",
Self::Blank => "blank",
Self::Dot => "dot",
Self::Aliased => "aliased",
}
}
}
impl fmt::Display for GoImportKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoImportKind {
type Err = GoImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"standard-library" | "standard_library" | "standard library" => {
Ok(Self::StandardLibrary)
}
"third-party" | "third_party" | "third party" => Ok(Self::ThirdParty),
"internal" => Ok(Self::Internal),
"relative" => Ok(Self::Relative),
"blank" => Ok(Self::Blank),
"dot" => Ok(Self::Dot),
"aliased" => Ok(Self::Aliased),
_ => Err(GoImportError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoImportGroup {
StandardLibrary,
External,
Internal,
Local,
}
impl GoImportGroup {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::StandardLibrary => "standard-library",
Self::External => "external",
Self::Internal => "internal",
Self::Local => "local",
}
}
}
impl fmt::Display for GoImportGroup {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoImportGroup {
type Err = GoImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"standard-library" | "standard_library" | "standard library" => {
Ok(Self::StandardLibrary)
}
"external" => Ok(Self::External),
"internal" => Ok(Self::Internal),
"local" => Ok(Self::Local),
_ => Err(GoImportError::UnknownLabel),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GoImportSpec {
path: GoImportPath,
alias: Option<GoImportAlias>,
kind: GoImportKind,
}
impl GoImportSpec {
#[must_use]
pub const fn new(path: GoImportPath, kind: GoImportKind) -> Self {
Self {
path,
alias: None,
kind,
}
}
#[must_use]
pub fn with_alias(mut self, alias: GoImportAlias) -> Self {
self.alias = Some(alias);
self
}
#[must_use]
pub const fn path(&self) -> &GoImportPath {
&self.path
}
#[must_use]
pub const fn alias(&self) -> Option<&GoImportAlias> {
self.alias.as_ref()
}
#[must_use]
pub const fn kind(&self) -> GoImportKind {
self.kind
}
}
fn normalized_label(value: &str) -> Result<String, GoImportError> {
let trimmed = value.trim();
if trimmed.is_empty() {
Err(GoImportError::UnknownLabel)
} else {
Ok(trimmed.to_ascii_lowercase())
}
}
#[cfg(test)]
mod tests {
use super::{
GoImportAlias, GoImportError, GoImportGroup, GoImportKind, GoImportPath, GoImportSpec,
};
#[test]
fn validates_import_paths() -> Result<(), GoImportError> {
let path = GoImportPath::new("net/http")?;
assert_eq!(path.as_str(), "net/http");
assert!(!path.is_relative());
assert!(GoImportPath::new("../internal").is_ok_and(|value| value.is_relative()));
assert_eq!(GoImportPath::new(""), Err(GoImportError::EmptyPath));
assert_eq!(
GoImportPath::new("net//http"),
Err(GoImportError::InvalidPath)
);
assert_eq!(
GoImportPath::new("net/http client"),
Err(GoImportError::InvalidPath)
);
Ok(())
}
#[test]
fn validates_import_aliases() -> Result<(), GoImportError> {
let blank = GoImportAlias::new("_")?;
let dot = GoImportAlias::new(".")?;
let named = GoImportAlias::new("httpx")?;
assert!(blank.is_blank());
assert!(dot.is_dot());
assert_eq!(named.as_str(), "httpx");
assert_eq!(GoImportAlias::new(""), Err(GoImportError::EmptyAlias));
assert_eq!(
GoImportAlias::new("bad-alias"),
Err(GoImportError::InvalidAlias)
);
Ok(())
}
#[test]
fn parses_import_enums() -> Result<(), GoImportError> {
assert_eq!(
"third party".parse::<GoImportKind>()?,
GoImportKind::ThirdParty
);
assert_eq!(
"standard_library".parse::<GoImportGroup>()?,
GoImportGroup::StandardLibrary
);
assert_eq!(GoImportKind::Aliased.to_string(), "aliased");
Ok(())
}
#[test]
fn models_import_specs() -> Result<(), GoImportError> {
let spec = GoImportSpec::new(GoImportPath::new("net/http")?, GoImportKind::Aliased)
.with_alias(GoImportAlias::new("httpx")?);
assert_eq!(spec.path().as_str(), "net/http");
assert_eq!(spec.kind(), GoImportKind::Aliased);
assert_eq!(spec.alias().map(GoImportAlias::as_str), Some("httpx"));
Ok(())
}
}