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 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct GoPackageName(String);
40
41impl GoPackageName {
42 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 #[must_use]
60 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63
64 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct GoPackagePath(String);
102
103impl GoPackagePath {
104 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 #[must_use]
117 pub fn as_str(&self) -> &str {
118 &self.0
119 }
120
121 #[must_use]
123 pub fn into_string(self) -> String {
124 self.0
125 }
126
127 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub struct GoPackageDocName(String);
164
165impl GoPackageDocName {
166 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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
203pub enum GoPackageVisibility {
204 Internal,
205 Public,
206}
207
208impl GoPackageVisibility {
209 #[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#[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 #[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#[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 #[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}