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