use crate::build_spec::{DepMode, ModuleSpec};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReplaceDirective {
pub old_path: String,
pub old_version: Option<String>,
pub new_path: String,
pub new_version: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GoMod {
pub module_path: String,
pub go_version: String,
pub toolchain: Option<String>,
pub requires: Vec<(String, String)>,
pub replaces: Vec<ReplaceDirective>,
}
impl GoMod {
#[must_use]
pub fn parse(text: &str) -> Self {
let mut m = GoMod::default();
let mut block: Option<Block> = None;
for raw in text.lines() {
let line = strip_comment(raw).trim();
if line.is_empty() {
continue;
}
if let Some(_b) = block {
if line == ")" {
block = None;
continue;
}
}
match block {
Some(Block::Require) => {
if let Some((p, v)) = parse_require_entry(line) {
m.requires.push((p, v));
}
continue;
}
Some(Block::Replace) => {
if let Some(r) = parse_replace_entry(line) {
m.replaces.push(r);
}
continue;
}
None => {}
}
if let Some(rest) = line.strip_prefix("module ") {
m.module_path = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("go ") {
m.go_version = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("toolchain ") {
m.toolchain = Some(rest.trim().to_string());
} else if line == "require (" || line == "require(" {
block = Some(Block::Require);
} else if let Some(rest) = line.strip_prefix("require ") {
if let Some((p, v)) = parse_require_entry(rest.trim()) {
m.requires.push((p, v));
}
} else if line == "replace (" || line == "replace(" {
block = Some(Block::Replace);
} else if let Some(rest) = line.strip_prefix("replace ") {
if let Some(r) = parse_replace_entry(rest.trim()) {
m.replaces.push(r);
}
}
}
m
}
#[must_use]
pub fn to_module_spec(&self, dep_mode: DepMode, vendor_hash: Option<String>) -> ModuleSpec {
ModuleSpec {
module_path: self.module_path.clone(),
go_version: self.go_version.clone(),
toolchain: self.toolchain.clone(),
has_external_deps: !self.requires.is_empty(),
dep_mode,
vendor_hash,
}
}
}
#[derive(Clone, Copy)]
enum Block {
Require,
Replace,
}
fn strip_comment(line: &str) -> &str {
match line.find("//") {
Some(i) => &line[..i],
None => line,
}
}
fn parse_require_entry(s: &str) -> Option<(String, String)> {
let mut it = s.split_whitespace();
let path = it.next()?.to_string();
let version = it.next()?.to_string();
Some((path, version))
}
fn parse_replace_entry(s: &str) -> Option<ReplaceDirective> {
let (lhs, rhs) = s.split_once("=>")?;
let (old_path, old_version) = split_path_version(lhs.trim());
let (new_path, new_version) = split_path_version(rhs.trim());
if old_path.is_empty() || new_path.is_empty() {
return None;
}
Some(ReplaceDirective { old_path, old_version, new_path, new_version })
}
fn split_path_version(s: &str) -> (String, Option<String>) {
let mut it = s.split_whitespace();
let path = it.next().unwrap_or_default().to_string();
let version = it.next().map(str::to_string);
(path, version)
}
#[cfg(test)]
mod tests {
use super::*;
const AKEYLESS_SHAPED: &str = r#"
module akeyless.io/akeyless-main-repo
go 1.26
toolchain go1.26.4
require (
github.com/foo/bar v1.2.3
github.com/baz/qux v0.4.0 // indirect
)
require github.com/single/dep v2.0.0
replace github.com/akeylesslabs/akeyless-go/v3 => ./go/src/client/sdktest/akeyless-go
replace (
github.com/old/mod v1.0.0 => github.com/new/mod v1.1.0
)
"#;
#[test]
fn parses_akeyless_shaped_go_mod() {
let m = GoMod::parse(AKEYLESS_SHAPED);
assert_eq!(m.module_path, "akeyless.io/akeyless-main-repo");
assert_eq!(m.go_version, "1.26");
assert_eq!(m.toolchain.as_deref(), Some("go1.26.4"));
assert_eq!(m.requires.len(), 3);
assert!(m.requires.contains(&("github.com/single/dep".into(), "v2.0.0".into())));
let fs_replace = m
.replaces
.iter()
.find(|r| r.old_path == "github.com/akeylesslabs/akeyless-go/v3")
.expect("in-tree replace present");
assert_eq!(fs_replace.new_path, "./go/src/client/sdktest/akeyless-go");
assert_eq!(fs_replace.new_version, None);
let mod_replace = m.replaces.iter().find(|r| r.old_path == "github.com/old/mod").unwrap();
assert_eq!(mod_replace.old_version.as_deref(), Some("v1.0.0"));
assert_eq!(mod_replace.new_version.as_deref(), Some("v1.1.0"));
}
#[test]
fn dep_free_module_has_no_external_deps() {
let m = GoMod::parse("module example.com/x\n\ngo 1.25\n");
assert!(m.requires.is_empty());
let spec = m.to_module_spec(DepMode::Vendored, None);
assert!(!spec.has_external_deps);
assert_eq!(spec.module_path, "example.com/x");
assert_eq!(spec.go_version, "1.25");
assert!(spec.toolchain.is_none());
}
#[test]
fn to_module_spec_carries_dep_mode_and_require_presence() {
let m = GoMod::parse(AKEYLESS_SHAPED);
let spec = m.to_module_spec(DepMode::Vendored, None);
assert!(spec.has_external_deps);
assert!(spec.dep_mode.is_vendored());
assert!(spec.vendor_hash.is_none());
}
}