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#[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct GoImportPath(String);
36
37impl GoImportPath {
38 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 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59
60 #[must_use]
62 pub fn is_relative(&self) -> bool {
63 self.0.starts_with("./") || self.0.starts_with("../")
64 }
65
66 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct GoImportAlias(String);
104
105impl GoImportAlias {
106 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 #[must_use]
124 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127
128 #[must_use]
130 pub fn is_blank(&self) -> bool {
131 self.0 == "_"
132 }
133
134 #[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#[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 #[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#[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 #[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#[derive(Clone, Debug, Eq, PartialEq)]
268pub struct GoImportSpec {
269 path: GoImportPath,
270 alias: Option<GoImportAlias>,
271 kind: GoImportKind,
272}
273
274impl GoImportSpec {
275 #[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 #[must_use]
287 pub fn with_alias(mut self, alias: GoImportAlias) -> Self {
288 self.alias = Some(alias);
289 self
290 }
291
292 #[must_use]
294 pub const fn path(&self) -> &GoImportPath {
295 &self.path
296 }
297
298 #[must_use]
300 pub const fn alias(&self) -> Option<&GoImportAlias> {
301 self.alias.as_ref()
302 }
303
304 #[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}