codex_patcher/toml/
query.rs1use crate::toml::errors::TomlError;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct SectionPath {
6 parts: Vec<String>,
7}
8
9impl SectionPath {
10 pub fn new(parts: Vec<String>) -> Result<Self, TomlError> {
11 if parts.is_empty() {
12 return Err(TomlError::InvalidSectionPath {
13 input: "".to_string(),
14 message: "empty section path".to_string(),
15 });
16 }
17 Ok(Self { parts })
18 }
19
20 pub fn parse(input: &str) -> Result<Self, TomlError> {
21 let parts = parse_dotted_path(input)?;
22 if parts.is_empty() {
23 return Err(TomlError::InvalidSectionPath {
24 input: input.to_string(),
25 message: "empty section path".to_string(),
26 });
27 }
28 Ok(Self { parts })
29 }
30
31 pub fn parts(&self) -> &[String] {
32 &self.parts
33 }
34
35 pub fn as_string(&self) -> String {
36 self.parts.join(".")
37 }
38}
39
40impl fmt::Display for SectionPath {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}", self.as_string())
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct KeyPath {
48 parts: Vec<String>,
49}
50
51impl KeyPath {
52 pub fn parse(input: &str) -> Result<Self, TomlError> {
53 let parts = parse_dotted_path(input)?;
54 if parts.is_empty() {
55 return Err(TomlError::InvalidSectionPath {
56 input: input.to_string(),
57 message: "empty key path".to_string(),
58 });
59 }
60 Ok(Self { parts })
61 }
62
63 pub fn parts(&self) -> &[String] {
64 &self.parts
65 }
66
67 pub fn as_string(&self) -> String {
68 self.parts.join(".")
69 }
70}
71
72impl fmt::Display for KeyPath {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}", self.as_string())
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum TomlQuery {
80 Section { path: SectionPath },
81 Key { section: SectionPath, key: KeyPath },
82}
83
84fn parse_dotted_path(input: &str) -> Result<Vec<String>, TomlError> {
85 let mut parts = Vec::new();
86 let mut current = String::new();
87 let mut chars = input.chars().peekable();
88 let mut in_quotes = false;
89 let mut quote_char = '\0';
90 let mut quote_open_pos: usize = 0;
91 let mut char_idx: usize = 0;
92
93 while let Some(ch) = chars.next() {
94 if in_quotes {
95 if ch == quote_char {
96 in_quotes = false;
97 continue;
98 }
99
100 if quote_char == '"' && ch == '\\' {
101 if let Some(next) = chars.next() {
102 let escaped = match next {
103 '"' => '"',
104 '\\' => '\\',
105 'n' => '\n',
106 't' => '\t',
107 'r' => '\r',
108 other => other,
109 };
110 current.push(escaped);
111 continue;
112 }
113 }
114
115 current.push(ch);
116 continue;
117 }
118
119 match ch {
120 '.' => {
121 if current.is_empty() {
122 return Err(TomlError::InvalidSectionPath {
123 input: input.to_string(),
124 message: "empty path segment".to_string(),
125 });
126 }
127 parts.push(current.clone());
128 current.clear();
129 }
130 '"' | '\'' => {
131 if !current.is_empty() {
132 return Err(TomlError::InvalidSectionPath {
133 input: input.to_string(),
134 message: "unexpected quote inside key".to_string(),
135 });
136 }
137 in_quotes = true;
138 quote_char = ch;
139 quote_open_pos = char_idx;
140 }
141 ch if ch.is_whitespace() => {
142 return Err(TomlError::InvalidSectionPath {
143 input: input.to_string(),
144 message: "whitespace not allowed in key".to_string(),
145 });
146 }
147 other => current.push(other),
148 }
149 char_idx += 1;
150 }
151
152 if in_quotes {
153 return Err(TomlError::InvalidSectionPath {
154 input: input.to_string(),
155 message: format!(
156 "unterminated quoted key (opening {quote_char} at position {quote_open_pos})"
157 ),
158 });
159 }
160
161 if !current.is_empty() {
162 parts.push(current);
163 }
164
165 Ok(parts)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn parse_section_path_basic() {
174 let path = SectionPath::parse("profile.zack").unwrap();
175 assert_eq!(path.parts(), &["profile", "zack"]);
176 }
177
178 #[test]
179 fn parse_section_path_quoted() {
180 let path = SectionPath::parse("profile.\"zack.test\"").unwrap();
181 assert_eq!(path.parts(), &["profile", "zack.test"]);
182 }
183
184 #[test]
185 fn parse_key_path_dotted() {
186 let key = KeyPath::parse("target.x86_64").unwrap();
187 assert_eq!(key.parts(), &["target", "x86_64"]);
188 }
189}