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