gomod_parser/
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
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}