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
49pub fn fromjson(value: String) -> Result<Value, Error> {
54 let parsed: serde_json::Value = serde_json::from_str(&value).map_err(|e| {
55 Error::new(
56 ErrorKind::InvalidOperation,
57 format!("fromjson: invalid JSON: {}", e),
58 )
59 })?;
60 Ok(Value::from_serialize(parsed))
61}
62
63pub fn fromyaml(value: String) -> Result<Value, Error> {
68 let parsed: serde_yaml::Value = serde_yaml::from_str(&value).map_err(|e| {
69 Error::new(
70 ErrorKind::InvalidOperation,
71 format!("fromyaml: invalid YAML: {}", e),
72 )
73 })?;
74 let json_value: serde_json::Value = serde_json::to_value(&parsed).map_err(|e| {
76 Error::new(
77 ErrorKind::InvalidOperation,
78 format!("fromyaml: failed to normalize parsed YAML: {}", e),
79 )
80 })?;
81 Ok(Value::from_serialize(json_value))
82}
83
84#[must_use]
88pub fn b64encode(value: String) -> String {
89 base64::engine::general_purpose::STANDARD.encode(value.as_bytes())
90}
91
92pub fn b64decode(value: String) -> Result<String, Error> {
96 let decoded = base64::engine::general_purpose::STANDARD
97 .decode(value.as_bytes())
98 .map_err(|e| {
99 Error::new(
100 ErrorKind::InvalidOperation,
101 format!("base64 decode error: {}", e),
102 )
103 })?;
104
105 String::from_utf8(decoded).map_err(|e| {
106 Error::new(
107 ErrorKind::InvalidOperation,
108 format!("UTF-8 decode error: {}", e),
109 )
110 })
111}
112
113#[must_use]
117pub fn quote(value: Value) -> String {
118 let s = if let Some(str_val) = value.as_str() {
119 str_val.to_string()
120 } else {
121 value.to_string()
122 };
123 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
124}
125
126#[must_use]
130pub fn squote(value: Value) -> String {
131 let s = if let Some(str_val) = value.as_str() {
132 str_val.to_string()
133 } else {
134 value.to_string()
135 };
136 format!("'{}'", s.replace('\'', "''"))
137}
138
139#[must_use]
143pub fn nindent(value: String, spaces: usize) -> String {
144 let line_count = value.lines().count();
145 let mut result = String::with_capacity(1 + value.len() + spaces * line_count + line_count);
147 result.push('\n');
148
149 let indent = " ".repeat(spaces);
150 let mut first = true;
151
152 for line in value.lines() {
153 if !first {
154 result.push('\n');
155 }
156 first = false;
157
158 if !line.is_empty() {
159 result.push_str(&indent);
160 result.push_str(line);
161 }
162 }
163
164 result
165}
166
167pub fn indent(value: String, spaces: usize) -> String {
171 let line_count = value.lines().count();
172 let mut result = String::with_capacity(value.len() + spaces * line_count + line_count);
174
175 let indent_str = " ".repeat(spaces);
176 let mut first = true;
177
178 for line in value.lines() {
179 if !first {
180 result.push('\n');
181 }
182 first = false;
183
184 if !line.is_empty() {
185 result.push_str(&indent_str);
186 }
187 result.push_str(line);
188 }
189
190 result
191}
192
193pub fn required(value: Value, message: Option<String>) -> Result<Value, Error> {
197 if value.is_undefined() || value.is_none() {
198 let msg = message.unwrap_or_else(|| "required value is missing".to_string());
199 Err(Error::new(ErrorKind::InvalidOperation, msg))
200 } else if let Some(s) = value.as_str() {
201 if s.is_empty() {
202 let msg = message.unwrap_or_else(|| "required value is empty".to_string());
203 Err(Error::new(ErrorKind::InvalidOperation, msg))
204 } else {
205 Ok(value)
206 }
207 } else {
208 Ok(value)
209 }
210}
211
212pub fn empty(value: Value) -> bool {
216 if value.is_undefined() || value.is_none() {
217 return true;
218 }
219
220 match value.len() {
221 Some(len) => len == 0,
222 None => {
223 if let Some(s) = value.as_str() {
224 s.is_empty()
225 } else {
226 false
227 }
228 }
229 }
230}
231
232pub fn coalesce(args: Vec<Value>) -> Value {
236 for arg in args {
237 if !arg.is_undefined() && !arg.is_none() {
238 if let Some(s) = arg.as_str() {
239 if !s.is_empty() {
240 return arg;
241 }
242 } else {
243 return arg;
244 }
245 }
246 }
247 Value::UNDEFINED
248}
249
250pub fn haskey(value: Value, key: String) -> bool {
254 value
255 .get_attr(&key)
256 .map(|v| !v.is_undefined())
257 .unwrap_or(false)
258}
259
260pub fn keys(value: Value) -> Result<Vec<String>, Error> {
264 match value.try_iter() {
265 Ok(iter) => {
266 let keys: Vec<String> = iter
267 .filter_map(|v| v.as_str().map(|s| s.to_string()))
268 .collect();
269 Ok(keys)
270 }
271 Err(_) => Err(Error::new(
272 ErrorKind::InvalidOperation,
273 "cannot get keys from non-mapping value",
274 )),
275 }
276}
277
278pub fn merge(base: Value, overlay: Value) -> Result<Value, Error> {
282 let mut base_json: serde_json::Value = serde_json::to_value(&base)
283 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
284 let overlay_json: serde_json::Value = serde_json::to_value(&overlay)
285 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
286
287 deep_merge_json(&mut base_json, &overlay_json);
288
289 Ok(Value::from_serialize(&base_json))
290}
291
292fn deep_merge_json(base: &mut serde_json::Value, overlay: &serde_json::Value) {
293 match (base, overlay) {
294 (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
295 for (key, overlay_value) in overlay_map {
296 match base_map.get_mut(key) {
297 Some(base_value) => deep_merge_json(base_value, overlay_value),
298 None => {
299 base_map.insert(key.clone(), overlay_value.clone());
300 }
301 }
302 }
303 }
304 (base, overlay) => {
305 *base = overlay.clone();
306 }
307 }
308}
309
310pub fn sha256sum(value: String) -> String {
314 use sha2::{Digest, Sha256};
315 let mut hasher = Sha256::new();
316 hasher.update(value.as_bytes());
317 format!("{:x}", hasher.finalize())
318}
319
320pub fn trunc(value: String, length: usize) -> String {
324 if value.len() <= length {
325 value
326 } else {
327 value.chars().take(length).collect()
328 }
329}
330
331pub fn trimprefix(value: String, prefix: String) -> String {
335 value.strip_prefix(&prefix).unwrap_or(&value).to_string()
336}
337
338pub fn trimsuffix(value: String, suffix: String) -> String {
342 value.strip_suffix(&suffix).unwrap_or(&value).to_string()
343}
344
345pub fn snakecase(value: String) -> String {
349 let mut result = String::with_capacity(value.len() + value.len() / 4);
351 let mut prev_upper = false;
352
353 for (i, c) in value.chars().enumerate() {
354 if c.is_uppercase() {
355 if i > 0 && !prev_upper {
356 result.push('_');
357 }
358 result.push(c.to_lowercase().next().unwrap_or(c));
359 prev_upper = true;
360 } else if c == '-' || c == ' ' {
361 result.push('_');
362 prev_upper = false;
363 } else {
364 result.push(c);
365 prev_upper = false;
366 }
367 }
368
369 result
370}
371
372pub fn kebabcase(value: String) -> String {
376 snakecase(value).replace('_', "-")
377}
378
379pub fn tostrings(value: Value, kwargs: minijinja::value::Kwargs) -> Result<Vec<String>, Error> {
406 let prefix: String = kwargs.get("prefix").ok().flatten().unwrap_or_default();
408 let suffix: String = kwargs.get("suffix").ok().flatten().unwrap_or_default();
409 let skip_empty: bool = kwargs.get("skip_empty").ok().flatten().unwrap_or(false);
410
411 kwargs.assert_all_used()?;
413
414 let has_prefix = !prefix.is_empty();
415 let has_suffix = !suffix.is_empty();
416
417 let convert_value = |v: Value| -> Option<String> {
418 if v.is_undefined() || v.is_none() {
420 if skip_empty {
421 return None;
422 }
423 return Some(String::new());
424 }
425
426 let s = if let Some(str_val) = v.as_str() {
427 str_val.to_string()
428 } else {
429 v.to_string()
430 };
431
432 if skip_empty && s.is_empty() {
434 return None;
435 }
436
437 if has_prefix || has_suffix {
439 let mut result = String::with_capacity(s.len() + prefix.len() + suffix.len());
440 result.push_str(&prefix);
441 result.push_str(&s);
442 result.push_str(&suffix);
443 Some(result)
444 } else {
445 Some(s)
446 }
447 };
448
449 match value.try_iter() {
450 Ok(iter) => {
451 let strings: Vec<String> = iter.filter_map(convert_value).collect();
452 Ok(strings)
453 }
454 Err(_) => {
455 match convert_value(value) {
457 Some(s) => Ok(vec![s]),
458 None => Ok(vec![]),
459 }
460 }
461 }
462}
463
464pub fn semver_match(version: Value, constraint: String) -> Result<bool, Error> {
475 let version_str = version
476 .as_str()
477 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "version must be a string"))?;
478
479 let version_clean = version_str.trim_start_matches('v');
481
482 let parsed_version = match Version::parse(version_clean) {
484 Ok(v) => v,
485 Err(_) => {
486 let parts: Vec<&str> = version_clean
488 .split('-')
489 .next()
490 .unwrap_or(version_clean)
491 .split('.')
492 .collect();
493
494 if parts.len() >= 3 {
495 let major: u64 = parts[0].parse().unwrap_or(0);
496 let minor: u64 = parts[1].parse().unwrap_or(0);
497 let patch: u64 = parts[2].parse().unwrap_or(0);
498 Version::new(major, minor, patch)
499 } else if parts.len() == 2 {
500 let major: u64 = parts[0].parse().unwrap_or(0);
501 let minor: u64 = parts[1].parse().unwrap_or(0);
502 Version::new(major, minor, 0)
503 } else {
504 return Err(Error::new(
505 ErrorKind::InvalidOperation,
506 format!("Invalid version format: {}", version_str),
507 ));
508 }
509 }
510 };
511
512 let constraint_clean = constraint.trim_start_matches(|c: char| c.is_whitespace());
514
515 let req = VersionReq::parse(constraint_clean)
517 .or_else(|_| {
518 let constraint_base = constraint_clean
520 .split('-')
521 .next()
522 .unwrap_or(constraint_clean);
523 VersionReq::parse(constraint_base)
524 })
525 .map_err(|e| {
526 Error::new(
527 ErrorKind::InvalidOperation,
528 format!("Invalid constraint '{}': {}", constraint, e),
529 )
530 })?;
531
532 Ok(req.matches(&parsed_version))
533}
534
535pub fn int(value: Value) -> Result<i64, Error> {
537 match value.kind() {
538 ValueKind::Number => {
539 if let Some(i) = value.as_i64() {
541 Ok(i)
542 } else if let Ok(f) = f64::try_from(value.clone()) {
543 Ok(f as i64)
544 } else {
545 Err(Error::new(
546 ErrorKind::InvalidOperation,
547 "Cannot convert to int",
548 ))
549 }
550 }
551 ValueKind::String => {
552 let s = value.as_str().unwrap_or("");
553 s.parse::<i64>()
554 .or_else(|_| s.parse::<f64>().map(|f| f as i64))
555 .map_err(|_| {
556 Error::new(
557 ErrorKind::InvalidOperation,
558 format!("Cannot parse '{}' as int", s),
559 )
560 })
561 }
562 ValueKind::Bool => Ok(if value.is_true() { 1 } else { 0 }),
563 _ => Err(Error::new(
564 ErrorKind::InvalidOperation,
565 format!("Cannot convert {:?} to int", value.kind()),
566 )),
567 }
568}
569
570pub fn float(value: Value) -> Result<f64, Error> {
572 match value.kind() {
573 ValueKind::Number => {
574 f64::try_from(value)
576 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "Cannot convert to float"))
577 }
578 ValueKind::String => {
579 let s = value.as_str().unwrap_or("");
580 s.parse::<f64>().map_err(|_| {
581 Error::new(
582 ErrorKind::InvalidOperation,
583 format!("Cannot parse '{}' as float", s),
584 )
585 })
586 }
587 ValueKind::Bool => Ok(if value.is_true() { 1.0 } else { 0.0 }),
588 _ => Err(Error::new(
589 ErrorKind::InvalidOperation,
590 format!("Cannot convert {:?} to float", value.kind()),
591 )),
592 }
593}
594
595pub fn abs(value: Value) -> Result<Value, Error> {
597 match value.kind() {
598 ValueKind::Number => {
599 if let Some(i) = value.as_i64() {
600 Ok(Value::from(i.abs()))
601 } else if let Ok(f) = f64::try_from(value) {
602 Ok(Value::from(f.abs()))
603 } else {
604 Err(Error::new(
605 ErrorKind::InvalidOperation,
606 "Cannot get absolute value",
607 ))
608 }
609 }
610 _ => Err(Error::new(
611 ErrorKind::InvalidOperation,
612 format!("abs requires a number, got {:?}", value.kind()),
613 )),
614 }
615}
616
617pub fn basename(path: String) -> String {
624 std::path::Path::new(&path)
625 .file_name()
626 .map(|s| s.to_string_lossy().to_string())
627 .unwrap_or_default()
628}
629
630pub fn dirname(path: String) -> String {
633 std::path::Path::new(&path)
634 .parent()
635 .map(|p| p.to_string_lossy().to_string())
636 .unwrap_or_default()
637}
638
639pub fn extname(path: String) -> String {
642 std::path::Path::new(&path)
643 .extension()
644 .map(|s| s.to_string_lossy().to_string())
645 .unwrap_or_default()
646}
647
648pub fn cleanpath(path: String) -> String {
651 let is_absolute = path.starts_with('/');
652 let mut parts: Vec<&str> = vec![];
653
654 for part in path.split('/') {
655 match part {
656 "" | "." => continue,
657 ".." => {
658 parts.pop();
659 }
660 _ => parts.push(part),
661 }
662 }
663
664 let result = parts.join("/");
665 if is_absolute {
666 format!("/{}", result)
667 } else if result.is_empty() {
668 ".".to_string()
669 } else {
670 result
671 }
672}
673
674pub fn regex_match(value: String, pattern: String) -> Result<bool, Error> {
681 regex::Regex::new(&pattern)
682 .map(|re| re.is_match(&value))
683 .map_err(|e| {
684 Error::new(
685 ErrorKind::InvalidOperation,
686 format!("invalid regex '{}': {}", pattern, e),
687 )
688 })
689}
690
691pub fn regex_replace(value: String, pattern: String, replacement: String) -> Result<String, Error> {
694 regex::Regex::new(&pattern)
695 .map(|re| re.replace_all(&value, replacement.as_str()).to_string())
696 .map_err(|e| {
697 Error::new(
698 ErrorKind::InvalidOperation,
699 format!("invalid regex '{}': {}", pattern, e),
700 )
701 })
702}
703
704pub fn regex_find(value: String, pattern: String) -> Result<String, Error> {
707 regex::Regex::new(&pattern)
708 .map(|re| {
709 re.find(&value)
710 .map(|m| m.as_str().to_string())
711 .unwrap_or_default()
712 })
713 .map_err(|e| {
714 Error::new(
715 ErrorKind::InvalidOperation,
716 format!("invalid regex '{}': {}", pattern, e),
717 )
718 })
719}
720
721pub fn regex_find_all(value: String, pattern: String) -> Result<Vec<String>, Error> {
724 regex::Regex::new(&pattern)
725 .map(|re| {
726 re.find_iter(&value)
727 .map(|m| m.as_str().to_string())
728 .collect()
729 })
730 .map_err(|e| {
731 Error::new(
732 ErrorKind::InvalidOperation,
733 format!("invalid regex '{}': {}", pattern, e),
734 )
735 })
736}
737
738pub fn values(dict: Value) -> Result<Value, Error> {
745 match dict.kind() {
746 ValueKind::Map => {
747 let items: Vec<Value> = dict
748 .try_iter()
749 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate dict"))?
750 .filter_map(|k| dict.get_item(&k).ok())
751 .collect();
752 Ok(Value::from(items))
753 }
754 _ => Err(Error::new(
755 ErrorKind::InvalidOperation,
756 format!("values requires a dict, got {:?}", dict.kind()),
757 )),
758 }
759}
760
761pub fn pick(dict: Value, keys: &[Value]) -> Result<Value, Error> {
764 match dict.kind() {
765 ValueKind::Map => {
766 let mut result = indexmap::IndexMap::new();
767 for key in keys {
768 if let Some(key_str) = key.as_str()
769 && let Ok(val) = dict.get_item(key)
770 {
771 result.insert(key_str.to_string(), val);
772 }
773 }
774 Ok(Value::from_iter(result))
775 }
776 _ => Err(Error::new(
777 ErrorKind::InvalidOperation,
778 format!("pick requires a dict, got {:?}", dict.kind()),
779 )),
780 }
781}
782
783pub fn omit(dict: Value, keys: &[Value]) -> Result<Value, Error> {
786 match dict.kind() {
787 ValueKind::Map => {
788 let exclude: std::collections::HashSet<String> = keys
789 .iter()
790 .filter_map(|k| k.as_str().map(|s| s.to_string()))
791 .collect();
792
793 let mut result = indexmap::IndexMap::new();
794 if let Ok(iter) = dict.try_iter() {
795 for key in iter {
796 if let Some(key_str) = key.as_str()
797 && !exclude.contains(key_str)
798 && let Ok(val) = dict.get_item(&key)
799 {
800 result.insert(key_str.to_string(), val);
801 }
802 }
803 }
804 Ok(Value::from_iter(result))
805 }
806 _ => Err(Error::new(
807 ErrorKind::InvalidOperation,
808 format!("omit requires a dict, got {:?}", dict.kind()),
809 )),
810 }
811}
812
813pub fn append(list: Value, item: Value) -> Result<Value, Error> {
820 match list.kind() {
821 ValueKind::Seq => {
822 let mut items: Vec<Value> = list
823 .try_iter()
824 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
825 .collect();
826 items.push(item);
827 Ok(Value::from(items))
828 }
829 _ => Err(Error::new(
830 ErrorKind::InvalidOperation,
831 format!("append requires a list, got {:?}", list.kind()),
832 )),
833 }
834}
835
836pub fn prepend(list: Value, item: Value) -> Result<Value, Error> {
839 match list.kind() {
840 ValueKind::Seq => {
841 let mut items: Vec<Value> = vec![item];
842 items.extend(
843 list.try_iter()
844 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?,
845 );
846 Ok(Value::from(items))
847 }
848 _ => Err(Error::new(
849 ErrorKind::InvalidOperation,
850 format!("prepend requires a list, got {:?}", list.kind()),
851 )),
852 }
853}
854
855pub fn concat(list1: Value, list2: Value) -> Result<Value, Error> {
858 match (list1.kind(), list2.kind()) {
859 (ValueKind::Seq, ValueKind::Seq) => {
860 let mut items: Vec<Value> = list1
861 .try_iter()
862 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate first list"))?
863 .collect();
864 items.extend(list2.try_iter().map_err(|_| {
865 Error::new(ErrorKind::InvalidOperation, "cannot iterate second list")
866 })?);
867 Ok(Value::from(items))
868 }
869 _ => Err(Error::new(
870 ErrorKind::InvalidOperation,
871 "concat requires two lists",
872 )),
873 }
874}
875
876pub fn without(list: Value, exclude: &[Value]) -> Result<Value, Error> {
879 match list.kind() {
880 ValueKind::Seq => {
881 let items: Vec<Value> = list
882 .try_iter()
883 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
884 .filter(|item| !exclude.contains(item))
885 .collect();
886 Ok(Value::from(items))
887 }
888 _ => Err(Error::new(
889 ErrorKind::InvalidOperation,
890 format!("without requires a list, got {:?}", list.kind()),
891 )),
892 }
893}
894
895pub fn compact(list: Value) -> Result<Value, Error> {
898 match list.kind() {
899 ValueKind::Seq => {
900 let items: Vec<Value> = list
901 .try_iter()
902 .map_err(|_| Error::new(ErrorKind::InvalidOperation, "cannot iterate list"))?
903 .filter(|item| match item.kind() {
904 ValueKind::Undefined | ValueKind::None => false,
905 ValueKind::String => !item.as_str().unwrap_or("").is_empty(),
906 ValueKind::Seq => item.len().unwrap_or(0) > 0,
907 ValueKind::Map => item.len().unwrap_or(0) > 0,
908 _ => true,
909 })
910 .collect();
911 Ok(Value::from(items))
912 }
913 _ => Err(Error::new(
914 ErrorKind::InvalidOperation,
915 format!("compact requires a list, got {:?}", list.kind()),
916 )),
917 }
918}
919
920pub fn floor(value: Value) -> Result<i64, Error> {
927 match value.kind() {
928 ValueKind::Number => {
929 if let Some(i) = value.as_i64() {
930 Ok(i)
931 } else if let Ok(f) = f64::try_from(value) {
932 Ok(f.floor() as i64)
933 } else {
934 Err(Error::new(
935 ErrorKind::InvalidOperation,
936 "cannot convert to number",
937 ))
938 }
939 }
940 _ => Err(Error::new(
941 ErrorKind::InvalidOperation,
942 format!("floor requires a number, got {:?}", value.kind()),
943 )),
944 }
945}
946
947pub fn ceil(value: Value) -> Result<i64, Error> {
950 match value.kind() {
951 ValueKind::Number => {
952 if let Some(i) = value.as_i64() {
953 Ok(i)
954 } else if let Ok(f) = f64::try_from(value) {
955 Ok(f.ceil() as i64)
956 } else {
957 Err(Error::new(
958 ErrorKind::InvalidOperation,
959 "cannot convert to number",
960 ))
961 }
962 }
963 _ => Err(Error::new(
964 ErrorKind::InvalidOperation,
965 format!("ceil requires a number, got {:?}", value.kind()),
966 )),
967 }
968}
969
970pub fn sha1sum(value: String) -> String {
977 use sha1::{Digest, Sha1};
978 let mut hasher = Sha1::new();
979 hasher.update(value.as_bytes());
980 format!("{:x}", hasher.finalize())
981}
982
983pub fn sha512sum(value: String) -> String {
986 use sha2::{Digest, Sha512};
987 let mut hasher = Sha512::new();
988 hasher.update(value.as_bytes());
989 format!("{:x}", hasher.finalize())
990}
991
992pub fn md5sum(value: String) -> String {
995 use md5::{Digest, Md5};
996 let mut hasher = Md5::new();
997 hasher.update(value.as_bytes());
998 format!("{:x}", hasher.finalize())
999}
1000
1001pub fn repeat(value: String, count: usize) -> String {
1008 value.repeat(count)
1009}
1010
1011pub fn camelcase(value: String) -> String {
1015 let mut result = String::with_capacity(value.len());
1016 let mut capitalize_next = false;
1017 let mut first = true;
1018
1019 for c in value.chars() {
1020 if c == '_' || c == '-' || c == ' ' {
1021 capitalize_next = true;
1022 } else if capitalize_next {
1023 result.extend(c.to_uppercase());
1024 capitalize_next = false;
1025 } else if first {
1026 result.extend(c.to_lowercase());
1027 first = false;
1028 } else {
1029 result.extend(c.to_lowercase());
1030 }
1031 }
1032
1033 result
1034}
1035
1036pub fn pascalcase(value: String) -> String {
1039 let mut result = String::with_capacity(value.len());
1040 let mut capitalize_next = true;
1041
1042 for c in value.chars() {
1043 if c == '_' || c == '-' || c == ' ' {
1044 capitalize_next = true;
1045 } else if capitalize_next {
1046 result.extend(c.to_uppercase());
1047 capitalize_next = false;
1048 } else {
1049 result.push(c);
1050 }
1051 }
1052
1053 result
1054}
1055
1056pub fn substr(value: String, start: usize, length: Option<usize>) -> String {
1060 let chars: Vec<char> = value.chars().collect();
1061 let end = length
1062 .map(|l| (start + l).min(chars.len()))
1063 .unwrap_or(chars.len());
1064 let start = start.min(chars.len());
1065 chars[start..end].iter().collect()
1066}
1067
1068pub fn wrap(value: String, width: usize) -> String {
1071 let mut result = String::with_capacity(value.len() + value.len() / width);
1072 let mut line_len = 0;
1073
1074 for word in value.split_whitespace() {
1075 let word_len = word.chars().count();
1076 if line_len > 0 && line_len + 1 + word_len > width {
1077 result.push('\n');
1078 line_len = 0;
1079 } else if line_len > 0 {
1080 result.push(' ');
1081 line_len += 1;
1082 }
1083 result.push_str(word);
1084 line_len += word_len;
1085 }
1086
1087 result
1088}
1089
1090pub fn hasprefix(value: String, prefix: String) -> bool {
1093 value.starts_with(&prefix)
1094}
1095
1096pub fn hassuffix(value: String, suffix: String) -> bool {
1099 value.ends_with(&suffix)
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use super::*;
1105
1106 #[test]
1107 fn test_toyaml() {
1108 let value = Value::from_serialize(serde_json::json!({
1109 "name": "test",
1110 "port": 8080
1111 }));
1112 let yaml = toyaml(value).unwrap();
1113 assert!(yaml.contains("name: test"));
1114 assert!(yaml.contains("port: 8080"));
1115 }
1116
1117 #[test]
1118 fn test_fromjson_object() {
1119 let v = fromjson(r#"{"name":"test","port":8080}"#.to_string()).unwrap();
1120 assert_eq!(v.get_attr("name").unwrap().to_string(), "test");
1121 assert_eq!(v.get_attr("port").unwrap().as_i64().unwrap(), 8080);
1122 }
1123
1124 #[test]
1125 fn test_fromjson_array() {
1126 let v = fromjson(r#"[1,2,3]"#.to_string()).unwrap();
1127 let len = v.len().unwrap();
1128 assert_eq!(len, 3);
1129 }
1130
1131 #[test]
1132 fn test_fromjson_invalid() {
1133 let err = fromjson("not json".to_string()).unwrap_err();
1134 assert!(err.to_string().contains("fromjson"));
1135 }
1136
1137 #[test]
1138 fn test_fromyaml_object() {
1139 let yaml = "name: test\nport: 8080\n".to_string();
1140 let v = fromyaml(yaml).unwrap();
1141 assert_eq!(v.get_attr("name").unwrap().to_string(), "test");
1142 assert_eq!(v.get_attr("port").unwrap().as_i64().unwrap(), 8080);
1143 }
1144
1145 #[test]
1146 fn test_fromyaml_nested() {
1147 let yaml = "a:\n b:\n c: deep\n".to_string();
1148 let v = fromyaml(yaml).unwrap();
1149 let a = v.get_attr("a").unwrap();
1150 let b = a.get_attr("b").unwrap();
1151 assert_eq!(b.get_attr("c").unwrap().to_string(), "deep");
1152 }
1153
1154 #[test]
1155 fn test_fromyaml_invalid() {
1156 let err = fromyaml("a: [1, 2".to_string()).unwrap_err();
1158 assert!(err.to_string().contains("fromyaml"));
1159 }
1160
1161 #[test]
1162 fn test_tojson_fromjson_roundtrip() {
1163 let original = Value::from_serialize(serde_json::json!({
1164 "list": [1, 2, 3],
1165 "nested": {"key": "value"}
1166 }));
1167 let json_str = tojson(original.clone()).unwrap();
1168 let parsed = fromjson(json_str).unwrap();
1169 let reparsed_str = tojson(parsed).unwrap();
1170 let original_str = tojson(original).unwrap();
1171 assert_eq!(reparsed_str, original_str);
1172 }
1173
1174 #[test]
1175 fn test_b64encode_decode() {
1176 let original = "hello world".to_string();
1177 let encoded = b64encode(original.clone());
1178 let decoded = b64decode(encoded).unwrap();
1179 assert_eq!(original, decoded);
1180 }
1181
1182 #[test]
1183 fn test_quote() {
1184 assert_eq!(quote(Value::from("test")), "\"test\"");
1185 assert_eq!(squote(Value::from("test")), "'test'");
1186 }
1187
1188 #[test]
1189 fn test_nindent() {
1190 let input = "line1\nline2".to_string();
1191 let result = nindent(input, 4);
1192 assert_eq!(result, "\n line1\n line2");
1193 }
1194
1195 #[test]
1196 fn test_required() {
1197 assert!(required(Value::from("test"), None).is_ok());
1198 assert!(required(Value::UNDEFINED, None).is_err());
1199 assert!(required(Value::from(""), None).is_err());
1200 }
1201
1202 #[test]
1203 fn test_empty() {
1204 assert!(empty(Value::UNDEFINED));
1205 assert!(empty(Value::from("")));
1206 assert!(empty(Value::from_serialize(Vec::<i32>::new())));
1207 assert!(!empty(Value::from("test")));
1208 }
1209
1210 #[test]
1211 fn test_trunc() {
1212 assert_eq!(trunc("hello".to_string(), 3), "hel");
1213 assert_eq!(trunc("hi".to_string(), 10), "hi");
1214 }
1215
1216 #[test]
1217 fn test_snakecase() {
1218 assert_eq!(snakecase("camelCase".to_string()), "camel_case");
1219 assert_eq!(snakecase("PascalCase".to_string()), "pascal_case");
1220 }
1221
1222 #[test]
1223 fn test_tostrings_list() {
1224 use minijinja::Environment;
1225
1226 let mut env = Environment::new();
1227 env.add_filter("tostrings", tostrings);
1228
1229 let result: Vec<String> = env
1230 .render_str("{{ [1, 2, 3] | tostrings }}", ())
1231 .unwrap()
1232 .trim_matches(|c| c == '[' || c == ']')
1233 .split(", ")
1234 .map(|s| s.trim_matches('"').to_string())
1235 .collect();
1236 assert_eq!(result, vec!["1", "2", "3"]);
1237 }
1238
1239 #[test]
1240 fn test_tostrings_with_prefix() {
1241 use minijinja::Environment;
1242
1243 let mut env = Environment::new();
1244 env.add_filter("tostrings", tostrings);
1245
1246 let template = r#"{{ [80, 443] | tostrings(prefix="port-") | join(",") }}"#;
1247 let result = env.render_str(template, ()).unwrap();
1248 assert_eq!(result, "port-80,port-443");
1249 }
1250
1251 #[test]
1252 fn test_tostrings_with_suffix() {
1253 use minijinja::Environment;
1254
1255 let mut env = Environment::new();
1256 env.add_filter("tostrings", tostrings);
1257
1258 let template = r#"{{ [1, 2] | tostrings(suffix="/TCP") | join(",") }}"#;
1259 let result = env.render_str(template, ()).unwrap();
1260 assert_eq!(result, "1/TCP,2/TCP");
1261 }
1262
1263 #[test]
1264 fn test_tostrings_skip_empty() {
1265 use minijinja::Environment;
1266
1267 let mut env = Environment::new();
1268 env.add_filter("tostrings", tostrings);
1269
1270 let template = r#"{{ ["a", "", "c"] | tostrings(skip_empty=true) | join(",") }}"#;
1271 let result = env.render_str(template, ()).unwrap();
1272 assert_eq!(result, "a,c");
1273 }
1274
1275 #[test]
1276 fn test_tostrings_mixed() {
1277 use minijinja::Environment;
1278
1279 let mut env = Environment::new();
1280 env.add_filter("tostrings", tostrings);
1281
1282 let template = r#"{{ ["hello", 42, true] | tostrings | join(",") }}"#;
1283 let result = env.render_str(template, ()).unwrap();
1284 assert_eq!(result, "hello,42,true");
1285 }
1286
1287 #[test]
1288 fn test_int_filter() {
1289 assert_eq!(int(Value::from(42)).unwrap(), 42);
1291 assert_eq!(int(Value::from(3.7)).unwrap(), 3);
1293 assert_eq!(int(Value::from(-3.7)).unwrap(), -3);
1294 assert_eq!(int(Value::from("123")).unwrap(), 123);
1296 assert_eq!(int(Value::from("45.9")).unwrap(), 45);
1297 assert_eq!(int(Value::from(true)).unwrap(), 1);
1299 assert_eq!(int(Value::from(false)).unwrap(), 0);
1300 }
1301
1302 #[test]
1303 fn test_float_filter() {
1304 let result = float(Value::from(2.5)).unwrap();
1306 assert!((result - 2.5).abs() < 0.001);
1307 let result = float(Value::from(42)).unwrap();
1309 assert!((result - 42.0).abs() < 0.001);
1310 let result = float(Value::from("2.5")).unwrap();
1312 assert!((result - 2.5).abs() < 0.001);
1313 assert_eq!(float(Value::from(true)).unwrap(), 1.0);
1315 assert_eq!(float(Value::from(false)).unwrap(), 0.0);
1316 }
1317
1318 #[test]
1319 fn test_abs_filter() {
1320 let result = abs(Value::from(5)).unwrap();
1322 assert_eq!(result.as_i64().unwrap(), 5);
1323 let result = abs(Value::from(-5)).unwrap();
1325 assert_eq!(result.as_i64().unwrap(), 5);
1326 let result = abs(Value::from(2.5)).unwrap();
1328 assert!((f64::try_from(result).unwrap() - 2.5).abs() < 0.001);
1329 let result = abs(Value::from(-2.5)).unwrap();
1331 assert!((f64::try_from(result).unwrap() - 2.5).abs() < 0.001);
1332 let result = abs(Value::from(0)).unwrap();
1334 assert_eq!(result.as_i64().unwrap(), 0);
1335 }
1336
1337 #[test]
1342 fn test_basename() {
1343 assert_eq!(basename("/etc/nginx/nginx.conf".to_string()), "nginx.conf");
1344 assert_eq!(basename("file.txt".to_string()), "file.txt");
1345 assert_eq!(basename("/path/to/dir/".to_string()), "dir"); assert_eq!(basename("/".to_string()), "");
1347 assert_eq!(basename("".to_string()), "");
1348 }
1349
1350 #[test]
1351 fn test_dirname() {
1352 assert_eq!(dirname("/etc/nginx/nginx.conf".to_string()), "/etc/nginx");
1353 assert_eq!(dirname("file.txt".to_string()), "");
1354 assert_eq!(dirname("/single".to_string()), "/");
1355 assert_eq!(dirname("a/b/c".to_string()), "a/b");
1356 }
1357
1358 #[test]
1359 fn test_extname() {
1360 assert_eq!(extname("file.txt".to_string()), "txt");
1361 assert_eq!(extname("archive.tar.gz".to_string()), "gz");
1362 assert_eq!(extname("noext".to_string()), "");
1363 assert_eq!(extname(".hidden".to_string()), "");
1364 }
1365
1366 #[test]
1367 fn test_cleanpath() {
1368 assert_eq!(cleanpath("a/b/../c".to_string()), "a/c");
1369 assert_eq!(cleanpath("a/./b/./c".to_string()), "a/b/c");
1370 assert_eq!(cleanpath("/a/b/../c".to_string()), "/a/c");
1371 assert_eq!(cleanpath("../a".to_string()), "a");
1372 assert_eq!(cleanpath("".to_string()), ".");
1373 }
1374
1375 #[test]
1380 fn test_regex_match() {
1381 assert!(regex_match("v1.2.3".to_string(), r"^v\d+".to_string()).unwrap());
1382 assert!(!regex_match("1.2.3".to_string(), r"^v\d+".to_string()).unwrap());
1383 assert!(regex_match("hello@world.com".to_string(), r"@.*\.".to_string()).unwrap());
1384 }
1385
1386 #[test]
1387 fn test_regex_replace() {
1388 assert_eq!(
1389 regex_replace(
1390 "v1.2.3".to_string(),
1391 r"v(\d+)".to_string(),
1392 "version-$1".to_string()
1393 )
1394 .unwrap(),
1395 "version-1.2.3"
1396 );
1397 assert_eq!(
1398 regex_replace(
1399 "foo bar baz".to_string(),
1400 r"\s+".to_string(),
1401 "-".to_string()
1402 )
1403 .unwrap(),
1404 "foo-bar-baz"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_regex_find() {
1410 assert_eq!(
1411 regex_find("port: 8080".to_string(), r"\d+".to_string()).unwrap(),
1412 "8080"
1413 );
1414 assert_eq!(
1415 regex_find("no numbers".to_string(), r"\d+".to_string()).unwrap(),
1416 ""
1417 );
1418 }
1419
1420 #[test]
1421 fn test_regex_find_all() {
1422 let result = regex_find_all("a1b2c3".to_string(), r"\d+".to_string()).unwrap();
1423 assert_eq!(result, vec!["1", "2", "3"]);
1424 }
1425
1426 #[test]
1431 fn test_values_filter() {
1432 use minijinja::Environment;
1433 let mut env = Environment::new();
1434 env.add_filter("values", values);
1435
1436 let result = env
1437 .render_str(r#"{{ {"a": 1, "b": 2} | values | sort | list }}"#, ())
1438 .unwrap();
1439 assert!(result.contains("1") && result.contains("2"));
1440 }
1441
1442 #[test]
1443 fn test_pick_filter() {
1444 use minijinja::Environment;
1445 let mut env = Environment::new();
1446 env.add_filter("pick", pick);
1447
1448 let result = env
1449 .render_str(r#"{{ {"a": 1, "b": 2, "c": 3} | pick("a", "c") }}"#, ())
1450 .unwrap();
1451 assert!(result.contains("a") && result.contains("c") && !result.contains("b"));
1452 }
1453
1454 #[test]
1455 fn test_omit_filter() {
1456 use minijinja::Environment;
1457 let mut env = Environment::new();
1458 env.add_filter("omit", omit);
1459
1460 let result = env
1461 .render_str(r#"{{ {"a": 1, "b": 2, "c": 3} | omit("b") }}"#, ())
1462 .unwrap();
1463 assert!(result.contains("a") && result.contains("c") && !result.contains(": 2"));
1464 }
1465
1466 #[test]
1471 fn test_append_filter() {
1472 use minijinja::Environment;
1473 let mut env = Environment::new();
1474 env.add_filter("append", append);
1475
1476 let result = env.render_str(r#"{{ [1, 2] | append(3) }}"#, ()).unwrap();
1477 assert_eq!(result, "[1, 2, 3]");
1478 }
1479
1480 #[test]
1481 fn test_prepend_filter() {
1482 use minijinja::Environment;
1483 let mut env = Environment::new();
1484 env.add_filter("prepend", prepend);
1485
1486 let result = env.render_str(r#"{{ [2, 3] | prepend(1) }}"#, ()).unwrap();
1487 assert_eq!(result, "[1, 2, 3]");
1488 }
1489
1490 #[test]
1491 fn test_concat_filter() {
1492 use minijinja::Environment;
1493 let mut env = Environment::new();
1494 env.add_filter("concat", concat);
1495
1496 let result = env
1497 .render_str(r#"{{ [1, 2] | concat([3, 4]) }}"#, ())
1498 .unwrap();
1499 assert_eq!(result, "[1, 2, 3, 4]");
1500 }
1501
1502 #[test]
1503 fn test_without_filter() {
1504 use minijinja::Environment;
1505 let mut env = Environment::new();
1506 env.add_filter("without", without);
1507
1508 let result = env
1509 .render_str(r#"{{ [1, 2, 3, 2] | without(2) }}"#, ())
1510 .unwrap();
1511 assert_eq!(result, "[1, 3]");
1512 }
1513
1514 #[test]
1515 fn test_compact_filter() {
1516 use minijinja::Environment;
1517 let mut env = Environment::new();
1518 env.add_filter("compact", compact);
1519
1520 let result = env
1521 .render_str(r#"{{ ["a", "", "b"] | compact }}"#, ())
1522 .unwrap();
1523 assert!(result.contains("a") && result.contains("b") && !result.contains(r#""""#));
1524 }
1525
1526 #[test]
1531 fn test_floor_filter() {
1532 assert_eq!(floor(Value::from(3.7)).unwrap(), 3);
1533 assert_eq!(floor(Value::from(3.2)).unwrap(), 3);
1534 assert_eq!(floor(Value::from(-3.2)).unwrap(), -4);
1535 assert_eq!(floor(Value::from(5)).unwrap(), 5);
1536 }
1537
1538 #[test]
1539 fn test_ceil_filter() {
1540 assert_eq!(ceil(Value::from(3.2)).unwrap(), 4);
1541 assert_eq!(ceil(Value::from(3.7)).unwrap(), 4);
1542 assert_eq!(ceil(Value::from(-3.7)).unwrap(), -3);
1543 assert_eq!(ceil(Value::from(5)).unwrap(), 5);
1544 }
1545
1546 #[test]
1551 fn test_sha1sum() {
1552 assert_eq!(
1554 sha1sum("hello".to_string()),
1555 "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_sha512sum() {
1561 let result = sha512sum("hello".to_string());
1563 assert!(result.starts_with("9b71d224bd62f3785d96d46ad3ea3d73"));
1564 assert_eq!(result.len(), 128); }
1566
1567 #[test]
1568 fn test_md5sum() {
1569 assert_eq!(
1571 md5sum("hello".to_string()),
1572 "5d41402abc4b2a76b9719d911017c592"
1573 );
1574 }
1575
1576 #[test]
1581 fn test_repeat_filter() {
1582 assert_eq!(repeat("-".to_string(), 5), "-----");
1583 assert_eq!(repeat("ab".to_string(), 3), "ababab");
1584 assert_eq!(repeat("x".to_string(), 0), "");
1585 }
1586
1587 #[test]
1588 fn test_camelcase_filter() {
1589 assert_eq!(camelcase("foo_bar_baz".to_string()), "fooBarBaz");
1590 assert_eq!(camelcase("foo-bar-baz".to_string()), "fooBarBaz");
1591 assert_eq!(camelcase("FOO_BAR".to_string()), "fooBar");
1592 assert_eq!(camelcase("already".to_string()), "already");
1593 }
1594
1595 #[test]
1596 fn test_pascalcase_filter() {
1597 assert_eq!(pascalcase("foo_bar".to_string()), "FooBar");
1598 assert_eq!(pascalcase("foo-bar-baz".to_string()), "FooBarBaz");
1599 assert_eq!(pascalcase("hello".to_string()), "Hello");
1600 }
1601
1602 #[test]
1603 fn test_substr_filter() {
1604 assert_eq!(substr("hello world".to_string(), 0, Some(5)), "hello");
1605 assert_eq!(substr("hello world".to_string(), 6, None), "world");
1606 assert_eq!(substr("hello".to_string(), 10, Some(5)), "");
1607 assert_eq!(substr("hello".to_string(), 0, Some(100)), "hello");
1608 }
1609
1610 #[test]
1611 fn test_wrap_filter() {
1612 assert_eq!(
1613 wrap("hello world foo bar".to_string(), 10),
1614 "hello\nworld foo\nbar"
1615 );
1616 assert_eq!(wrap("short".to_string(), 20), "short");
1617 }
1618
1619 #[test]
1620 fn test_hasprefix_filter() {
1621 assert!(hasprefix("hello world".to_string(), "hello".to_string()));
1622 assert!(!hasprefix("hello world".to_string(), "world".to_string()));
1623 }
1624
1625 #[test]
1626 fn test_hassuffix_filter() {
1627 assert!(hassuffix("hello.txt".to_string(), ".txt".to_string()));
1628 assert!(!hassuffix("hello.txt".to_string(), ".yaml".to_string()));
1629 }
1630}