1#![warn(clippy::pedantic)]
34#![warn(clippy::nursery)]
35#![warn(clippy::cargo)]
36
37use crate::parser::{gomod, Directive};
38use std::collections::HashMap;
39use winnow::Parser;
40
41mod combinator;
42pub mod parser;
43
44#[derive(Debug, Default, PartialEq, Eq)]
45pub struct GoMod {
46 pub comment: Vec<String>,
47 pub module: String,
48 pub go: Option<String>,
49 pub godebug: HashMap<String, String>,
50 pub tool: Vec<String>,
51 pub toolchain: Option<String>,
52 pub require: Vec<ModuleDependency>,
53 pub exclude: Vec<ModuleDependency>,
54 pub replace: Vec<ModuleReplacement>,
55 pub retract: Vec<ModuleRetract>,
56}
57
58impl std::str::FromStr for GoMod {
59 type Err = String;
60
61 fn from_str(input: &str) -> Result<Self, Self::Err> {
62 let mut res = Self::default();
63
64 for directive in &mut gomod.parse(input).map_err(|e| e.to_string())? {
65 match directive {
66 Directive::Comment(d) => res.comment.push((**d).to_string()),
67 Directive::Module(d) => res.module = (**d).to_string(),
68 Directive::Go(d) => res.go = Some((**d).to_string()),
69 Directive::GoDebug(d) => res.godebug.extend((*d).clone()),
70 Directive::Tool(d) => res.tool.append(d),
71 Directive::Toolchain(d) => res.toolchain = Some((**d).to_string()),
72 Directive::Require(d) => res.require.append(d),
73 Directive::Exclude(d) => res.exclude.append(d),
74 Directive::Replace(d) => res.replace.append(d),
75 Directive::Retract(d) => res.retract.append(d),
76 }
77 }
78
79 Ok(res)
80 }
81}
82
83#[derive(Debug, PartialEq, Eq)]
84pub struct Module {
85 pub module_path: String,
86 pub version: String,
87}
88
89#[derive(Debug, PartialEq, Eq)]
90pub struct ModuleDependency {
91 pub module: Module,
92 pub indirect: bool,
93}
94
95#[derive(Debug, PartialEq, Eq)]
96pub struct ModuleReplacement {
97 pub module_path: String,
98 pub version: Option<String>,
99 pub replacement: Replacement,
100}
101
102#[derive(Debug, PartialEq, Eq)]
103pub enum Replacement {
104 FilePath(String),
105 Module(Module),
106}
107
108#[derive(Debug, PartialEq, Eq)]
109pub enum ModuleRetract {
110 Single(String),
111 Range(String, String),
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use indoc::indoc;
118 use std::str::FromStr;
119
120 #[test]
121 fn test_parse_complete() {
122 let input = indoc! {r#"
123 // Complete example
124
125 module github.com/complete
126
127 go 1.21
128
129 toolchain go1.21.1
130
131 require golang.org/x/net v0.20.0
132
133 exclude golang.org/x/net v0.19.1
134
135 replace golang.org/x/net v0.19.0 => example.com/fork/net v0.19.1
136
137 retract v1.0.0
138 "#};
139
140 let go_mod = GoMod::from_str(input).unwrap();
141
142 assert_eq!(go_mod.module, "github.com/complete".to_string());
143 assert_eq!(go_mod.go, Some("1.21".to_string()));
144 assert_eq!(go_mod.toolchain, Some("go1.21.1".to_string()));
145 assert_eq!(
146 go_mod.require,
147 vec![ModuleDependency {
148 module: Module {
149 module_path: "golang.org/x/net".to_string(),
150 version: "v0.20.0".to_string()
151 },
152 indirect: false
153 }]
154 );
155 assert_eq!(
156 go_mod.exclude,
157 vec![ModuleDependency {
158 module: Module {
159 module_path: "golang.org/x/net".to_string(),
160 version: "v0.19.1".to_string()
161 },
162 indirect: false
163 }]
164 );
165 assert_eq!(
166 go_mod.replace,
167 vec![ModuleReplacement {
168 module_path: "golang.org/x/net".to_string(),
169 version: Some("v0.19.0".to_string()),
170 replacement: Replacement::Module(Module {
171 module_path: "example.com/fork/net".to_string(),
172 version: "v0.19.1".to_string(),
173 })
174 }]
175 );
176 assert_eq!(
177 go_mod.retract,
178 vec![ModuleRetract::Single("v1.0.0".to_string())]
179 );
180 assert_eq!(go_mod.comment, vec!["Complete example".to_string()]);
181 }
182
183 #[test]
184 fn test_invalid_content() {
185 let input = indoc! {r#"
186 modulegithub.com/no-space
187 "#};
188
189 let go_mod = GoMod::from_str(input);
190
191 assert!(go_mod.is_err());
192 }
193
194 #[test]
195 fn test_no_line_ending() {
196 let input = indoc! {r#"
197 module github.com/no-line-ending
198
199 require (
200 golang.org/x/net v0.20.0
201 )"#};
202
203 let go_mod = GoMod::from_str(input).unwrap();
204
205 assert_eq!(go_mod.module, "github.com/no-line-ending".to_string());
206 assert_eq!(
207 go_mod.require,
208 vec![ModuleDependency {
209 module: Module {
210 module_path: "golang.org/x/net".to_string(),
211 version: "v0.20.0".to_string()
212 },
213 indirect: false
214 }]
215 );
216 }
217}