Skip to content

Commit 01c318f

Browse files
authored
Merge of #1966
2 parents de6453d + 618c897 commit 01c318f

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type: Added
2+
title: Add list and string helper functions to `pulumi_gestalt_rust::stdlib`
3+
description: |
4+
- `element`: Returns an item by index with clear errors for negative and out-of-bounds indices.
5+
- `join`: Joins a list of strings with a separator.
6+
- `length`: Returns list length as `i64`.
7+
- `split`: Splits text by a separator into `Vec<String>`.
8+
- `single_or_none`: Returns `None` for empty lists, `Some` for single-item lists, and an error for larger lists.

crates/rust/src/stdlib.rs

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{Context, Result, anyhow};
1+
use anyhow::{Context, Result, anyhow, bail};
22
use base64::Engine;
33
use base64::engine::general_purpose::STANDARD;
44

@@ -28,9 +28,59 @@ pub fn from_base64(input: impl AsRef<str>) -> Result<String> {
2828
)
2929
}
3030

31+
pub fn element<T: Clone, I: TryInto<i64>>(list: impl AsRef<[T]>, index: I) -> Result<T> {
32+
let index = index
33+
.try_into()
34+
.map_err(|_| anyhow!("Failed to convert list index to i64"))?;
35+
if index < 0 {
36+
bail!("List index cannot be negative: {index}");
37+
}
38+
let index = usize::try_from(index).context("Failed to convert list index to usize")?;
39+
let list_ref = list.as_ref();
40+
list_ref.get(index).cloned().ok_or_else(|| {
41+
anyhow!(
42+
"List index {index} is out of bounds for length {}",
43+
list_ref.len()
44+
)
45+
})
46+
}
47+
48+
pub fn join<T: AsRef<str>>(separator: impl AsRef<str>, list: impl AsRef<[T]>) -> String {
49+
list.as_ref()
50+
.iter()
51+
.map(AsRef::as_ref)
52+
.collect::<Vec<_>>()
53+
.join(separator.as_ref())
54+
}
55+
56+
pub fn length<T>(list: impl AsRef<[T]>) -> i64 {
57+
i64::try_from(list.as_ref().len()).expect("List length exceeds i64::MAX")
58+
}
59+
60+
pub fn split(separator: impl AsRef<str>, text: impl AsRef<str>) -> Vec<String> {
61+
text.as_ref()
62+
.split(separator.as_ref())
63+
.map(ToOwned::to_owned)
64+
.collect()
65+
}
66+
67+
pub fn single_or_none<T: Clone>(list: impl AsRef<[T]>) -> Result<Option<T>> {
68+
let list_ref = list.as_ref();
69+
if list_ref.is_empty() {
70+
return Ok(None);
71+
}
72+
if list_ref.len() != 1 {
73+
bail!(
74+
"singleOrNone expected input list to have at most one element, got {}",
75+
list_ref.len()
76+
);
77+
}
78+
Ok(Some(list_ref[0].clone()))
79+
}
80+
3181
#[cfg(test)]
3282
mod tests {
33-
use super::{cwd, from_base64, to_base64};
83+
use super::{cwd, element, from_base64, join, length, single_or_none, split, to_base64};
3484

3585
#[test]
3686
fn to_base64_encodes_known_text() {
@@ -92,4 +142,60 @@ mod tests {
92142
assert_eq!(from_str, "YWJj");
93143
assert_eq!(from_str, from_bytes);
94144
}
145+
146+
#[test]
147+
fn element_returns_item_for_valid_index() {
148+
let values = vec!["a".to_string(), "b".to_string(), "c".to_string()];
149+
assert_eq!(element(&values, 1).unwrap(), "b");
150+
}
151+
152+
#[test]
153+
fn element_returns_error_for_negative_index() {
154+
let values = vec!["a", "b"];
155+
let error = element(&values, -1).unwrap_err().to_string();
156+
assert!(error.contains("cannot be negative"));
157+
}
158+
159+
#[test]
160+
fn element_returns_error_for_out_of_bounds_index() {
161+
let values = vec!["a", "b"];
162+
let error = element(&values, 2).unwrap_err().to_string();
163+
assert!(error.contains("out of bounds"));
164+
}
165+
166+
#[test]
167+
fn join_joins_values_with_separator() {
168+
let values = vec!["a".to_string(), "b".to_string(), "c".to_string()];
169+
assert_eq!(join("|", &values), "a|b|c");
170+
}
171+
172+
#[test]
173+
fn length_returns_i64_length() {
174+
let values = vec!["x", "y", "z"];
175+
assert_eq!(length(&values), 3_i64);
176+
}
177+
178+
#[test]
179+
fn split_returns_segments() {
180+
assert_eq!(split("-", "a-b-c"), vec!["a", "b", "c"]);
181+
}
182+
183+
#[test]
184+
fn single_or_none_returns_none_for_empty_list() {
185+
let values: Vec<String> = vec![];
186+
assert_eq!(single_or_none(&values).unwrap(), None);
187+
}
188+
189+
#[test]
190+
fn single_or_none_returns_some_for_single_item_list() {
191+
let values = vec!["only".to_string()];
192+
assert_eq!(single_or_none(&values).unwrap(), Some("only".to_string()));
193+
}
194+
195+
#[test]
196+
fn single_or_none_returns_error_for_multi_item_list() {
197+
let values = vec!["a", "b"];
198+
let error = single_or_none(&values).unwrap_err().to_string();
199+
assert!(error.contains("at most one element"));
200+
}
95201
}

0 commit comments

Comments
 (0)