gomod_parser2/
lib.rs

1//! A simple `go.mod` file parser
2//!
3//! # Example
4//!
5//! ```rust
6//! use gomod_parser::{GoMod, Module, ModuleDependency};
7//! use std::str::FromStr;
8//!
9//! let input = r#"
10//! module github.com/example
11//!
12//! go 1.21
13//!
14//! require golang.org/x/net v0.20.0
15//! "#;
16//!
17//! let go_mod = GoMod::from_str(input).unwrap();
18//!
19//! assert_eq!(go_mod.module, "github.com/example".to_string());
20//! assert_eq!(go_mod.go, Some("1.21".to_string()));
21//! assert_eq!(
22//!     go_mod.require,
23//!     vec![ModuleDependency {
24//!         module: Module {
25//!             module_path: "golang.org/x/net".to_string(),
26//!             version: "v0.20.0".to_string()
27//!         },
28//!         indirect: false
29//!     }]
30//! );
31//! ```
32
33#![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}