risingwave_common/config/mutate.rs
1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::Context;
16use itertools::Itertools as _;
17use serde::Serialize;
18use toml::{Table, Value};
19
20def_anyhow_newtype! {
21 /// Error type for mutating a configuration [`toml::Table`].
22 pub ConfigMutateError,
23
24 toml::ser::Error => transparent,
25}
26
27enum Op {
28 Upsert(Value),
29 Delete,
30}
31
32/// Mutate a [`toml::Table`] at the given dot-separated path.
33///
34/// Returns an error if the path is invalid.
35fn mutate(map: &mut Table, path: &str, op: Op) -> Result<(), ConfigMutateError> {
36 let segments = path.split('.').collect_vec();
37 let (key, segments) = segments.split_last().context("empty path")?;
38
39 let mut map = map;
40 for segment in segments {
41 use toml::map::Entry;
42
43 let sub_map = match map.entry(segment.to_owned()) {
44 Entry::Occupied(entry) => entry.into_mut(),
45 Entry::Vacant(entry) => match &op {
46 // Create new tables if not exists.
47 Op::Upsert(_) => entry.insert(Value::Table(Table::new())),
48 // Delete from a non-existing path is a no-op.
49 Op::Delete => return Ok(()),
50 },
51 }
52 .as_table_mut()
53 .with_context(|| format!("expect a table at {segment}"))?;
54
55 map = sub_map;
56 }
57
58 match op {
59 Op::Upsert(value) => {
60 map.insert(key.to_string(), value);
61 }
62 Op::Delete => {
63 map.remove(*key);
64 }
65 }
66
67 Ok(())
68}
69
70/// Extension trait for [`toml::Table`] to mutate values at a given dot-separated path.
71#[easy_ext::ext(TomlTableMutateExt)]
72impl Table {
73 /// Upsert a value at the given dot-separated path.
74 ///
75 /// Returns an error if the path or the value is invalid.
76 pub fn upsert(&mut self, path: &str, value: impl Serialize) -> Result<(), ConfigMutateError> {
77 let value = Value::try_from(value)?;
78 mutate(self, path, Op::Upsert(value))
79 }
80
81 /// Delete a value at the given path.
82 ///
83 /// Returns an error if the path is invalid.
84 pub fn delete(&mut self, path: &str) -> Result<(), ConfigMutateError> {
85 mutate(self, path, Op::Delete)
86 }
87}