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