Line data Source code
1 : //! Check for fields in the on-disk config file that were ignored when
2 : //! deserializing [`pageserver_api::config::ConfigToml`].
3 : //!
4 : //! This could have been part of the [`pageserver_api::config`] module,
5 : //! but the way we identify unused fields in this module
6 : //! is specific to the format (TOML) and the implementation of the
7 : //! deserialization for that format ([`toml_edit`]).
8 :
9 : use std::collections::HashSet;
10 :
11 : use itertools::Itertools;
12 :
13 : /// Pass in the user-specified config and the re-serialized [`pageserver_api::config::ConfigToml`].
14 : /// The returned [`Paths`] contains the paths to the fields that were ignored by deserialization
15 : /// of the [`pageserver_api::config::ConfigToml`].
16 48 : pub fn find(user_specified: toml_edit::DocumentMut, reserialized: toml_edit::DocumentMut) -> Paths {
17 48 : let user_specified = paths(user_specified);
18 48 : let reserialized = paths(reserialized);
19 96 : fn paths(doc: toml_edit::DocumentMut) -> HashSet<String> {
20 96 : let mut out = Vec::new();
21 96 : let mut visitor = PathsVisitor::new(&mut out);
22 96 : visitor.visit_table_like(doc.as_table());
23 96 : HashSet::from_iter(out)
24 96 : }
25 :
26 48 : let mut ignored = HashSet::new();
27 :
28 : // O(n) because of HashSet
29 144 : for path in user_specified {
30 96 : if !reserialized.contains(&path) {
31 48 : ignored.insert(path);
32 48 : }
33 : }
34 :
35 48 : Paths {
36 48 : paths: ignored
37 48 : .into_iter()
38 48 : // sort lexicographically for deterministic output
39 48 : .sorted()
40 48 : .collect(),
41 48 : }
42 48 : }
43 :
44 : pub struct Paths {
45 : pub paths: Vec<String>,
46 : }
47 :
48 : struct PathsVisitor<'a> {
49 : stack: Vec<String>,
50 : out: &'a mut Vec<String>,
51 : }
52 :
53 : impl<'a> PathsVisitor<'a> {
54 96 : fn new(out: &'a mut Vec<String>) -> Self {
55 96 : Self {
56 96 : stack: Vec::new(),
57 96 : out,
58 96 : }
59 96 : }
60 :
61 216 : fn visit_table_like(&mut self, table_like: &dyn toml_edit::TableLike) {
62 288 : for (entry, item) in table_like.iter() {
63 288 : self.stack.push(entry.to_string());
64 288 : self.visit_item(item);
65 288 : self.stack.pop();
66 288 : }
67 216 : }
68 :
69 288 : fn visit_item(&mut self, item: &toml_edit::Item) {
70 288 : match item {
71 0 : toml_edit::Item::None => (),
72 192 : toml_edit::Item::Value(value) => self.visit_value(value),
73 72 : toml_edit::Item::Table(table) => {
74 72 : self.visit_table_like(table);
75 72 : }
76 24 : toml_edit::Item::ArrayOfTables(array_of_tables) => {
77 24 : for (i, table) in array_of_tables.iter().enumerate() {
78 24 : self.stack.push(format!("[{i}]"));
79 24 : self.visit_table_like(table);
80 24 : self.stack.pop();
81 24 : }
82 : }
83 : }
84 288 : }
85 :
86 216 : fn visit_value(&mut self, value: &toml_edit::Value) {
87 216 : match value {
88 : toml_edit::Value::String(_)
89 : | toml_edit::Value::Integer(_)
90 : | toml_edit::Value::Float(_)
91 : | toml_edit::Value::Boolean(_)
92 168 : | toml_edit::Value::Datetime(_) => self.out.push(self.stack.join(".")),
93 24 : toml_edit::Value::Array(array) => {
94 24 : for (i, value) in array.iter().enumerate() {
95 24 : self.stack.push(format!("[{i}]"));
96 24 : self.visit_value(value);
97 24 : self.stack.pop();
98 24 : }
99 : }
100 24 : toml_edit::Value::InlineTable(inline_table) => self.visit_table_like(inline_table),
101 : }
102 216 : }
103 : }
104 :
105 : #[cfg(test)]
106 : pub(crate) mod tests {
107 :
108 48 : fn test_impl(original: &str, parsed: &str, expect: [&str; 1]) {
109 48 : let original: toml_edit::DocumentMut = original.parse().expect("parse original config");
110 48 : let parsed: toml_edit::DocumentMut = parsed.parse().expect("parse re-serialized config");
111 48 :
112 48 : let super::Paths { paths: actual } = super::find(original, parsed);
113 48 : assert_eq!(actual, &expect);
114 48 : }
115 :
116 : #[test]
117 12 : fn top_level() {
118 12 : test_impl(
119 12 : r#"
120 12 : [a]
121 12 : b = 1
122 12 : c = 2
123 12 : d = 3
124 12 : "#,
125 12 : r#"
126 12 : [a]
127 12 : b = 1
128 12 : c = 2
129 12 : "#,
130 12 : ["a.d"],
131 12 : );
132 12 : }
133 :
134 : #[test]
135 12 : fn nested() {
136 12 : test_impl(
137 12 : r#"
138 12 : [a.b.c]
139 12 : d = 23
140 12 : "#,
141 12 : r#"
142 12 : [a]
143 12 : e = 42
144 12 : "#,
145 12 : ["a.b.c.d"],
146 12 : );
147 12 : }
148 :
149 : #[test]
150 12 : fn array_of_tables() {
151 12 : test_impl(
152 12 : r#"
153 12 : [[a]]
154 12 : b = 1
155 12 : c = 2
156 12 : d = 3
157 12 : "#,
158 12 : r#"
159 12 : [[a]]
160 12 : b = 1
161 12 : c = 2
162 12 : "#,
163 12 : ["a.[0].d"],
164 12 : );
165 12 : }
166 :
167 : #[test]
168 12 : fn array() {
169 12 : test_impl(
170 12 : r#"
171 12 : foo = [ {bar = 23} ]
172 12 : "#,
173 12 : r#"
174 12 : foo = [ { blup = 42 }]
175 12 : "#,
176 12 : ["foo.[0].bar"],
177 12 : );
178 12 : }
179 : }
|