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 4 : pub fn find(user_specified: toml_edit::DocumentMut, reserialized: toml_edit::DocumentMut) -> Paths {
17 4 : let user_specified = paths(user_specified);
18 4 : let reserialized = paths(reserialized);
19 8 : fn paths(doc: toml_edit::DocumentMut) -> HashSet<String> {
20 8 : let mut out = Vec::new();
21 8 : let mut visitor = PathsVisitor::new(&mut out);
22 8 : visitor.visit_table_like(doc.as_table());
23 8 : HashSet::from_iter(out)
24 8 : }
25 :
26 4 : let mut ignored = HashSet::new();
27 :
28 : // O(n) because of HashSet
29 12 : for path in user_specified {
30 8 : if !reserialized.contains(&path) {
31 4 : ignored.insert(path);
32 4 : }
33 : }
34 :
35 4 : Paths {
36 4 : paths: ignored
37 4 : .into_iter()
38 4 : // sort lexicographically for deterministic output
39 4 : .sorted()
40 4 : .collect(),
41 4 : }
42 4 : }
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 8 : fn new(out: &'a mut Vec<String>) -> Self {
55 8 : Self {
56 8 : stack: Vec::new(),
57 8 : out,
58 8 : }
59 8 : }
60 :
61 18 : fn visit_table_like(&mut self, table_like: &dyn toml_edit::TableLike) {
62 24 : for (entry, item) in table_like.iter() {
63 24 : self.stack.push(entry.to_string());
64 24 : self.visit_item(item);
65 24 : self.stack.pop();
66 24 : }
67 18 : }
68 :
69 24 : fn visit_item(&mut self, item: &toml_edit::Item) {
70 24 : match item {
71 0 : toml_edit::Item::None => (),
72 16 : toml_edit::Item::Value(value) => self.visit_value(value),
73 6 : toml_edit::Item::Table(table) => {
74 6 : self.visit_table_like(table);
75 6 : }
76 2 : toml_edit::Item::ArrayOfTables(array_of_tables) => {
77 2 : for (i, table) in array_of_tables.iter().enumerate() {
78 2 : self.stack.push(format!("[{i}]"));
79 2 : self.visit_table_like(table);
80 2 : self.stack.pop();
81 2 : }
82 : }
83 : }
84 24 : }
85 :
86 18 : fn visit_value(&mut self, value: &toml_edit::Value) {
87 18 : match value {
88 : toml_edit::Value::String(_)
89 : | toml_edit::Value::Integer(_)
90 : | toml_edit::Value::Float(_)
91 : | toml_edit::Value::Boolean(_)
92 14 : | toml_edit::Value::Datetime(_) => self.out.push(self.stack.join(".")),
93 2 : toml_edit::Value::Array(array) => {
94 2 : for (i, value) in array.iter().enumerate() {
95 2 : self.stack.push(format!("[{i}]"));
96 2 : self.visit_value(value);
97 2 : self.stack.pop();
98 2 : }
99 : }
100 2 : toml_edit::Value::InlineTable(inline_table) => self.visit_table_like(inline_table),
101 : }
102 18 : }
103 : }
104 :
105 : #[cfg(test)]
106 : pub(crate) mod tests {
107 :
108 4 : fn test_impl(original: &str, parsed: &str, expect: [&str; 1]) {
109 4 : let original: toml_edit::DocumentMut = original.parse().expect("parse original config");
110 4 : let parsed: toml_edit::DocumentMut = parsed.parse().expect("parse re-serialized config");
111 :
112 4 : let super::Paths { paths: actual } = super::find(original, parsed);
113 4 : assert_eq!(actual, &expect);
114 4 : }
115 :
116 : #[test]
117 1 : fn top_level() {
118 1 : test_impl(
119 1 : r#"
120 1 : [a]
121 1 : b = 1
122 1 : c = 2
123 1 : d = 3
124 1 : "#,
125 1 : r#"
126 1 : [a]
127 1 : b = 1
128 1 : c = 2
129 1 : "#,
130 1 : ["a.d"],
131 : );
132 1 : }
133 :
134 : #[test]
135 1 : fn nested() {
136 1 : test_impl(
137 1 : r#"
138 1 : [a.b.c]
139 1 : d = 23
140 1 : "#,
141 1 : r#"
142 1 : [a]
143 1 : e = 42
144 1 : "#,
145 1 : ["a.b.c.d"],
146 : );
147 1 : }
148 :
149 : #[test]
150 1 : fn array_of_tables() {
151 1 : test_impl(
152 1 : r#"
153 1 : [[a]]
154 1 : b = 1
155 1 : c = 2
156 1 : d = 3
157 1 : "#,
158 1 : r#"
159 1 : [[a]]
160 1 : b = 1
161 1 : c = 2
162 1 : "#,
163 1 : ["a.[0].d"],
164 : );
165 1 : }
166 :
167 : #[test]
168 1 : fn array() {
169 1 : test_impl(
170 1 : r#"
171 1 : foo = [ {bar = 23} ]
172 1 : "#,
173 1 : r#"
174 1 : foo = [ { blup = 42 }]
175 1 : "#,
176 1 : ["foo.[0].bar"],
177 : );
178 1 : }
179 : }
|