#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_go_module::GoModuleReplacement;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoWorkError {
EmptyModulePath,
InvalidModulePath,
EmptyLabel,
UnknownLabel,
}
impl fmt::Display for GoWorkError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyModulePath => {
formatter.write_str("Go workspace module path cannot be empty")
}
Self::InvalidModulePath => formatter.write_str("invalid Go workspace module path"),
Self::EmptyLabel => formatter.write_str("go.work metadata label cannot be empty"),
Self::UnknownLabel => formatter.write_str("unknown go.work metadata label"),
}
}
}
impl Error for GoWorkError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoWorkConfigFile {
GoWork,
}
impl GoWorkConfigFile {
#[must_use]
pub const fn as_str(self) -> &'static str {
"go.work"
}
}
impl fmt::Display for GoWorkConfigFile {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoWorkConfigFile {
type Err = GoWorkError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"go.work" | "gowork" => Ok(Self::GoWork),
_ => Err(GoWorkError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoWorkspaceLayout {
SingleModule,
MultiModule,
Monorepo,
ToolWorkspace,
}
impl GoWorkspaceLayout {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SingleModule => "single-module",
Self::MultiModule => "multi-module",
Self::Monorepo => "monorepo",
Self::ToolWorkspace => "tool-workspace",
}
}
}
impl fmt::Display for GoWorkspaceLayout {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoWorkspaceLayout {
type Err = GoWorkError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_label(value)?.as_str() {
"single-module" | "single_module" | "single module" => Ok(Self::SingleModule),
"multi-module" | "multi_module" | "multi module" => Ok(Self::MultiModule),
"monorepo" => Ok(Self::Monorepo),
"tool-workspace" | "tool_workspace" | "tool workspace" => Ok(Self::ToolWorkspace),
_ => Err(GoWorkError::UnknownLabel),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoWorkModulePath(String);
impl GoWorkModulePath {
pub fn new(value: impl AsRef<str>) -> Result<Self, GoWorkError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(GoWorkError::EmptyModulePath);
}
if trimmed.chars().any(char::is_whitespace) || trimmed.split('/').any(str::is_empty) {
return Err(GoWorkError::InvalidModulePath);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for GoWorkModulePath {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GoWorkModulePath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoWorkModulePath {
type Err = GoWorkError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for GoWorkModulePath {
type Error = GoWorkError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GoWorkFile {
directives: Vec<GoWorkDirective>,
}
impl GoWorkFile {
#[must_use]
pub const fn new() -> Self {
Self {
directives: Vec::new(),
}
}
#[must_use]
pub fn with_directive(mut self, directive: GoWorkDirective) -> Self {
self.directives.push(directive);
self
}
#[must_use]
pub fn directives(&self) -> &[GoWorkDirective] {
&self.directives
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GoWorkDirective {
Use(GoWorkUseDirective),
Replace(GoWorkReplaceDirective),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GoWorkUseDirective {
module_path: GoWorkModulePath,
}
impl GoWorkUseDirective {
#[must_use]
pub const fn new(module_path: GoWorkModulePath) -> Self {
Self { module_path }
}
#[must_use]
pub const fn module_path(&self) -> &GoWorkModulePath {
&self.module_path
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GoWorkReplaceDirective {
replacement: GoModuleReplacement,
}
impl GoWorkReplaceDirective {
#[must_use]
pub const fn new(replacement: GoModuleReplacement) -> Self {
Self { replacement }
}
#[must_use]
pub const fn replacement(&self) -> &GoModuleReplacement {
&self.replacement
}
}
fn normalized_label(value: &str) -> Result<String, GoWorkError> {
let trimmed = value.trim();
if trimmed.is_empty() {
Err(GoWorkError::EmptyLabel)
} else {
Ok(trimmed.to_ascii_lowercase())
}
}
#[cfg(test)]
mod tests {
use super::{
GoWorkConfigFile, GoWorkDirective, GoWorkError, GoWorkFile, GoWorkModulePath,
GoWorkReplaceDirective, GoWorkUseDirective, GoWorkspaceLayout,
};
use use_go_module::{GoModulePath, GoModuleReplacement};
#[test]
fn validates_workspace_module_paths() -> Result<(), GoWorkError> {
let path = GoWorkModulePath::new("./app")?;
assert_eq!(path.as_str(), "./app");
assert_eq!(GoWorkModulePath::new(""), Err(GoWorkError::EmptyModulePath));
assert_eq!(
GoWorkModulePath::new("./app module"),
Err(GoWorkError::InvalidModulePath)
);
assert_eq!(
GoWorkModulePath::new("app//module"),
Err(GoWorkError::InvalidModulePath)
);
Ok(())
}
#[test]
fn models_go_work_directives() -> Result<(), Box<dyn std::error::Error>> {
let use_directive = GoWorkUseDirective::new(GoWorkModulePath::new("./app")?);
let replacement = GoWorkReplaceDirective::new(GoModuleReplacement::new(
GoModulePath::new("example.com/app")?,
GoModulePath::new("./app")?,
));
let file = GoWorkFile::new()
.with_directive(GoWorkDirective::Use(use_directive))
.with_directive(GoWorkDirective::Replace(replacement));
assert_eq!(file.directives().len(), 2);
Ok(())
}
#[test]
fn parses_config_and_layout_labels() -> Result<(), GoWorkError> {
assert_eq!(
"go.work".parse::<GoWorkConfigFile>()?,
GoWorkConfigFile::GoWork
);
assert_eq!(
"multi module".parse::<GoWorkspaceLayout>()?,
GoWorkspaceLayout::MultiModule
);
assert_eq!(
GoWorkspaceLayout::ToolWorkspace.to_string(),
"tool-workspace"
);
Ok(())
}
}