Skip to main content

use_go_work/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_go_module::GoModuleReplacement;
8
9/// Error returned by Go workspace metadata constructors.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum GoWorkError {
12    EmptyModulePath,
13    InvalidModulePath,
14    EmptyLabel,
15    UnknownLabel,
16}
17
18impl fmt::Display for GoWorkError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::EmptyModulePath => {
22                formatter.write_str("Go workspace module path cannot be empty")
23            }
24            Self::InvalidModulePath => formatter.write_str("invalid Go workspace module path"),
25            Self::EmptyLabel => formatter.write_str("go.work metadata label cannot be empty"),
26            Self::UnknownLabel => formatter.write_str("unknown go.work metadata label"),
27        }
28    }
29}
30
31impl Error for GoWorkError {}
32
33/// `go.work` config file kind.
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum GoWorkConfigFile {
36    GoWork,
37}
38
39impl GoWorkConfigFile {
40    /// Returns the config file name.
41    #[must_use]
42    pub const fn as_str(self) -> &'static str {
43        "go.work"
44    }
45}
46
47impl fmt::Display for GoWorkConfigFile {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        formatter.write_str(self.as_str())
50    }
51}
52
53impl FromStr for GoWorkConfigFile {
54    type Err = GoWorkError;
55
56    fn from_str(value: &str) -> Result<Self, Self::Err> {
57        match normalized_label(value)?.as_str() {
58            "go.work" | "gowork" => Ok(Self::GoWork),
59            _ => Err(GoWorkError::UnknownLabel),
60        }
61    }
62}
63
64/// Go workspace layout metadata.
65#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
66pub enum GoWorkspaceLayout {
67    SingleModule,
68    MultiModule,
69    Monorepo,
70    ToolWorkspace,
71}
72
73impl GoWorkspaceLayout {
74    /// Returns the workspace layout label.
75    #[must_use]
76    pub const fn as_str(self) -> &'static str {
77        match self {
78            Self::SingleModule => "single-module",
79            Self::MultiModule => "multi-module",
80            Self::Monorepo => "monorepo",
81            Self::ToolWorkspace => "tool-workspace",
82        }
83    }
84}
85
86impl fmt::Display for GoWorkspaceLayout {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter.write_str(self.as_str())
89    }
90}
91
92impl FromStr for GoWorkspaceLayout {
93    type Err = GoWorkError;
94
95    fn from_str(value: &str) -> Result<Self, Self::Err> {
96        match normalized_label(value)?.as_str() {
97            "single-module" | "single_module" | "single module" => Ok(Self::SingleModule),
98            "multi-module" | "multi_module" | "multi module" => Ok(Self::MultiModule),
99            "monorepo" => Ok(Self::Monorepo),
100            "tool-workspace" | "tool_workspace" | "tool workspace" => Ok(Self::ToolWorkspace),
101            _ => Err(GoWorkError::UnknownLabel),
102        }
103    }
104}
105
106/// Validated `go.work` module path metadata.
107#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct GoWorkModulePath(String);
109
110impl GoWorkModulePath {
111    /// Creates workspace module path metadata.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`GoWorkError`] when the path is empty or has obvious whitespace/empty segments.
116    pub fn new(value: impl AsRef<str>) -> Result<Self, GoWorkError> {
117        let trimmed = value.as_ref().trim();
118        if trimmed.is_empty() {
119            return Err(GoWorkError::EmptyModulePath);
120        }
121        if trimmed.chars().any(char::is_whitespace) || trimmed.split('/').any(str::is_empty) {
122            return Err(GoWorkError::InvalidModulePath);
123        }
124        Ok(Self(trimmed.to_string()))
125    }
126
127    /// Returns the module path text.
128    #[must_use]
129    pub fn as_str(&self) -> &str {
130        &self.0
131    }
132}
133
134impl AsRef<str> for GoWorkModulePath {
135    fn as_ref(&self) -> &str {
136        self.as_str()
137    }
138}
139
140impl fmt::Display for GoWorkModulePath {
141    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142        formatter.write_str(self.as_str())
143    }
144}
145
146impl FromStr for GoWorkModulePath {
147    type Err = GoWorkError;
148
149    fn from_str(value: &str) -> Result<Self, Self::Err> {
150        Self::new(value)
151    }
152}
153
154impl TryFrom<&str> for GoWorkModulePath {
155    type Error = GoWorkError;
156
157    fn try_from(value: &str) -> Result<Self, Self::Error> {
158        Self::new(value)
159    }
160}
161
162/// Lightweight `go.work` file metadata.
163#[derive(Clone, Debug, Default, Eq, PartialEq)]
164pub struct GoWorkFile {
165    directives: Vec<GoWorkDirective>,
166}
167
168impl GoWorkFile {
169    /// Creates empty `go.work` metadata.
170    #[must_use]
171    pub const fn new() -> Self {
172        Self {
173            directives: Vec::new(),
174        }
175    }
176
177    /// Adds a directive and returns the updated metadata.
178    #[must_use]
179    pub fn with_directive(mut self, directive: GoWorkDirective) -> Self {
180        self.directives.push(directive);
181        self
182    }
183
184    /// Returns the directives.
185    #[must_use]
186    pub fn directives(&self) -> &[GoWorkDirective] {
187        &self.directives
188    }
189}
190
191/// `go.work` directive metadata.
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum GoWorkDirective {
194    Use(GoWorkUseDirective),
195    Replace(GoWorkReplaceDirective),
196}
197
198/// `use` directive metadata.
199#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct GoWorkUseDirective {
201    module_path: GoWorkModulePath,
202}
203
204impl GoWorkUseDirective {
205    /// Creates a `use` directive.
206    #[must_use]
207    pub const fn new(module_path: GoWorkModulePath) -> Self {
208        Self { module_path }
209    }
210
211    /// Returns the workspace module path.
212    #[must_use]
213    pub const fn module_path(&self) -> &GoWorkModulePath {
214        &self.module_path
215    }
216}
217
218/// `replace` directive metadata.
219#[derive(Clone, Debug, Eq, PartialEq)]
220pub struct GoWorkReplaceDirective {
221    replacement: GoModuleReplacement,
222}
223
224impl GoWorkReplaceDirective {
225    /// Creates a `replace` directive.
226    #[must_use]
227    pub const fn new(replacement: GoModuleReplacement) -> Self {
228        Self { replacement }
229    }
230
231    /// Returns the replacement metadata.
232    #[must_use]
233    pub const fn replacement(&self) -> &GoModuleReplacement {
234        &self.replacement
235    }
236}
237
238fn normalized_label(value: &str) -> Result<String, GoWorkError> {
239    let trimmed = value.trim();
240    if trimmed.is_empty() {
241        Err(GoWorkError::EmptyLabel)
242    } else {
243        Ok(trimmed.to_ascii_lowercase())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::{
250        GoWorkConfigFile, GoWorkDirective, GoWorkError, GoWorkFile, GoWorkModulePath,
251        GoWorkReplaceDirective, GoWorkUseDirective, GoWorkspaceLayout,
252    };
253    use use_go_module::{GoModulePath, GoModuleReplacement};
254
255    #[test]
256    fn validates_workspace_module_paths() -> Result<(), GoWorkError> {
257        let path = GoWorkModulePath::new("./app")?;
258        assert_eq!(path.as_str(), "./app");
259        assert_eq!(GoWorkModulePath::new(""), Err(GoWorkError::EmptyModulePath));
260        assert_eq!(
261            GoWorkModulePath::new("./app module"),
262            Err(GoWorkError::InvalidModulePath)
263        );
264        assert_eq!(
265            GoWorkModulePath::new("app//module"),
266            Err(GoWorkError::InvalidModulePath)
267        );
268        Ok(())
269    }
270
271    #[test]
272    fn models_go_work_directives() -> Result<(), Box<dyn std::error::Error>> {
273        let use_directive = GoWorkUseDirective::new(GoWorkModulePath::new("./app")?);
274        let replacement = GoWorkReplaceDirective::new(GoModuleReplacement::new(
275            GoModulePath::new("example.com/app")?,
276            GoModulePath::new("./app")?,
277        ));
278        let file = GoWorkFile::new()
279            .with_directive(GoWorkDirective::Use(use_directive))
280            .with_directive(GoWorkDirective::Replace(replacement));
281
282        assert_eq!(file.directives().len(), 2);
283        Ok(())
284    }
285
286    #[test]
287    fn parses_config_and_layout_labels() -> Result<(), GoWorkError> {
288        assert_eq!(
289            "go.work".parse::<GoWorkConfigFile>()?,
290            GoWorkConfigFile::GoWork
291        );
292        assert_eq!(
293            "multi module".parse::<GoWorkspaceLayout>()?,
294            GoWorkspaceLayout::MultiModule
295        );
296        assert_eq!(
297            GoWorkspaceLayout::ToolWorkspace.to_string(),
298            "tool-workspace"
299        );
300        Ok(())
301    }
302}