Skip to main content

sanitize_engine/
strip_values.rs

1//! Key-only structure extraction for configuration files.
2//!
3//! Strips the value side of every `key = value` line, leaving only the key
4//! and delimiter. Comments, blank lines, and section headers (lines without a
5//! delimiter, such as `[section]`) are passed through unchanged.
6//!
7//! This is useful for sharing a configuration file's structure (e.g. for a
8//! code review or LLM prompt) without exposing the actual values.
9//!
10//! # Example
11//!
12//! ```rust
13//! use sanitize_engine::strip_values_from_text;
14//!
15//! let input = "# db settings\nhost = localhost\nport = 5432\n";
16//! let output = strip_values_from_text(input, "=", "#");
17//!
18//! assert!(output.contains("host =\n"));
19//! assert!(output.contains("port =\n"));
20//! assert!(!output.contains("localhost"));
21//! assert!(output.contains("# db settings\n"));
22//! ```
23
24/// Strip values from `content`, preserving keys, comments, and structure.
25///
26/// For each line:
27/// - Lines that are empty or start with `comment_prefix` are emitted unchanged.
28/// - Lines containing `delimiter` have everything after the first occurrence
29///   of the delimiter removed (the delimiter itself is kept).
30/// - Lines without a delimiter (e.g. section headers like `[section]`) are
31///   emitted unchanged.
32#[must_use]
33pub fn strip_values_from_text(content: &str, delimiter: &str, comment_prefix: &str) -> String {
34    let mut out = String::with_capacity(content.len() / 2);
35    for line in content.lines() {
36        let trimmed = line.trim();
37        if trimmed.is_empty() || trimmed.starts_with(comment_prefix) {
38            out.push_str(line);
39            out.push('\n');
40            continue;
41        }
42        if let Some(delim_pos) = line.find(delimiter) {
43            let raw_key = &line[..delim_pos];
44            out.push_str(raw_key);
45            out.push_str(delimiter);
46            out.push('\n');
47        } else {
48            out.push_str(line);
49            out.push('\n');
50        }
51    }
52    out
53}
54
55// ---------------------------------------------------------------------------
56// Tests
57// ---------------------------------------------------------------------------
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn removes_values_preserves_keys() {
65        let out = strip_values_from_text("host = localhost\nport = 5432\n", "=", "#");
66        assert!(out.contains("host =\n"), "key should remain, got:\n{out}");
67        assert!(out.contains("port =\n"), "key should remain, got:\n{out}");
68        assert!(!out.contains("localhost"), "value should be stripped");
69        assert!(!out.contains("5432"), "value should be stripped");
70    }
71
72    #[test]
73    fn preserves_comments_and_blank_lines() {
74        let input = "# a comment\n\nkey = value\n";
75        let out = strip_values_from_text(input, "=", "#");
76        assert!(out.contains("# a comment\n"), "comment should be preserved");
77        assert!(!out.contains("value"), "value should be stripped");
78        assert!(
79            out.contains("\n\n") || out.starts_with('\n'),
80            "blank line should be preserved"
81        );
82    }
83
84    #[test]
85    fn preserves_section_headers() {
86        let out = strip_values_from_text("[database]\nhost = localhost\n", "=", "#");
87        assert!(
88            out.contains("[database]\n"),
89            "section header should be preserved"
90        );
91        assert!(!out.contains("localhost"), "value should be stripped");
92    }
93
94    #[test]
95    fn no_delimiter_line_passes_through() {
96        let out = strip_values_from_text("just a bare line\n", "=", "#");
97        assert_eq!(out, "just a bare line\n");
98    }
99}