1use base64::Engine as _;
6use minijinja::{Error, ErrorKind, Value, value::ValueKind};
7use semver::{Version, VersionReq};
8
9pub fn toyaml(value: Value) -> Result<String, Error> {
13 let json_value: serde_json::Value = serde_json::to_value(&value)
15 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
16
17 let yaml = serde_yaml::to_string(&json_value)
19 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
20
21 let yaml = yaml.trim_start_matches("---\n").trim_end();
23
24 Ok(yaml.to_string())
25}
26
27pub fn tojson(value: Value) -> Result<String, Error> {
31 let json_value: serde_json::Value = serde_json::to_value(&value)
32 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
33
34 serde_json::to_string(&json_value)
35 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))
36}
37
38pub fn tojson_pretty(value: Value) -> Result<String, Error> {
42 let json_value: serde_json::Value = serde_json::to_value(&value)
43 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
44
45 serde_json::to_string_pretty(&json_value)
46 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))
47}
48
49#[must_use]
53pub fn b64encode(value: String) -> String {
54 base64::engine::general_purpose::STANDARD.encode(value.as_bytes())
55}
56
57pub fn b64decode(value: String) -> Result<String, Error> {
61 let decoded = base64::engine::general_purpose::STANDARD
62 .decode(value.as_bytes())
63 .map_err(|e| {
64 Error::new(
65 ErrorKind::InvalidOperation,
66 format!("base64 decode error: {}", e),
67 )
68 })?;
69
70 String::from_utf8(decoded).map_err(|e| {
71 Error::new(
72 ErrorKind::InvalidOperation,
73 format!("UTF-8 decode error: {}", e),
74 )
75 })
76}
77
78#[must_use]
82pub fn quote(value: Value) -> String {
83 let s = if let Some(str_val) = value.as_str() {
84 str_val.to_string()
85 } else {
86 value.to_string()
87 };
88 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
89}
90
91#[must_use]
95pub fn squote(value: Value) -> String {
96 let s = if let Some(str_val) = value.as_str() {
97 str_val.to_string()
98 } else {
99 value.to_string()
100 };
101 format!("'{}'", s.replace('\'', "''"))
102}
103
104#[must_use]
108pub fn nindent(value: String, spaces: usize) -> String {
109 let line_count = value.lines().count();
110 let mut result = String::with_capacity(1 + value.len() + spaces * line_count + line_count);
112 result.push('\n');
113
114 let indent = " ".repeat(spaces);
115 let mut first = true;
116
117 for line in value.lines() {
118 if !first {
119 result.push('\n');
120 }
121 first = false;
122
123 if !line.is_empty() {
124 result.push_str(&indent);
125 result.push_str(line);
126 }
127 }
128
129 result
130}
131
132pub fn indent(value: String, spaces: usize) -> String {
136 let line_count = value.lines().count();
137 let mut result = String::with_capacity(value.len() + spaces * line_count + line_count);
139
140 let indent_str = " ".repeat(spaces);
141 let mut first = true;
142
143 for line in value.lines() {
144 if !first {
145 result.push('\n');
146 }
147 first = false;
148
149 if !line.is_empty() {
150 result.push_str(&indent_str);
151 }
152 result.push_str(line);
153 }
154
155 result
156}
157
158pub fn required(value: Value, message: Option<String>) -> Result<Value, Error> {
162 if value.is_undefined() || value.is_none() {
163 let msg = message.unwrap_or_else(|| "required value is missing".to_string());
164 Err(Error::new(ErrorKind::InvalidOperation, msg))
165 } else if let Some(s) = value.as_str() {
166 if s.is_empty() {
167 let msg = message.unwrap_or_else(|| "required value is empty".to_string());
168 Err(Error::new(ErrorKind::InvalidOperation, msg))
169 } else {
170 Ok(value)
171 }
172 } else {
173 Ok(value)
174 }
175}
176
177pub fn empty(value: Value) -> bool {
181 if value.is_undefined() || value.is_none() {
182 return true;
183 }
184
185 match value.len() {
186 Some(len) => len == 0,
187 None => {
188 if let Some(s) = value.as_str() {
189 s.is_empty()
190 } else {
191 false
192 }
193 }
194 }
195}
196
197pub fn coalesce(args: Vec<Value>) -> Value {
201 for arg in args {
202 if !arg.is_undefined() && !arg.is_none() {
203 if let Some(s) = arg.as_str() {
204 if !s.is_empty() {
205 return arg;
206 }
207 } else {
208 return arg;
209 }
210 }
211 }
212 Value::UNDEFINED
213}
214
215pub fn haskey(value: Value, key: String) -> bool {
219 value
220 .get_attr(&key)
221 .map(|v| !v.is_undefined())
222 .unwrap_or(false)
223}
224
225pub fn keys(value: Value) -> Result<Vec<String>, Error> {
229 match value.try_iter() {
230 Ok(iter) => {
231 let keys: Vec<String> = iter
232 .filter_map(|v| v.as_str().map(|s| s.to_string()))
233 .collect();
234 Ok(keys)
235 }
236 Err(_) => Err(Error::new(
237 ErrorKind::InvalidOperation,
238 "cannot get keys from non-mapping value",
239 )),
240 }
241}
242
243pub fn merge(base: Value, overlay: Value) -> Result<Value, Error> {
247 let mut base_json: serde_json::Value = serde_json::to_value(&base)
248 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
249 let overlay_json: serde_json::Value = serde_json::to_value(&overlay)
250 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
251
252 deep_merge_json(&mut base_json, &overlay_json);
253
254 Ok(Value::from_serialize(&base_json))
255}
256
257fn deep_merge_json(base: &mut serde_json::Value, overlay: &serde_json::Value) {
258 match (base, overlay) {
259 (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
260 for (key, overlay_value) in overlay_map {
261 match base_map.get_mut(key) {
262 Some(base_value) => deep_merge_json(base_value, overlay_value),
263 None => {
264 base_map.insert(key.clone(), overlay_value.clone());
265 }
266 }
267 }
268 }
269 (base, overlay) => {
270 *base = overlay.clone();
271 }
272 }
273}
274
275pub fn sha256sum(value: String) -> String {
279 use sha2::{Digest, Sha256};
280 let mut hasher = Sha256::new();
281 hasher.update(value.as_bytes());
282 format!("{:x}", hasher.finalize())
283}
284
285pub fn trunc(value: String, length: usize) -> String {
289 if value.len() <= length {
290 value
291 } else {
292 value.chars().take(length).collect()
293 }
294}
295
296pub fn trimprefix(value: String, prefix: String) -> String {
300 value.strip_prefix(&prefix).unwrap_or(&value).to_string()
301}
302
303pub fn trimsuffix(value: String, suffix: String) -> String {
307 value.strip_suffix(&suffix).unwrap_or(&value).to_string()
308}
309
310pub fn snakecase(value: String) -> String {
314 let mut result = String::with_capacity(value.len() + value.len() / 4);
316 let mut prev_upper = false;
317
318 for (i, c) in value.chars().enumerate() {
319 if c.is_uppercase() {
320 if i > 0 && !prev_upper {
321 result.push('_');
322 }
323 result.push(c.to_lowercase().next().unwrap_or(c));
324 prev_upper = true;
325 } else if c == '-' || c == ' ' {
326 result.push('_');
327 prev_upper = false;
328 } else {
329 result.push(c);
330 prev_upper = false;
331 }
332 }
333
334 result
335}
336
337pub fn kebabcase(value: String) -> String {
341 snakecase(value).replace('_', "-")
342}
343
344pub fn tostrings(value: Value, kwargs: minijinja::value::Kwargs) -> Result<Vec<String>, Error> {
371 let prefix: String = kwargs.get("prefix").ok().flatten().unwrap_or_default();
373 let suffix: String = kwargs.get("suffix").ok().flatten().unwrap_or_default();
374 let skip_empty: bool = kwargs.get("skip_empty").ok().flatten().unwrap_or(false);
375
376 kwargs.assert_all_used()?;
378
379 let has_prefix = !prefix.is_empty();
380 let has_suffix = !suffix.is_empty();
381
382 let convert_value = |v: Value| -> Option<String> {
383 if v.is_undefined() || v.is_none() {
385 if skip_empty {
386 return None;
387 }
388 return Some(String::new());
389 }
390
391 let s = if let Some(str_val) = v.as_str() {
392 str_val.to_string()
393 } else {
394 v.to_string()
395 };
396
397 if skip_empty && s.is_empty() {
399 return None;
400 }
401
402 if has_prefix || has_suffix {
404 let mut result = String::with_capacity(s.len() + prefix.len() + suffix.len());
405 result.push_str(&prefix);
406 result.push_str(&s);
407 result.push_str(&suffix);
408 Some(result)
409 } else {
410 Some(s)
411 }
412 };
413
414 match value.try_iter() {
415 Ok(iter) => {
416 let strings: Vec<String> = iter.filter_map(convert_value).collect();
417 Ok(strings)
418 }
419 Err(_) => {
420 match convert_value(value) {
422 Some(s) => Ok(vec![s]),
423 None => Ok(vec![]),
424 }
425 }
426 }
427}
428
429pub fn semver_match(version: Value, constraint: String) -> Result<bool, Error> {
440 let version_str = version
441 .as_str()
442 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "version must be a string"))?;
443
444 let version_clean = version_str.trim_start_matches('v');
446
447 let parsed_version = match Version::parse(version_clean) {
449 Ok(v) => v,
450 Err(_) => {
451 let parts: Vec<&str> = version_clean
453 .split('-')
454 .next()
455 .unwrap_or(version_clean)
456 .split('.')
457 .collect();
458
459 if parts.len() >= 3 {
460 let major: u64 = parts[0].parse().unwrap_or(0);
461 let minor: u64 = parts[1].parse().unwrap_or(0);
462 let patch: u64 = parts[2].parse().unwrap_or(0);
463 Version::new(major, minor, patch)
464 } else if parts.len() == 2 {
465 let major: u64 = parts[0].parse().unwrap_or(0);
466 let minor: u64 = parts[1].parse().unwrap_or(0);
467 Version::new(major, minor, 0)
468 } else {
469 return Err(Error::new(
470 ErrorKind::InvalidOperation,
471 format!("Invalid version format: {}", version_str),
472 ));
473 }
474 }
475 };
476
477 let constraint_clean = constraint.trim_start_matches(|c: char| c.is_whitespace());
479
480 let req = VersionReq::parse(constraint_clean)
482 .or_else(|_| {
483 let constraint_base = constraint_clean
485 .split('-')
486 .next()
487 .unwrap_or(constraint_clean);
488 VersionReq::parse(constraint_base)
489 })
490 .map_err(|e| {
491 Error::new(
492 ErrorKind::InvalidOperation,
493 format!("Invalid constraint '{}': {}", constraint, e),
494 )
495 })?;
496
497 Ok(req.matches(&parsed_version))
498}
499
500pub fn int(value: Value) -> Result<i64, Error> {
502 match value.kind() {
503 ValueKind::Number => {
504 if let Some(i) = value.as_i64() {
506 Ok(i)
507 } else if let Ok(f) = f64::try_from(value.clone()) {
508 Ok(f as i64)
509 } else {
510 Err(Error::new(
511 ErrorKind::InvalidOperation,
512 "Cannot convert to int",
513 ))
514 }
515 }
516 ValueKind::String => {
517 let s = value.as_str().unwrap_or("");
518 s.parse::<i64>()
519 .or_else(|_| s.parse::<f64>().map(|f| f as i64))
520 .map_err(|_| {
521 Error::new(
522 ErrorKind::InvalidOperation,
523 format!("Cannot parse '{}' as int", s),
524 )
525 })
526 }
527 ValueKind::Bool => Ok(if value.is_true() { 1 } else { 0 }),
528 _ => Err(Error::new(
529 ErrorKind::InvalidOperation,
530 format!("Cannot convert {:?} to int", value.kind()),
531 )),
532 }
533}
534
535pub fn float(value: Value) -> Result<f64, Error> {
537 match value.kind() {
538 ValueKind::Number => {
539 f64::try_from(value)
541 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "Cannot convert to float"))
542 }
543 ValueKind::String => {
544 let s = value.as_str().unwrap_or("");
545 s.parse::<f64>().map_err(|_| {
546 Error::new(
547 ErrorKind::InvalidOperation,
548 format!("Cannot parse '{}' as float", s),
549 )
550 })
551 }
552 ValueKind::Bool => Ok(if value.is_true() { 1.0 } else { 0.0 }),
553 _ => Err(Error::new(
554 ErrorKind::InvalidOperation,
555 format!("Cannot convert {:?} to float", value.kind()),
556 )),
557 }
558}
559
560pub fn abs(value: Value) -> Result<Value, Error> {
562 match value.kind() {
563 ValueKind::Number => {
564 if let Some(i) = value.as_i64() {
565 Ok(Value::from(i.abs()))
566 } else if let Ok(f) = f64::try_from(value) {
567 Ok(Value::from(f.abs()))
568 } else {
569 Err(Error::new(
570 ErrorKind::InvalidOperation,
571 "Cannot get absolute value",
572 ))
573 }
574 }
575 _ => Err(Error::new(
576 ErrorKind::InvalidOperation,
577 format!("abs requires a number, got {:?}", value.kind()),
578 )),
579 }
580}
581
582pub fn basename(path: String) -> String {
589 std::path::Path::new(&path)
590 .file_name()
591 .map(|s| s.to_string_lossy().to_string())
592 .unwrap_or_default()
593}
594
595pub fn dirname(path: String) -> String {
598 std::path::Path::new(&path)
599 .parent()
600 .map(|p| p.to_string_lossy().to_string())
601 .unwrap_or_default()
602}
603
604pub fn extname(path: String) -> String {
607 std::path::Path::new(&path)
608 .extension()
609 .map(|s| s.to_string_lossy().to_string())
610 .unwrap_or_default()
611}
612
613pub fn cleanpath(path: String) -> String {
616 let is_absolute = path.starts_with('/');
617 let mut parts: Vec<&str> = vec![];
618
619 for part in path.split('/') {
620 match part {
621 "" | "." => continue,
622 ".." => {
623 parts.pop();
624 }
625 _ => parts.push(part),
626 }
627 }
628
629 let result = parts.join("/");
630 if is_absolute {
631 format!("/{}", result)
632 } else if result.is_empty() {
633 ".".to_string()
634 } else {
635 result
636 }
637}
638
639pub fn regex_match(value: String, pattern: String) -> Result<bool, Error> {
646 regex::Regex::new(&pattern)
647 .map(|re| re.is_match(&value))
648 .map_err(|e| {
649 Error::new(
650 ErrorKind::InvalidOperation,
651 format!("invalid regex '{}': {}", pattern, e),
652 )
653 })
654}
655
656pub fn regex_replace(value: String, pattern: String, replacement: String) -> Result<String, Error> {
659 regex::Regex::new(&pattern)
660 .map(|re| re.replace_all(&value, replacement.as_str()).to_string())
661 .map_err(|e| {
662 Error::new(
663 ErrorKind::InvalidOperation,
664 format!("invalid regex '{}': {}", pattern, e),
665 )
666 })
667}
668
669pub fn regex_find(value: String, pattern: String) -> Result<String, Error> {
672 regex::Regex::new(&pattern)
673 .map(|re| {
674 re.find(&value)
675 .map(|m| m.as_str().to_string())
676 .unwrap_or_default()
677 })
678 .map_err(|e| {
679 Error::new(
680 ErrorKind::InvalidOperation,
681 format!("invalid regex '{}': {}", pattern, e),
682 )
683 })
684}
685
686pub fn regex_find_all(value: String, pattern: String) -> Result<Vec<String>, Error> {
689 regex::Regex::new(&pattern)
690 .map(|re| {
691 re.find_iter(&value)
692 .map(|m| m.as_str().to_string())
693 .collect()
694 })
695 .map_err(|e| {
696 Error::new(
697 ErrorKind::InvalidOperation,
698 format!("invalid regex '{}': {}", pattern, e),
699 )
700 })
701}
702
703pub fn values(dict: Value) -> Result<Value, Error> {
710 match dict.kind() {
711 ValueKind::Map => {
712 let items: Vec<Value> = dict
713 .try_iter()
714 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate dict"))?
715 .filter_map(|k| dict.get_item(&k).ok())
716 .collect();
717 Ok(Value::from(items))
718 }
719 _ => Err(Error::new(
720 ErrorKind::InvalidOperation,
721 format!("values requires a dict, got {:?}", dict.kind()),
722 )),
723 }
724}
725
726pub fn pick(dict: Value, keys: &[Value]) -> Result<Value, Error> {
729 match dict.kind() {
730 ValueKind::Map => {
731 let mut result = indexmap::IndexMap::new();
732 for key in keys {
733 if let Some(key_str) = key.as_str()
734 && let Ok(val) = dict.get_item(key)
735 {
736 result.insert(key_str.to_string(), val);
737 }
738 }
739 Ok(Value::from_iter(result))
740 }
741 _ => Err(Error::new(
742 ErrorKind::InvalidOperation,
743 format!("pick requires a dict, got {:?}", dict.kind()),
744 )),
745 }
746}
747
748pub fn omit(dict: Value, keys: &[Value]) -> Result<Value, Error> {
751 match dict.kind() {
752 ValueKind::Map => {
753 let exclude: std::collections::HashSet<String> = keys
754 .iter()
755 .filter_map(|k| k.as_str().map(|s| s.to_string()))
756 .collect();
757
758 let mut result = indexmap::IndexMap::new();
759 if let Ok(iter) = dict.try_iter() {
760 for key in iter {
761 if let Some(key_str) = key.as_str()
762 && !exclude.contains(key_str)
763 && let Ok(val) = dict.get_item(&key)
764 {
765 result.insert(key_str.to_string(), val);
766 }
767 }
768 }
769 Ok(Value::from_iter(result))
770 }
771 _ => Err(Error::new(
772 ErrorKind::InvalidOperation,
773 format!("omit requires a dict, got {:?}", dict.kind()),
774 )),
775 }
776}
777
778pub fn append(list: Value, item: Value) -> Result<Value, Error> {
785 match list.kind() {
786 ValueKind::Seq => {
787 let mut items: Vec<Value> = list
788 .try_iter()
789 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
790 .collect();
791 items.push(item);
792 Ok(Value::from(items))
793 }
794 _ => Err(Error::new(
795 ErrorKind::InvalidOperation,
796 format!("append requires a list, got {:?}", list.kind()),
797 )),
798 }
799}
800
801pub fn prepend(list: Value, item: Value) -> Result<Value, Error> {
804 match list.kind() {
805 ValueKind::Seq => {
806 let mut items: Vec<Value> = vec![item];
807 items.extend(
808 list.try_iter()
809 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?,
810 );
811 Ok(Value::from(items))
812 }
813 _ => Err(Error::new(
814 ErrorKind::InvalidOperation,
815 format!("prepend requires a list, got {:?}", list.kind()),
816 )),
817 }
818}
819
820pub fn concat(list1: Value, list2: Value) -> Result<Value, Error> {
823 match (list1.kind(), list2.kind()) {
824 (ValueKind::Seq, ValueKind::Seq) => {
825 let mut items: Vec<Value> = list1
826 .try_iter()
827 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate first list"))?
828 .collect();
829 items.extend(list2.try_iter().map_err(|_| {
830 Error::new(ErrorKind::InvalidOperation, "cannot iterate second list")
831 })?);
832 Ok(Value::from(items))
833 }
834 _ => Err(Error::new(
835 ErrorKind::InvalidOperation,
836 "concat requires two lists",
837 )),
838 }
839}
840
841pub fn without(list: Value, exclude: &[Value]) -> Result<Value, Error> {
844 match list.kind() {
845 ValueKind::Seq => {
846 let items: Vec<Value> = list
847 .try_iter()
848 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
849 .filter(|item| !exclude.contains(item))
850 .collect();
851 Ok(Value::from(items))
852 }
853 _ => Err(Error::new(
854 ErrorKind::InvalidOperation,
855 format!("without requires a list, got {:?}", list.kind()),
856 )),
857 }
858}
859
860pub fn compact(list: Value) -> Result<Value, Error> {
863 match list.kind() {
864 ValueKind::Seq => {
865 let items: Vec<Value> = list
866 .try_iter()
867 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
868 .filter(|item| match item.kind() {
869 ValueKind::Undefined | ValueKind::None => false,
870 ValueKind::String => !item.as_str().unwrap_or("").is_empty(),
871 ValueKind::Seq => item.len().unwrap_or(0) > 0,
872 ValueKind::Map => item.len().unwrap_or(0) > 0,
873 _ => true,
874 })
875 .collect();
876 Ok(Value::from(items))
877 }
878 _ => Err(Error::new(
879 ErrorKind::InvalidOperation,
880 format!("compact requires a list, got {:?}", list.kind()),
881 )),
882 }
883}
884
885pub fn floor(value: Value) -> Result<i64, Error> {
892 match value.kind() {
893 ValueKind::Number => {
894 if let Some(i) = value.as_i64() {
895 Ok(i)
896 } else if let Ok(f) = f64::try_from(value) {
897 Ok(f.floor() as i64)
898 } else {
899 Err(Error::new(
900 ErrorKind::InvalidOperation,
901 "cannot convert to number",
902 ))
903 }
904 }
905 _ => Err(Error::new(
906 ErrorKind::InvalidOperation,
907 format!("floor requires a number, got {:?}", value.kind()),
908 )),
909 }
910}
911
912pub fn ceil(value: Value) -> Result<i64, Error> {
915 match value.kind() {
916 ValueKind::Number => {
917 if let Some(i) = value.as_i64() {
918 Ok(i)
919 } else if let Ok(f) = f64::try_from(value) {
920 Ok(f.ceil() as i64)
921 } else {
922 Err(Error::new(
923 ErrorKind::InvalidOperation,
924 "cannot convert to number",
925 ))
926 }
927 }
928 _ => Err(Error::new(
929 ErrorKind::InvalidOperation,
930 format!("ceil requires a number, got {:?}", value.kind()),
931 )),
932 }
933}
934
935pub fn sha1sum(value: String) -> String {
942 use sha1::{Digest, Sha1};
943 let mut hasher = Sha1::new();
944 hasher.update(value.as_bytes());
945 format!("{:x}", hasher.finalize())
946}
947
948pub fn sha512sum(value: String) -> String {
951 use sha2::{Digest, Sha512};
952 let mut hasher = Sha512::new();
953 hasher.update(value.as_bytes());
954 format!("{:x}", hasher.finalize())
955}
956
957pub fn md5sum(value: String) -> String {
960 use md5::{Digest, Md5};
961 let mut hasher = Md5::new();
962 hasher.update(value.as_bytes());
963 format!("{:x}", hasher.finalize())
964}
965
966pub fn repeat(value: String, count: usize) -> String {
973 value.repeat(count)
974}
975
976pub fn camelcase(value: String) -> String {
980 let mut result = String::with_capacity(value.len());
981 let mut capitalize_next = false;
982 let mut first = true;
983
984 for c in value.chars() {
985 if c == '_' || c == '-' || c == ' ' {
986 capitalize_next = true;
987 } else if capitalize_next {
988 result.extend(c.to_uppercase());
989 capitalize_next = false;
990 } else if first {
991 result.extend(c.to_lowercase());
992 first = false;
993 } else {
994 result.extend(c.to_lowercase());
995 }
996 }
997
998 result
999}
1000
1001pub fn pascalcase(value: String) -> String {
1004 let mut result = String::with_capacity(value.len());
1005 let mut capitalize_next = true;
1006
1007 for c in value.chars() {
1008 if c == '_' || c == '-' || c == ' ' {
1009 capitalize_next = true;
1010 } else if capitalize_next {
1011 result.extend(c.to_uppercase());
1012 capitalize_next = false;
1013 } else {
1014 result.push(c);
1015 }
1016 }
1017
1018 result
1019}
1020
1021pub fn substr(value: String, start: usize, length: Option<usize>) -> String {
1025 let chars: Vec<char> = value.chars().collect();
1026 let end = length
1027 .map(|l| (start + l).min(chars.len()))
1028 .unwrap_or(chars.len());
1029 let start = start.min(chars.len());
1030 chars[start..end].iter().collect()
1031}
1032
1033pub fn wrap(value: String, width: usize) -> String {
1036 let mut result = String::with_capacity(value.len() + value.len() / width);
1037 let mut line_len = 0;
1038
1039 for word in value.split_whitespace() {
1040 let word_len = word.chars().count();
1041 if line_len > 0 && line_len + 1 + word_len > width {
1042 result.push('\n');
1043 line_len = 0;
1044 } else if line_len > 0 {
1045 result.push(' ');
1046 line_len += 1;
1047 }
1048 result.push_str(word);
1049 line_len += word_len;
1050 }
1051
1052 result
1053}
1054
1055pub fn hasprefix(value: String, prefix: String) -> bool {
1058 value.starts_with(&prefix)
1059}
1060
1061pub fn hassuffix(value: String, suffix: String) -> bool {
1064 value.ends_with(&suffix)
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use super::*;
1070
1071 #[test]
1072 fn test_toyaml() {
1073 let value = Value::from_serialize(serde_json::json!({
1074 "name": "test",
1075 "port": 8080
1076 }));
1077 let yaml = toyaml(value).unwrap();
1078 assert!(yaml.contains("name: test"));
1079 assert!(yaml.contains("port: 8080"));
1080 }
1081
1082 #[test]
1083 fn test_b64encode_decode() {
1084 let original = "hello world".to_string();
1085 let encoded = b64encode(original.clone());
1086 let decoded = b64decode(encoded).unwrap();
1087 assert_eq!(original, decoded);
1088 }
1089
1090 #[test]
1091 fn test_quote() {
1092 assert_eq!(quote(Value::from("test")), "\"test\"");
1093 assert_eq!(squote(Value::from("test")), "'test'");
1094 }
1095
1096 #[test]
1097 fn test_nindent() {
1098 let input = "line1\nline2".to_string();
1099 let result = nindent(input, 4);
1100 assert_eq!(result, "\n line1\n line2");
1101 }
1102
1103 #[test]
1104 fn test_required() {
1105 assert!(required(Value::from("test"), None).is_ok());
1106 assert!(required(Value::UNDEFINED, None).is_err());
1107 assert!(required(Value::from(""), None).is_err());
1108 }
1109
1110 #[test]
1111 fn test_empty() {
1112 assert!(empty(Value::UNDEFINED));
1113 assert!(empty(Value::from("")));
1114 assert!(empty(Value::from_serialize(Vec::<i32>::new())));
1115 assert!(!empty(Value::from("test")));
1116 }
1117
1118 #[test]
1119 fn test_trunc() {
1120 assert_eq!(trunc("hello".to_string(), 3), "hel");
1121 assert_eq!(trunc("hi".to_string(), 10), "hi");
1122 }
1123
1124 #[test]
1125 fn test_snakecase() {
1126 assert_eq!(snakecase("camelCase".to_string()), "camel_case");
1127 assert_eq!(snakecase("PascalCase".to_string()), "pascal_case");
1128 }
1129
1130 #[test]
1131 fn test_tostrings_list() {
1132 use minijinja::Environment;
1133
1134 let mut env = Environment::new();
1135 env.add_filter("tostrings", tostrings);
1136
1137 let result: Vec<String> = env
1138 .render_str("{{ [1, 2, 3] | tostrings }}", ())
1139 .unwrap()
1140 .trim_matches(|c| c == '[' || c == ']')
1141 .split(", ")
1142 .map(|s| s.trim_matches('"').to_string())
1143 .collect();
1144 assert_eq!(result, vec!["1", "2", "3"]);
1145 }
1146
1147 #[test]
1148 fn test_tostrings_with_prefix() {
1149 use minijinja::Environment;
1150
1151 let mut env = Environment::new();
1152 env.add_filter("tostrings", tostrings);
1153
1154 let template = r#"{{ [80, 443] | tostrings(prefix="port-") | join(",") }}"#;
1155 let result = env.render_str(template, ()).unwrap();
1156 assert_eq!(result, "port-80,port-443");
1157 }
1158
1159 #[test]
1160 fn test_tostrings_with_suffix() {
1161 use minijinja::Environment;
1162
1163 let mut env = Environment::new();
1164 env.add_filter("tostrings", tostrings);
1165
1166 let template = r#"{{ [1, 2] | tostrings(suffix="/TCP") | join(",") }}"#;
1167 let result = env.render_str(template, ()).unwrap();
1168 assert_eq!(result, "1/TCP,2/TCP");
1169 }
1170
1171 #[test]
1172 fn test_tostrings_skip_empty() {
1173 use minijinja::Environment;
1174
1175 let mut env = Environment::new();
1176 env.add_filter("tostrings", tostrings);
1177
1178 let template = r#"{{ ["a", "", "c"] | tostrings(skip_empty=true) | join(",") }}"#;
1179 let result = env.render_str(template, ()).unwrap();
1180 assert_eq!(result, "a,c");
1181 }
1182
1183 #[test]
1184 fn test_tostrings_mixed() {
1185 use minijinja::Environment;
1186
1187 let mut env = Environment::new();
1188 env.add_filter("tostrings", tostrings);
1189
1190 let template = r#"{{ ["hello", 42, true] | tostrings | join(",") }}"#;
1191 let result = env.render_str(template, ()).unwrap();
1192 assert_eq!(result, "hello,42,true");
1193 }
1194
1195 #[test]
1196 fn test_int_filter() {
1197 assert_eq!(int(Value::from(42)).unwrap(), 42);
1199 assert_eq!(int(Value::from(3.7)).unwrap(), 3);
1201 assert_eq!(int(Value::from(-3.7)).unwrap(), -3);
1202 assert_eq!(int(Value::from("123")).unwrap(), 123);
1204 assert_eq!(int(Value::from("45.9")).unwrap(), 45);
1205 assert_eq!(int(Value::from(true)).unwrap(), 1);
1207 assert_eq!(int(Value::from(false)).unwrap(), 0);
1208 }
1209
1210 #[test]
1211 fn test_float_filter() {
1212 let result = float(Value::from(2.5)).unwrap();
1214 assert!((result - 2.5).abs() < 0.001);
1215 let result = float(Value::from(42)).unwrap();
1217 assert!((result - 42.0).abs() < 0.001);
1218 let result = float(Value::from("2.5")).unwrap();
1220 assert!((result - 2.5).abs() < 0.001);
1221 assert_eq!(float(Value::from(true)).unwrap(), 1.0);
1223 assert_eq!(float(Value::from(false)).unwrap(), 0.0);
1224 }
1225
1226 #[test]
1227 fn test_abs_filter() {
1228 let result = abs(Value::from(5)).unwrap();
1230 assert_eq!(result.as_i64().unwrap(), 5);
1231 let result = abs(Value::from(-5)).unwrap();
1233 assert_eq!(result.as_i64().unwrap(), 5);
1234 let result = abs(Value::from(2.5)).unwrap();
1236 assert!((f64::try_from(result).unwrap() - 2.5).abs() < 0.001);
1237 let result = abs(Value::from(-2.5)).unwrap();
1239 assert!((f64::try_from(result).unwrap() - 2.5).abs() < 0.001);
1240 let result = abs(Value::from(0)).unwrap();
1242 assert_eq!(result.as_i64().unwrap(), 0);
1243 }
1244
1245 #[test]
1250 fn test_basename() {
1251 assert_eq!(basename("/etc/nginx/nginx.conf".to_string()), "nginx.conf");
1252 assert_eq!(basename("file.txt".to_string()), "file.txt");
1253 assert_eq!(basename("/path/to/dir/".to_string()), "dir"); assert_eq!(basename("/".to_string()), "");
1255 assert_eq!(basename("".to_string()), "");
1256 }
1257
1258 #[test]
1259 fn test_dirname() {
1260 assert_eq!(dirname("/etc/nginx/nginx.conf".to_string()), "/etc/nginx");
1261 assert_eq!(dirname("file.txt".to_string()), "");
1262 assert_eq!(dirname("/single".to_string()), "/");
1263 assert_eq!(dirname("a/b/c".to_string()), "a/b");
1264 }
1265
1266 #[test]
1267 fn test_extname() {
1268 assert_eq!(extname("file.txt".to_string()), "txt");
1269 assert_eq!(extname("archive.tar.gz".to_string()), "gz");
1270 assert_eq!(extname("noext".to_string()), "");
1271 assert_eq!(extname(".hidden".to_string()), "");
1272 }
1273
1274 #[test]
1275 fn test_cleanpath() {
1276 assert_eq!(cleanpath("a/b/../c".to_string()), "a/c");
1277 assert_eq!(cleanpath("a/./b/./c".to_string()), "a/b/c");
1278 assert_eq!(cleanpath("/a/b/../c".to_string()), "/a/c");
1279 assert_eq!(cleanpath("../a".to_string()), "a");
1280 assert_eq!(cleanpath("".to_string()), ".");
1281 }
1282
1283 #[test]
1288 fn test_regex_match() {
1289 assert!(regex_match("v1.2.3".to_string(), r"^v\d+".to_string()).unwrap());
1290 assert!(!regex_match("1.2.3".to_string(), r"^v\d+".to_string()).unwrap());
1291 assert!(regex_match("hello@world.com".to_string(), r"@.*\.".to_string()).unwrap());
1292 }
1293
1294 #[test]
1295 fn test_regex_replace() {
1296 assert_eq!(
1297 regex_replace(
1298 "v1.2.3".to_string(),
1299 r"v(\d+)".to_string(),
1300 "version-$1".to_string()
1301 )
1302 .unwrap(),
1303 "version-1.2.3"
1304 );
1305 assert_eq!(
1306 regex_replace(
1307 "foo bar baz".to_string(),
1308 r"\s+".to_string(),
1309 "-".to_string()
1310 )
1311 .unwrap(),
1312 "foo-bar-baz"
1313 );
1314 }
1315
1316 #[test]
1317 fn test_regex_find() {
1318 assert_eq!(
1319 regex_find("port: 8080".to_string(), r"\d+".to_string()).unwrap(),
1320 "8080"
1321 );
1322 assert_eq!(
1323 regex_find("no numbers".to_string(), r"\d+".to_string()).unwrap(),
1324 ""
1325 );
1326 }
1327
1328 #[test]
1329 fn test_regex_find_all() {
1330 let result = regex_find_all("a1b2c3".to_string(), r"\d+".to_string()).unwrap();
1331 assert_eq!(result, vec!["1", "2", "3"]);
1332 }
1333
1334 #[test]
1339 fn test_values_filter() {
1340 use minijinja::Environment;
1341 let mut env = Environment::new();
1342 env.add_filter("values", values);
1343
1344 let result = env
1345 .render_str(r#"{{ {"a": 1, "b": 2} | values | sort | list }}"#, ())
1346 .unwrap();
1347 assert!(result.contains("1") && result.contains("2"));
1348 }
1349
1350 #[test]
1351 fn test_pick_filter() {
1352 use minijinja::Environment;
1353 let mut env = Environment::new();
1354 env.add_filter("pick", pick);
1355
1356 let result = env
1357 .render_str(r#"{{ {"a": 1, "b": 2, "c": 3} | pick("a", "c") }}"#, ())
1358 .unwrap();
1359 assert!(result.contains("a") && result.contains("c") && !result.contains("b"));
1360 }
1361
1362 #[test]
1363 fn test_omit_filter() {
1364 use minijinja::Environment;
1365 let mut env = Environment::new();
1366 env.add_filter("omit", omit);
1367
1368 let result = env
1369 .render_str(r#"{{ {"a": 1, "b": 2, "c": 3} | omit("b") }}"#, ())
1370 .unwrap();
1371 assert!(result.contains("a") && result.contains("c") && !result.contains(": 2"));
1372 }
1373
1374 #[test]
1379 fn test_append_filter() {
1380 use minijinja::Environment;
1381 let mut env = Environment::new();
1382 env.add_filter("append", append);
1383
1384 let result = env.render_str(r#"{{ [1, 2] | append(3) }}"#, ()).unwrap();
1385 assert_eq!(result, "[1, 2, 3]");
1386 }
1387
1388 #[test]
1389 fn test_prepend_filter() {
1390 use minijinja::Environment;
1391 let mut env = Environment::new();
1392 env.add_filter("prepend", prepend);
1393
1394 let result = env.render_str(r#"{{ [2, 3] | prepend(1) }}"#, ()).unwrap();
1395 assert_eq!(result, "[1, 2, 3]");
1396 }
1397
1398 #[test]
1399 fn test_concat_filter() {
1400 use minijinja::Environment;
1401 let mut env = Environment::new();
1402 env.add_filter("concat", concat);
1403
1404 let result = env
1405 .render_str(r#"{{ [1, 2] | concat([3, 4]) }}"#, ())
1406 .unwrap();
1407 assert_eq!(result, "[1, 2, 3, 4]");
1408 }
1409
1410 #[test]
1411 fn test_without_filter() {
1412 use minijinja::Environment;
1413 let mut env = Environment::new();
1414 env.add_filter("without", without);
1415
1416 let result = env
1417 .render_str(r#"{{ [1, 2, 3, 2] | without(2) }}"#, ())
1418 .unwrap();
1419 assert_eq!(result, "[1, 3]");
1420 }
1421
1422 #[test]
1423 fn test_compact_filter() {
1424 use minijinja::Environment;
1425 let mut env = Environment::new();
1426 env.add_filter("compact", compact);
1427
1428 let result = env
1429 .render_str(r#"{{ ["a", "", "b"] | compact }}"#, ())
1430 .unwrap();
1431 assert!(result.contains("a") && result.contains("b") && !result.contains(r#""""#));
1432 }
1433
1434 #[test]
1439 fn test_floor_filter() {
1440 assert_eq!(floor(Value::from(3.7)).unwrap(), 3);
1441 assert_eq!(floor(Value::from(3.2)).unwrap(), 3);
1442 assert_eq!(floor(Value::from(-3.2)).unwrap(), -4);
1443 assert_eq!(floor(Value::from(5)).unwrap(), 5);
1444 }
1445
1446 #[test]
1447 fn test_ceil_filter() {
1448 assert_eq!(ceil(Value::from(3.2)).unwrap(), 4);
1449 assert_eq!(ceil(Value::from(3.7)).unwrap(), 4);
1450 assert_eq!(ceil(Value::from(-3.7)).unwrap(), -3);
1451 assert_eq!(ceil(Value::from(5)).unwrap(), 5);
1452 }
1453
1454 #[test]
1459 fn test_sha1sum() {
1460 assert_eq!(
1462 sha1sum("hello".to_string()),
1463 "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
1464 );
1465 }
1466
1467 #[test]
1468 fn test_sha512sum() {
1469 let result = sha512sum("hello".to_string());
1471 assert!(result.starts_with("9b71d224bd62f3785d96d46ad3ea3d73"));
1472 assert_eq!(result.len(), 128); }
1474
1475 #[test]
1476 fn test_md5sum() {
1477 assert_eq!(
1479 md5sum("hello".to_string()),
1480 "5d41402abc4b2a76b9719d911017c592"
1481 );
1482 }
1483
1484 #[test]
1489 fn test_repeat_filter() {
1490 assert_eq!(repeat("-".to_string(), 5), "-----");
1491 assert_eq!(repeat("ab".to_string(), 3), "ababab");
1492 assert_eq!(repeat("x".to_string(), 0), "");
1493 }
1494
1495 #[test]
1496 fn test_camelcase_filter() {
1497 assert_eq!(camelcase("foo_bar_baz".to_string()), "fooBarBaz");
1498 assert_eq!(camelcase("foo-bar-baz".to_string()), "fooBarBaz");
1499 assert_eq!(camelcase("FOO_BAR".to_string()), "fooBar");
1500 assert_eq!(camelcase("already".to_string()), "already");
1501 }
1502
1503 #[test]
1504 fn test_pascalcase_filter() {
1505 assert_eq!(pascalcase("foo_bar".to_string()), "FooBar");
1506 assert_eq!(pascalcase("foo-bar-baz".to_string()), "FooBarBaz");
1507 assert_eq!(pascalcase("hello".to_string()), "Hello");
1508 }
1509
1510 #[test]
1511 fn test_substr_filter() {
1512 assert_eq!(substr("hello world".to_string(), 0, Some(5)), "hello");
1513 assert_eq!(substr("hello world".to_string(), 6, None), "world");
1514 assert_eq!(substr("hello".to_string(), 10, Some(5)), "");
1515 assert_eq!(substr("hello".to_string(), 0, Some(100)), "hello");
1516 }
1517
1518 #[test]
1519 fn test_wrap_filter() {
1520 assert_eq!(
1521 wrap("hello world foo bar".to_string(), 10),
1522 "hello\nworld foo\nbar"
1523 );
1524 assert_eq!(wrap("short".to_string(), 20), "short");
1525 }
1526
1527 #[test]
1528 fn test_hasprefix_filter() {
1529 assert!(hasprefix("hello world".to_string(), "hello".to_string()));
1530 assert!(!hasprefix("hello world".to_string(), "world".to_string()));
1531 }
1532
1533 #[test]
1534 fn test_hassuffix_filter() {
1535 assert!(hassuffix("hello.txt".to_string(), ".txt".to_string()));
1536 assert!(!hassuffix("hello.txt".to_string(), ".yaml".to_string()));
1537 }
1538}