Skip to main content

use_go_package/
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_identifier::is_valid_ascii_go_identifier;
8
9/// Error returned by Go package metadata constructors.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum GoPackageError {
12    EmptyName,
13    InvalidName,
14    EmptyPath,
15    EmptyPathSegment,
16    InvalidPathSegment,
17    UnknownLabel,
18}
19
20impl fmt::Display for GoPackageError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::EmptyName => formatter.write_str("Go package name cannot be empty"),
24            Self::InvalidName => formatter.write_str("invalid Go package name"),
25            Self::EmptyPath => formatter.write_str("Go package path cannot be empty"),
26            Self::EmptyPathSegment => {
27                formatter.write_str("Go package path contains an empty segment")
28            }
29            Self::InvalidPathSegment => formatter.write_str("invalid Go package path segment"),
30            Self::UnknownLabel => formatter.write_str("unknown Go package metadata label"),
31        }
32    }
33}
34
35impl Error for GoPackageError {}
36
37/// Validated Go package name metadata.
38#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct GoPackageName(String);
40
41impl GoPackageName {
42    /// Creates a package name from ASCII identifier-shaped text.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`GoPackageError`] when the name is empty or not ASCII identifier-shaped.
47    pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
48        let trimmed = value.as_ref().trim();
49        if trimmed.is_empty() {
50            return Err(GoPackageError::EmptyName);
51        }
52        if !is_valid_ascii_go_identifier(trimmed) {
53            return Err(GoPackageError::InvalidName);
54        }
55        Ok(Self(trimmed.to_string()))
56    }
57
58    /// Returns the package name.
59    #[must_use]
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63
64    /// Consumes the name and returns the owned text.
65    #[must_use]
66    pub fn into_string(self) -> String {
67        self.0
68    }
69}
70
71impl AsRef<str> for GoPackageName {
72    fn as_ref(&self) -> &str {
73        self.as_str()
74    }
75}
76
77impl fmt::Display for GoPackageName {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83impl FromStr for GoPackageName {
84    type Err = GoPackageError;
85
86    fn from_str(value: &str) -> Result<Self, Self::Err> {
87        Self::new(value)
88    }
89}
90
91impl TryFrom<&str> for GoPackageName {
92    type Error = GoPackageError;
93
94    fn try_from(value: &str) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99/// Validated slash-separated Go package path metadata.
100#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct GoPackagePath(String);
102
103impl GoPackagePath {
104    /// Creates a package path from slash-separated text.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`GoPackageError`] when the path is empty or contains empty/whitespace segments.
109    pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
110        let trimmed = value.as_ref().trim();
111        validate_path(trimmed)?;
112        Ok(Self(trimmed.to_string()))
113    }
114
115    /// Returns the package path.
116    #[must_use]
117    pub fn as_str(&self) -> &str {
118        &self.0
119    }
120
121    /// Consumes the path and returns the owned text.
122    #[must_use]
123    pub fn into_string(self) -> String {
124        self.0
125    }
126
127    /// Returns path segments.
128    pub fn segments(&self) -> impl Iterator<Item = &str> {
129        self.0.split('/')
130    }
131}
132
133impl AsRef<str> for GoPackagePath {
134    fn as_ref(&self) -> &str {
135        self.as_str()
136    }
137}
138
139impl fmt::Display for GoPackagePath {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        formatter.write_str(self.as_str())
142    }
143}
144
145impl FromStr for GoPackagePath {
146    type Err = GoPackageError;
147
148    fn from_str(value: &str) -> Result<Self, Self::Err> {
149        Self::new(value)
150    }
151}
152
153impl TryFrom<&str> for GoPackagePath {
154    type Error = GoPackageError;
155
156    fn try_from(value: &str) -> Result<Self, Self::Error> {
157        Self::new(value)
158    }
159}
160
161/// Go package documentation name metadata.
162#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub struct GoPackageDocName(String);
164
165impl GoPackageDocName {
166    /// Creates a package documentation name from non-empty text.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`GoPackageError::EmptyName`] when the value is empty after trimming.
171    pub fn new(value: impl AsRef<str>) -> Result<Self, GoPackageError> {
172        let trimmed = value.as_ref().trim();
173        if trimmed.is_empty() {
174            Err(GoPackageError::EmptyName)
175        } else {
176            Ok(Self(trimmed.to_string()))
177        }
178    }
179
180    /// Returns the documentation name.
181    #[must_use]
182    pub fn as_str(&self) -> &str {
183        &self.0
184    }
185}
186
187impl fmt::Display for GoPackageDocName {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        formatter.write_str(self.as_str())
190    }
191}
192
193impl FromStr for GoPackageDocName {
194    type Err = GoPackageError;
195
196    fn from_str(value: &str) -> Result<Self, Self::Err> {
197        Self::new(value)
198    }
199}
200
201/// Go package visibility metadata.
202#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
203pub enum GoPackageVisibility {
204    Internal,
205    Public,
206}
207
208impl GoPackageVisibility {
209    /// Returns the visibility label.
210    #[must_use]
211    pub const fn as_str(self) -> &'static str {
212        match self {
213            Self::Internal => "internal",
214            Self::Public => "public",
215        }
216    }
217}
218
219impl fmt::Display for GoPackageVisibility {
220    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221        formatter.write_str(self.as_str())
222    }
223}
224
225impl FromStr for GoPackageVisibility {
226    type Err = GoPackageError;
227
228    fn from_str(value: &str) -> Result<Self, Self::Err> {
229        match normalized_label(value)?.as_str() {
230            "internal" => Ok(Self::Internal),
231            "public" => Ok(Self::Public),
232            _ => Err(GoPackageError::UnknownLabel),
233        }
234    }
235}
236
237/// Go package layout metadata.
238#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
239pub enum GoPackageLayout {
240    SinglePackage,
241    MultiPackage,
242    InternalPackage,
243    CmdPackage,
244}
245
246impl GoPackageLayout {
247    /// Returns the layout label.
248    #[must_use]
249    pub const fn as_str(self) -> &'static str {
250        match self {
251            Self::SinglePackage => "single-package",
252            Self::MultiPackage => "multi-package",
253            Self::InternalPackage => "internal-package",
254            Self::CmdPackage => "cmd-package",
255        }
256    }
257}
258
259impl fmt::Display for GoPackageLayout {
260    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261        formatter.write_str(self.as_str())
262    }
263}
264
265impl FromStr for GoPackageLayout {
266    type Err = GoPackageError;
267
268    fn from_str(value: &str) -> Result<Self, Self::Err> {
269        match normalized_label(value)?.as_str() {
270            "single-package" | "single_package" | "single package" => Ok(Self::SinglePackage),
271            "multi-package" | "multi_package" | "multi package" => Ok(Self::MultiPackage),
272            "internal-package" | "internal_package" | "internal package" => {
273                Ok(Self::InternalPackage)
274            }
275            "cmd-package" | "cmd_package" | "cmd package" => Ok(Self::CmdPackage),
276            _ => Err(GoPackageError::UnknownLabel),
277        }
278    }
279}
280
281/// Go file kind metadata.
282#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
283pub enum GoFileKind {
284    Source,
285    Test,
286    Generated,
287    BuildTagged,
288    Cgo,
289    ModuleConfig,
290    WorkspaceConfig,
291}
292
293impl GoFileKind {
294    /// Returns the file-kind label.
295    #[must_use]
296    pub const fn as_str(self) -> &'static str {
297        match self {
298            Self::Source => "source",
299            Self::Test => "test",
300            Self::Generated => "generated",
301            Self::BuildTagged => "build-tagged",
302            Self::Cgo => "cgo",
303            Self::ModuleConfig => "module-config",
304            Self::WorkspaceConfig => "workspace-config",
305        }
306    }
307}
308
309impl fmt::Display for GoFileKind {
310    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
311        formatter.write_str(self.as_str())
312    }
313}
314
315impl FromStr for GoFileKind {
316    type Err = GoPackageError;
317
318    fn from_str(value: &str) -> Result<Self, Self::Err> {
319        match normalized_label(value)?.as_str() {
320            "source" => Ok(Self::Source),
321            "test" => Ok(Self::Test),
322            "generated" => Ok(Self::Generated),
323            "build-tagged" | "build_tagged" | "build tagged" => Ok(Self::BuildTagged),
324            "cgo" => Ok(Self::Cgo),
325            "module-config" | "module_config" | "module config" => Ok(Self::ModuleConfig),
326            "workspace-config" | "workspace_config" | "workspace config" => {
327                Ok(Self::WorkspaceConfig)
328            }
329            _ => Err(GoPackageError::UnknownLabel),
330        }
331    }
332}
333
334fn validate_path(value: &str) -> Result<(), GoPackageError> {
335    if value.is_empty() {
336        return Err(GoPackageError::EmptyPath);
337    }
338    for segment in value.split('/') {
339        if segment.is_empty() {
340            return Err(GoPackageError::EmptyPathSegment);
341        }
342        if segment.trim() != segment
343            || segment.chars().any(char::is_whitespace)
344            || segment.contains('\\')
345        {
346            return Err(GoPackageError::InvalidPathSegment);
347        }
348    }
349    Ok(())
350}
351
352fn normalized_label(value: &str) -> Result<String, GoPackageError> {
353    let trimmed = value.trim();
354    if trimmed.is_empty() {
355        Err(GoPackageError::UnknownLabel)
356    } else {
357        Ok(trimmed.to_ascii_lowercase())
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::{
364        GoFileKind, GoPackageDocName, GoPackageError, GoPackageLayout, GoPackageName,
365        GoPackagePath, GoPackageVisibility,
366    };
367
368    #[test]
369    fn validates_package_names() -> Result<(), GoPackageError> {
370        let name = GoPackageName::new("http")?;
371        assert_eq!(name.as_str(), "http");
372        assert_eq!(GoPackageName::new(""), Err(GoPackageError::EmptyName));
373        assert_eq!(
374            GoPackageName::new("net/http"),
375            Err(GoPackageError::InvalidName)
376        );
377        Ok(())
378    }
379
380    #[test]
381    fn validates_package_paths() -> Result<(), GoPackageError> {
382        let path = GoPackagePath::new("net/http")?;
383        assert_eq!(path.segments().collect::<Vec<_>>(), vec!["net", "http"]);
384        assert_eq!(GoPackagePath::new(""), Err(GoPackageError::EmptyPath));
385        assert_eq!(
386            GoPackagePath::new("net//http"),
387            Err(GoPackageError::EmptyPathSegment)
388        );
389        assert_eq!(
390            GoPackagePath::new("net/http server"),
391            Err(GoPackageError::InvalidPathSegment)
392        );
393        Ok(())
394    }
395
396    #[test]
397    fn stores_doc_names() -> Result<(), GoPackageError> {
398        let doc_name = GoPackageDocName::new("Package http")?;
399        assert_eq!(doc_name.to_string(), "Package http");
400        Ok(())
401    }
402
403    #[test]
404    fn parses_package_enums() -> Result<(), GoPackageError> {
405        assert_eq!(
406            "internal".parse::<GoPackageVisibility>()?,
407            GoPackageVisibility::Internal
408        );
409        assert_eq!(
410            "cmd package".parse::<GoPackageLayout>()?,
411            GoPackageLayout::CmdPackage
412        );
413        assert_eq!(
414            "build_tagged".parse::<GoFileKind>()?,
415            GoFileKind::BuildTagged
416        );
417        assert_eq!(GoFileKind::ModuleConfig.to_string(), "module-config");
418        Ok(())
419    }
420}