systemprompt_cloud/deploy/provider/
values_file.rs1#[derive(Debug, thiserror::Error, PartialEq, Eq)]
13pub enum ValuesFileError {
14 #[error("the key path is empty or has an empty segment")]
15 EmptyPath,
16 #[error("'{path}' holds a sequence, not a mapping")]
17 Sequence { path: String },
18 #[error("'{path}' is a mapping, not a scalar")]
19 Mapping { path: String },
20 #[error("'{path}' is a scalar, not a mapping")]
21 Scalar { path: String },
22}
23
24struct KeyLine<'a> {
25 raw_key: &'a str,
26 key: &'a str,
27 rest: &'a str,
28}
29
30fn indent_of(line: &str) -> usize {
31 line.len() - line.trim_start_matches(' ').len()
32}
33
34fn is_content(line: &str) -> bool {
35 let trimmed = line.trim();
36 !trimmed.is_empty() && !trimmed.starts_with('#')
37}
38
39fn key_line(line: &str) -> Option<KeyLine<'_>> {
40 let body = line.trim_start_matches(' ');
41 if body.starts_with('-') || body.starts_with('{') || body.starts_with('[') {
42 return None;
43 }
44 let colon = body
45 .char_indices()
46 .find(|&(i, c)| c == ':' && body[i + 1..].chars().next().is_none_or(|n| n == ' '))
47 .map(|(i, _)| i)?;
48 let raw_key = &body[..colon];
49 let key = raw_key
50 .strip_prefix('"')
51 .and_then(|k| k.strip_suffix('"'))
52 .or_else(|| {
53 raw_key
54 .strip_prefix('\'')
55 .and_then(|k| k.strip_suffix('\''))
56 })
57 .unwrap_or(raw_key);
58 Some(KeyLine {
59 raw_key,
60 key,
61 rest: body[colon + 1..].trim(),
62 })
63}
64
65fn trailing_comment(rest: &str) -> &str {
66 if rest.starts_with('#') {
67 return rest;
68 }
69 rest.find(" #").map_or("", |pos| &rest[pos..])
70}
71
72fn holds_value(rest: &str) -> bool {
73 !rest.is_empty() && !rest.starts_with('#')
74}
75
76fn quote(value: &str) -> String {
77 format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
78}
79
80fn block_end(lines: &[&str], from: usize, owner_indent: usize) -> usize {
81 (from..lines.len())
82 .find(|&i| is_content(lines[i]) && indent_of(lines[i]) <= owner_indent)
83 .unwrap_or(lines.len())
84}
85
86struct Block {
87 start: usize,
88 end: usize,
89 owner_indent: Option<usize>,
90}
91
92pub fn set_yaml_scalar(
93 content: &str,
94 key_path: &[&str],
95 value: &str,
96) -> Result<String, ValuesFileError> {
97 if key_path.is_empty() || key_path.iter().any(|segment| segment.is_empty()) {
98 return Err(ValuesFileError::EmptyPath);
99 }
100 let view: Vec<&str> = content.lines().collect();
101 let mut lines: Vec<String> = view.iter().map(|line| (*line).to_owned()).collect();
102 let mut block = Block {
103 start: 0,
104 end: view.len(),
105 owner_indent: None,
106 };
107
108 for (depth, segment) in key_path.iter().enumerate() {
109 let child_indent = first_child_indent(&view, &block, &key_path[..depth])?;
110 let Some(index) = find_key(&view, &block, child_indent, segment) else {
111 let base =
112 child_indent.unwrap_or_else(|| block.owner_indent.map_or(0, |owner| owner + 2));
113 let insert_at = insert_point(&view, &block);
114 insert_path(&mut lines, insert_at, base, &key_path[depth..], value);
115 return Ok(render(&lines, content));
116 };
117
118 let line_indent = indent_of(view[index]);
119 let rest = key_line(view[index]).map_or("", |parsed| parsed.rest);
120 let child_end = block_end(&view, index + 1, line_indent);
121
122 if depth + 1 == key_path.len() {
123 let has_children = view[index + 1..child_end].iter().any(|l| is_content(l));
124 if has_children && !holds_value(rest) {
125 return Err(ValuesFileError::Mapping {
126 path: key_path.join("."),
127 });
128 }
129 lines[index] = replaced_line(view[index], value);
130 return Ok(render(&lines, content));
131 }
132
133 if holds_value(rest) {
134 return Err(ValuesFileError::Scalar {
135 path: key_path[..=depth].join("."),
136 });
137 }
138 block = Block {
139 start: index + 1,
140 end: child_end,
141 owner_indent: Some(line_indent),
142 };
143 }
144 Err(ValuesFileError::EmptyPath)
145}
146
147fn first_child_indent(
148 view: &[&str],
149 block: &Block,
150 parent_path: &[&str],
151) -> Result<Option<usize>, ValuesFileError> {
152 let Some(first) = view[block.start..block.end]
153 .iter()
154 .find(|line| is_content(line))
155 else {
156 return Ok(None);
157 };
158 if first.trim_start().starts_with('-') {
159 return Err(ValuesFileError::Sequence {
160 path: parent_path.join("."),
161 });
162 }
163 Ok(Some(indent_of(first)))
164}
165
166fn find_key(
167 view: &[&str],
168 block: &Block,
169 child_indent: Option<usize>,
170 segment: &str,
171) -> Option<usize> {
172 (block.start..block.end).find(|&i| {
173 is_content(view[i])
174 && Some(indent_of(view[i])) == child_indent
175 && key_line(view[i]).is_some_and(|k| k.key == segment)
176 })
177}
178
179fn insert_point(view: &[&str], block: &Block) -> usize {
180 (block.start..block.end)
181 .rev()
182 .find(|&i| is_content(view[i]))
183 .map_or(block.start, |i| i + 1)
184}
185
186fn insert_path(lines: &mut Vec<String>, insert_at: usize, base: usize, keys: &[&str], value: &str) {
187 for (offset, key) in keys.iter().enumerate() {
188 let pad = " ".repeat(base + offset * 2);
189 let line = if offset + 1 == keys.len() {
190 format!("{pad}{key}: {}", quote(value))
191 } else {
192 format!("{pad}{key}:")
193 };
194 lines.insert(insert_at + offset, line);
195 }
196}
197
198fn replaced_line(line: &str, value: &str) -> String {
199 let indent = indent_of(line);
200 let Some(parsed) = key_line(line) else {
201 return line.to_owned();
202 };
203 let comment = trailing_comment(parsed.rest);
204 let separator = if comment.is_empty() || comment.starts_with(' ') {
205 ""
206 } else {
207 " "
208 };
209 format!(
210 "{}{}: {}{separator}{comment}",
211 " ".repeat(indent),
212 parsed.raw_key,
213 quote(value)
214 )
215}
216
217fn render(lines: &[String], original: &str) -> String {
218 let mut out = lines.join("\n");
219 if original.ends_with('\n') || original.is_empty() {
220 out.push('\n');
221 }
222 out
223}