Skip to main content

use_go_import/
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 import metadata constructors.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum GoImportError {
12    EmptyPath,
13    InvalidPath,
14    EmptyAlias,
15    InvalidAlias,
16    UnknownLabel,
17}
18
19impl fmt::Display for GoImportError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::EmptyPath => formatter.write_str("Go import path cannot be empty"),
23            Self::InvalidPath => formatter.write_str("invalid Go import path"),
24            Self::EmptyAlias => formatter.write_str("Go import alias cannot be empty"),
25            Self::InvalidAlias => formatter.write_str("invalid Go import alias"),
26            Self::UnknownLabel => formatter.write_str("unknown Go import metadata label"),
27        }
28    }
29}
30
31impl Error for GoImportError {}
32
33/// Validated Go import path metadata.
34#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct GoImportPath(String);
36
37impl GoImportPath {
38    /// Creates a Go import path from non-empty text.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`GoImportError`] when the path is empty or contains obvious whitespace.
43    pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
44        let trimmed = value.as_ref().trim();
45        if trimmed.is_empty() {
46            return Err(GoImportError::EmptyPath);
47        }
48        if trimmed.chars().any(char::is_whitespace) || trimmed.split('/').any(str::is_empty) {
49            return Err(GoImportError::InvalidPath);
50        }
51        Ok(Self(trimmed.to_string()))
52    }
53
54    /// Returns the import path.
55    #[must_use]
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59
60    /// Returns whether this import path is relative.
61    #[must_use]
62    pub fn is_relative(&self) -> bool {
63        self.0.starts_with("./") || self.0.starts_with("../")
64    }
65
66    /// Consumes the path and returns the owned text.
67    #[must_use]
68    pub fn into_string(self) -> String {
69        self.0
70    }
71}
72
73impl AsRef<str> for GoImportPath {
74    fn as_ref(&self) -> &str {
75        self.as_str()
76    }
77}
78
79impl fmt::Display for GoImportPath {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter.write_str(self.as_str())
82    }
83}
84
85impl FromStr for GoImportPath {
86    type Err = GoImportError;
87
88    fn from_str(value: &str) -> Result<Self, Self::Err> {
89        Self::new(value)
90    }
91}
92
93impl TryFrom<&str> for GoImportPath {
94    type Error = GoImportError;
95
96    fn try_from(value: &str) -> Result<Self, Self::Error> {
97        Self::new(value)
98    }
99}
100
101/// Validated Go import alias metadata.
102#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct GoImportAlias(String);
104
105impl GoImportAlias {
106    /// Creates an import alias from `_`, `.`, or an ASCII Go identifier.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`GoImportError`] when the alias is empty or invalid.
111    pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
112        let trimmed = value.as_ref().trim();
113        if trimmed.is_empty() {
114            return Err(GoImportError::EmptyAlias);
115        }
116        if trimmed != "_" && trimmed != "." && !is_valid_ascii_go_identifier(trimmed) {
117            return Err(GoImportError::InvalidAlias);
118        }
119        Ok(Self(trimmed.to_string()))
120    }
121
122    /// Returns the alias text.
123    #[must_use]
124    pub fn as_str(&self) -> &str {
125        &self.0
126    }
127
128    /// Returns whether this is the blank import alias.
129    #[must_use]
130    pub fn is_blank(&self) -> bool {
131        self.0 == "_"
132    }
133
134    /// Returns whether this is the dot import alias.
135    #[must_use]
136    pub fn is_dot(&self) -> bool {
137        self.0 == "."
138    }
139}
140
141impl AsRef<str> for GoImportAlias {
142    fn as_ref(&self) -> &str {
143        self.as_str()
144    }
145}
146
147impl fmt::Display for GoImportAlias {
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        formatter.write_str(self.as_str())
150    }
151}
152
153impl FromStr for GoImportAlias {
154    type Err = GoImportError;
155
156    fn from_str(value: &str) -> Result<Self, Self::Err> {
157        Self::new(value)
158    }
159}
160
161impl TryFrom<&str> for GoImportAlias {
162    type Error = GoImportError;
163
164    fn try_from(value: &str) -> Result<Self, Self::Error> {
165        Self::new(value)
166    }
167}
168
169/// Go import kind metadata.
170#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub enum GoImportKind {
172    StandardLibrary,
173    ThirdParty,
174    Internal,
175    Relative,
176    Blank,
177    Dot,
178    Aliased,
179}
180
181impl GoImportKind {
182    /// Returns the import kind label.
183    #[must_use]
184    pub const fn as_str(self) -> &'static str {
185        match self {
186            Self::StandardLibrary => "standard-library",
187            Self::ThirdParty => "third-party",
188            Self::Internal => "internal",
189            Self::Relative => "relative",
190            Self::Blank => "blank",
191            Self::Dot => "dot",
192            Self::Aliased => "aliased",
193        }
194    }
195}
196
197impl fmt::Display for GoImportKind {
198    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199        formatter.write_str(self.as_str())
200    }
201}
202
203impl FromStr for GoImportKind {
204    type Err = GoImportError;
205
206    fn from_str(value: &str) -> Result<Self, Self::Err> {
207        match normalized_label(value)?.as_str() {
208            "standard-library" | "standard_library" | "standard library" => {
209                Ok(Self::StandardLibrary)
210            }
211            "third-party" | "third_party" | "third party" => Ok(Self::ThirdParty),
212            "internal" => Ok(Self::Internal),
213            "relative" => Ok(Self::Relative),
214            "blank" => Ok(Self::Blank),
215            "dot" => Ok(Self::Dot),
216            "aliased" => Ok(Self::Aliased),
217            _ => Err(GoImportError::UnknownLabel),
218        }
219    }
220}
221
222/// Go import group metadata.
223#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224pub enum GoImportGroup {
225    StandardLibrary,
226    External,
227    Internal,
228    Local,
229}
230
231impl GoImportGroup {
232    /// Returns the import group label.
233    #[must_use]
234    pub const fn as_str(self) -> &'static str {
235        match self {
236            Self::StandardLibrary => "standard-library",
237            Self::External => "external",
238            Self::Internal => "internal",
239            Self::Local => "local",
240        }
241    }
242}
243
244impl fmt::Display for GoImportGroup {
245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246        formatter.write_str(self.as_str())
247    }
248}
249
250impl FromStr for GoImportGroup {
251    type Err = GoImportError;
252
253    fn from_str(value: &str) -> Result<Self, Self::Err> {
254        match normalized_label(value)?.as_str() {
255            "standard-library" | "standard_library" | "standard library" => {
256                Ok(Self::StandardLibrary)
257            }
258            "external" => Ok(Self::External),
259            "internal" => Ok(Self::Internal),
260            "local" => Ok(Self::Local),
261            _ => Err(GoImportError::UnknownLabel),
262        }
263    }
264}
265
266/// Go import specification metadata.
267#[derive(Clone, Debug, Eq, PartialEq)]
268pub struct GoImportSpec {
269    path: GoImportPath,
270    alias: Option<GoImportAlias>,
271    kind: GoImportKind,
272}
273
274impl GoImportSpec {
275    /// Creates import specification metadata.
276    #[must_use]
277    pub const fn new(path: GoImportPath, kind: GoImportKind) -> Self {
278        Self {
279            path,
280            alias: None,
281            kind,
282        }
283    }
284
285    /// Adds an import alias.
286    #[must_use]
287    pub fn with_alias(mut self, alias: GoImportAlias) -> Self {
288        self.alias = Some(alias);
289        self
290    }
291
292    /// Returns the import path.
293    #[must_use]
294    pub const fn path(&self) -> &GoImportPath {
295        &self.path
296    }
297
298    /// Returns the import alias.
299    #[must_use]
300    pub const fn alias(&self) -> Option<&GoImportAlias> {
301        self.alias.as_ref()
302    }
303
304    /// Returns the import kind.
305    #[must_use]
306    pub const fn kind(&self) -> GoImportKind {
307        self.kind
308    }
309}
310
311fn normalized_label(value: &str) -> Result<String, GoImportError> {
312    let trimmed = value.trim();
313    if trimmed.is_empty() {
314        Err(GoImportError::UnknownLabel)
315    } else {
316        Ok(trimmed.to_ascii_lowercase())
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::{
323        GoImportAlias, GoImportError, GoImportGroup, GoImportKind, GoImportPath, GoImportSpec,
324    };
325
326    #[test]
327    fn validates_import_paths() -> Result<(), GoImportError> {
328        let path = GoImportPath::new("net/http")?;
329        assert_eq!(path.as_str(), "net/http");
330        assert!(!path.is_relative());
331        assert!(GoImportPath::new("../internal").is_ok_and(|value| value.is_relative()));
332        assert_eq!(GoImportPath::new(""), Err(GoImportError::EmptyPath));
333        assert_eq!(
334            GoImportPath::new("net//http"),
335            Err(GoImportError::InvalidPath)
336        );
337        assert_eq!(
338            GoImportPath::new("net/http client"),
339            Err(GoImportError::InvalidPath)
340        );
341        Ok(())
342    }
343
344    #[test]
345    fn validates_import_aliases() -> Result<(), GoImportError> {
346        let blank = GoImportAlias::new("_")?;
347        let dot = GoImportAlias::new(".")?;
348        let named = GoImportAlias::new("httpx")?;
349
350        assert!(blank.is_blank());
351        assert!(dot.is_dot());
352        assert_eq!(named.as_str(), "httpx");
353        assert_eq!(GoImportAlias::new(""), Err(GoImportError::EmptyAlias));
354        assert_eq!(
355            GoImportAlias::new("bad-alias"),
356            Err(GoImportError::InvalidAlias)
357        );
358        Ok(())
359    }
360
361    #[test]
362    fn parses_import_enums() -> Result<(), GoImportError> {
363        assert_eq!(
364            "third party".parse::<GoImportKind>()?,
365            GoImportKind::ThirdParty
366        );
367        assert_eq!(
368            "standard_library".parse::<GoImportGroup>()?,
369            GoImportGroup::StandardLibrary
370        );
371        assert_eq!(GoImportKind::Aliased.to_string(), "aliased");
372        Ok(())
373    }
374
375    #[test]
376    fn models_import_specs() -> Result<(), GoImportError> {
377        let spec = GoImportSpec::new(GoImportPath::new("net/http")?, GoImportKind::Aliased)
378            .with_alias(GoImportAlias::new("httpx")?);
379
380        assert_eq!(spec.path().as_str(), "net/http");
381        assert_eq!(spec.kind(), GoImportKind::Aliased);
382        assert_eq!(spec.alias().map(GoImportAlias::as_str), Some("httpx"));
383        Ok(())
384    }
385}