Skip to main content

hyperlane_cli/bump/
fn.rs

1use super::*;
2
3/// Parse a version string into Version struct
4///
5/// # Arguments
6///
7/// - `&str`: The version string to parse (e.g., "0.1.0" or "0.1.0-alpha")
8///
9/// # Returns
10///
11/// - `Option<Version>`: Parsed version if successful, None otherwise
12fn parse_version(version_str: &str) -> Option<Version> {
13    let parts: Vec<&str> = version_str.split('-').collect();
14    let version_part: &str = parts.first()?;
15    let prerelease: Option<String> = parts.get(1).map(|s: &&str| s.to_string());
16    let nums: Vec<&str> = version_part.split('.').collect();
17    if nums.len() != 3 {
18        return None;
19    }
20    let major: u64 = nums.first()?.parse().ok()?;
21    let minor: u64 = nums.get(1)?.parse().ok()?;
22    let patch: u64 = nums.get(2)?.parse().ok()?;
23    Some(Version {
24        major,
25        minor,
26        patch,
27        prerelease,
28    })
29}
30
31/// Parse pre-release identifier to extract type and number
32///
33/// # Arguments
34///
35/// - `&str`: The pre-release string (e.g., "alpha", "alpha.1", "beta.2")
36///
37/// # Returns
38///
39/// - `Option<(&str, u64)>`: Tuple of (pre_release_type, number) if parsed successfully
40fn parse_prerelease(prerelease: &str) -> Option<(&str, u64)> {
41    let parts: Vec<&str> = prerelease.split('.').collect();
42    let pre_type: &str = parts.first()?;
43    let number: u64 = parts
44        .get(1)
45        .and_then(|s: &&str| s.parse().ok())
46        .unwrap_or(0);
47    Some((pre_type, number))
48}
49
50/// Get the next pre-release version string
51///
52/// # Arguments
53///
54/// - `Option<&String>`: Current pre-release identifier
55/// - `&str`: Target pre-release type ("alpha", "beta", "rc")
56///
57/// # Returns
58///
59/// - `String`: The new pre-release identifier
60fn get_next_prerelease(current: Option<&String>, target_type: &str) -> String {
61    match current {
62        Some(pre) => {
63            if let Some((pre_type, number)) = parse_prerelease(pre)
64                && pre_type == target_type
65                && number > 0
66            {
67                return format!("{}.{}", target_type, number + 1);
68            }
69            format!("{target_type}.1")
70        }
71        None => target_type.to_string(),
72    }
73}
74
75/// Convert Version back to string representation
76///
77/// # Arguments
78///
79/// - `&Version`: The Version struct to convert
80///
81/// # Returns
82///
83/// - `String`: Version string (e.g., "0.1.0" or "0.1.0-alpha")
84fn version_to_string(version: &Version) -> String {
85    let base: String = format!("{}.{}.{}", version.major, version.minor, version.patch);
86    match &version.prerelease {
87        Some(pre) => format!("{base}-{pre}"),
88        None => base,
89    }
90}
91
92/// Apply version bump according to the specified type
93///
94/// # Arguments
95///
96/// - `&Version`: The current version
97/// - `&BumpVersionType`: The type of version bump to apply
98///
99/// # Returns
100///
101/// - `Version`: The new version after bumping
102fn bump_version(version: &Version, bump_type: &BumpVersionType) -> Version {
103    match bump_type {
104        BumpVersionType::Patch => Version {
105            major: version.major,
106            minor: version.minor,
107            patch: version.patch + 1,
108            prerelease: None,
109        },
110        BumpVersionType::Minor => Version {
111            major: version.major,
112            minor: version.minor + 1,
113            patch: 0,
114            prerelease: None,
115        },
116        BumpVersionType::Major => Version {
117            major: version.major + 1,
118            minor: 0,
119            patch: 0,
120            prerelease: None,
121        },
122        BumpVersionType::Release => Version {
123            major: version.major,
124            minor: version.minor,
125            patch: version.patch,
126            prerelease: None,
127        },
128        BumpVersionType::Alpha => {
129            let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "alpha");
130            Version {
131                major: version.major,
132                minor: version.minor,
133                patch: version.patch,
134                prerelease: Some(prerelease),
135            }
136        }
137        BumpVersionType::Beta => {
138            let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "beta");
139            Version {
140                major: version.major,
141                minor: version.minor,
142                patch: version.patch,
143                prerelease: Some(prerelease),
144            }
145        }
146        BumpVersionType::Rc => {
147            let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "rc");
148            Version {
149                major: version.major,
150                minor: version.minor,
151                patch: version.patch,
152                prerelease: Some(prerelease),
153            }
154        }
155    }
156}
157
158/// Read and update version in Cargo.toml
159///
160/// The edit is applied on a `toml_edit::DocumentMut`, so comments, key
161/// order and inline-table formatting outside the version value are
162/// preserved byte-for-byte.
163///
164/// # Arguments
165///
166/// - `&str`: Path to Cargo.toml file
167/// - `&BumpVersionType`: Type of version bump to apply
168///
169/// # Returns
170///
171/// - `Result<String, Box<dyn std::error::Error>>`: The new version string or an error
172pub async fn execute_bump(
173    manifest_path: &str,
174    bump_type: &BumpVersionType,
175) -> Result<String, Box<dyn std::error::Error>> {
176    let path: &Path = Path::new(manifest_path);
177    let content: String = read_to_string(path).await?;
178    let mut doc: DocumentMut = content
179        .parse()
180        .map_err(|e: TomlError| format!("failed to parse {}: {}", manifest_path, e))?;
181    let version_slot: &mut Item = if doc
182        .get("workspace")
183        .and_then(|workspace: &Item| workspace.get("package"))
184        .is_some()
185    {
186        doc.get_mut("workspace")
187            .and_then(|workspace: &mut Item| workspace.get_mut("package"))
188            .and_then(|package: &mut Item| package.get_mut("version"))
189            .ok_or_else(|| -> Box<dyn std::error::Error> {
190                "workspace.package.version not found".into()
191            })?
192    } else if doc.get("package").is_some() {
193        doc.get_mut("package")
194            .and_then(|package: &mut Item| package.get_mut("version"))
195            .ok_or_else(|| -> Box<dyn std::error::Error> { "package.version not found".into() })?
196    } else {
197        return Err("neither [package] nor [workspace.package] found in Cargo.toml".into());
198    };
199    let version_str: String = version_slot
200        .as_str()
201        .ok_or_else(|| -> Box<dyn std::error::Error> { "version field is not a string".into() })?
202        .to_string();
203    let version: Version =
204        parse_version(&version_str).ok_or_else(|| -> Box<dyn std::error::Error> {
205            format!("failed to parse version: {}", version_str).into()
206        })?;
207    let bumped: Version = bump_version(&version, bump_type);
208    let version_string: String = version_to_string(&bumped);
209    set_item_string_preserving_decor(version_slot, &version_string);
210    write(path, doc.to_string()).await?;
211    Ok(version_string)
212}