Skip to main content

hyperlane_cli/bump/
fn.rs

1use crate::*;
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/// Find version value position in a line
159///
160/// # Arguments
161///
162/// - `&str`: The line to search
163///
164/// # Returns
165///
166/// - `Option<(usize, usize)>`: Start and end positions of version string within quotes
167fn find_version_position(line: &str) -> Option<(usize, usize)> {
168    let trimmed: &str = line.trim();
169    if !trimmed.starts_with("version") || !trimmed.contains('=') {
170        return None;
171    }
172    let eq_pos: usize = line.find('=')?;
173    let after_eq: &str = &line[eq_pos + 1..];
174    let quote_start: usize = after_eq.find('"')?;
175    let after_first_quote: &str = &after_eq[quote_start + 1..];
176    let quote_end: usize = after_first_quote.find('"')?;
177    let version_start: usize = eq_pos + 1 + quote_start + 1;
178    let version_end: usize = version_start + quote_end;
179    Some((version_start, version_end))
180}
181
182/// Read and update version in Cargo.toml
183///
184/// # Arguments
185///
186/// - `&str`: Path to Cargo.toml file
187/// - `&BumpVersionType`: Type of version bump to apply
188///
189/// # Returns
190///
191/// - `Result<String, Box<dyn std::error::Error>>`: The new version string or an error
192pub async fn execute_bump(
193    manifest_path: &str,
194    bump_type: &BumpVersionType,
195) -> Result<String, Box<dyn std::error::Error>> {
196    let path: &Path = Path::new(manifest_path);
197    let content: String = read_to_string(path).await?;
198    let mut new_version: Option<String> = None;
199    let mut found_version: bool = false;
200    let mut updated_content: String = content.clone();
201    for line in content.lines() {
202        if found_version {
203            break;
204        }
205        if let Some((version_start, version_end)) = find_version_position(line) {
206            let version_str: &str = &line[version_start..version_end];
207            if let Some(version) = parse_version(version_str) {
208                let bumped: Version = bump_version(&version, bump_type);
209                let version_string: String = version_to_string(&bumped);
210                new_version = Some(version_string.clone());
211                let new_line: String = format!(
212                    "{}{version_string}{}",
213                    &line[..version_start],
214                    &line[version_end..]
215                );
216                updated_content = updated_content.replacen(line, &new_line, 1);
217                found_version = true;
218            }
219        }
220    }
221    if !found_version {
222        return Err("version field not found in Cargo.toml".into());
223    }
224    write(path, updated_content).await?;
225    match new_version {
226        Some(v) => Ok(v),
227        None => Err("failed to bump version".into()),
228    }
229}