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#[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum GoWorkConfigFile {
36 GoWork,
37}
38
39impl GoWorkConfigFile {
40 #[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#[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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct GoWorkModulePath(String);
109
110impl GoWorkModulePath {
111 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 #[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
164pub struct GoWorkFile {
165 directives: Vec<GoWorkDirective>,
166}
167
168impl GoWorkFile {
169 #[must_use]
171 pub const fn new() -> Self {
172 Self {
173 directives: Vec::new(),
174 }
175 }
176
177 #[must_use]
179 pub fn with_directive(mut self, directive: GoWorkDirective) -> Self {
180 self.directives.push(directive);
181 self
182 }
183
184 #[must_use]
186 pub fn directives(&self) -> &[GoWorkDirective] {
187 &self.directives
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum GoWorkDirective {
194 Use(GoWorkUseDirective),
195 Replace(GoWorkReplaceDirective),
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct GoWorkUseDirective {
201 module_path: GoWorkModulePath,
202}
203
204impl GoWorkUseDirective {
205 #[must_use]
207 pub const fn new(module_path: GoWorkModulePath) -> Self {
208 Self { module_path }
209 }
210
211 #[must_use]
213 pub const fn module_path(&self) -> &GoWorkModulePath {
214 &self.module_path
215 }
216}
217
218#[derive(Clone, Debug, Eq, PartialEq)]
220pub struct GoWorkReplaceDirective {
221 replacement: GoModuleReplacement,
222}
223
224impl GoWorkReplaceDirective {
225 #[must_use]
227 pub const fn new(replacement: GoModuleReplacement) -> Self {
228 Self { replacement }
229 }
230
231 #[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}