system_power_mode_config/
lib.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{bail, ensure, Context as _, Error};
6pub use fidl_fuchsia_power_clientlevel::ClientType;
7pub use fidl_fuchsia_power_systemmode::{ClientConfig, ModeMatch, SystemMode};
8use serde::Deserialize;
9use std::collections::{HashMap, HashSet};
10use std::fs::File;
11use std::io::Read as _;
12use std::path::Path;
13
14/// This library is used to parse a system power mode configuration JSON file into a data structure
15/// which also implements some convenience methods for accessing and consuming the data.
16///
17/// The intended usage is that `SystemPowerModeConfig::read()` is called with the path to a system
18/// power mode configuration JSON file. If successful, the function returns a
19/// `SystemPowerModeConfig` instance containing the parsed configuration.
20///
21/// The parser expects a a valid JSON5 serialization of `SystemPowerModeConfig`, such as the
22/// following:
23/// ```
24///   {
25///     clients: {
26///       wlan: {
27///         mode_matches: [
28///           {
29///             mode: 'battery_saver',
30///             level: 0,
31///           },
32///         ],
33///         default_level: 1,
34///       },
35///     },
36///   }
37/// ```
38
39/// Represents the top level of a system power mode configuration.
40///
41/// This struct wraps the contents of a system power mode configuration file. All of the types
42/// contained within this top level struct are defined in the `fuchsia.power.clientlevel` and
43/// `fuchsia.power.systemmode` libraries.
44#[derive(Debug, PartialEq)]
45pub struct SystemPowerModeConfig {
46    clients: HashMap<ClientType, ClientConfig>,
47}
48
49impl SystemPowerModeConfig {
50    /// Reads the supplied JSON file path and parses into a `SystemPowerModeConfig` instance.
51    ///
52    /// Attempts to open, read, and parse the supplied JSON file into a valid
53    /// `SystemPowerModeConfig` instance. If parsing was successful, then it is also tested for
54    /// validity. If a `SystemPowerModeConfig` instance could not be created, or validation fails,
55    /// then an error is returned.
56    pub fn read(json_file_path: &Path) -> Result<SystemPowerModeConfig, Error> {
57        let mut buffer = String::new();
58        File::open(&json_file_path)
59            .context(format!("Failed to open file at path {}", json_file_path.display()))?
60            .read_to_string(&mut buffer)?;
61
62        let config = Self::from_json_str(&buffer)?;
63        config.validate().context("SystemPowerModeConfig validation failed")?;
64        Ok(config)
65    }
66
67    /// Attempts to create a `SystemPowerModeConfig` instance from a JSON5 string.
68    ///
69    /// The function eases deserialization by using locally defined structs consisting only of types
70    /// that are naturally supported by Serde's deserialization system (these local types end in
71    /// "De"). If deserialization of these structs succeeds, then they are converted to the desired
72    /// `SystemPowerModeConfig` struct with the help of `TryInto` impls and helper functions.
73    fn from_json_str(json: &str) -> Result<SystemPowerModeConfig, Error> {
74        #[derive(Deserialize)]
75        struct SystemPowerModeConfigDe {
76            clients: HashMap<String, ClientConfigDe>,
77        }
78
79        #[derive(Deserialize)]
80        struct ClientConfigDe {
81            mode_matches: Vec<ModeMatchDe>,
82            default_level: u64,
83        }
84
85        #[derive(Deserialize)]
86        struct ModeMatchDe {
87            mode: String,
88            power_level: u64,
89        }
90
91        impl TryInto<SystemPowerModeConfig> for SystemPowerModeConfigDe {
92            type Error = anyhow::Error;
93            fn try_into(self) -> Result<SystemPowerModeConfig, Self::Error> {
94                let mut clients = HashMap::new();
95                for (k, v) in self.clients.into_iter() {
96                    clients.insert(str_to_client_type(&k)?, v.try_into()?);
97                }
98                Ok(SystemPowerModeConfig { clients })
99            }
100        }
101
102        impl TryInto<ClientConfig> for ClientConfigDe {
103            type Error = Error;
104            fn try_into(self) -> Result<ClientConfig, Self::Error> {
105                let mode_matches = self
106                    .mode_matches
107                    .into_iter()
108                    .map(|m| m.try_into())
109                    .collect::<Result<Vec<_>, _>>()?;
110                Ok(ClientConfig { mode_matches, default_level: self.default_level })
111            }
112        }
113
114        impl TryInto<ModeMatch> for ModeMatchDe {
115            type Error = Error;
116            fn try_into(self) -> Result<ModeMatch, Self::Error> {
117                Ok(ModeMatch {
118                    mode: str_to_system_mode(&self.mode)?,
119                    power_level: self.power_level,
120                })
121            }
122        }
123
124        let deserializer: SystemPowerModeConfigDe = serde_json5::from_str(json)?;
125        deserializer.try_into()
126    }
127
128    /// Validates the configuration.
129    pub fn validate(&self) -> Result<(), Error> {
130        // Iterate and validate each underlying `ClientConfig` instance
131        for (client_name, client_config) in self.clients.iter() {
132            client_config
133                .validate()
134                .context(format!("Validation failed for client {:?}", client_name))?;
135        }
136
137        Ok(())
138    }
139
140    /// Gets the `ClientConfig` instance for the specified client.
141    pub fn get_client_config(&self, client_type: ClientType) -> Option<&ClientConfig> {
142        self.clients.get(&client_type)
143    }
144
145    pub fn into_iter(self) -> impl Iterator<Item = (ClientType, ClientConfig)> {
146        self.clients.into_iter()
147    }
148}
149
150/// Parses a string into a `ClientType` variant.
151///
152/// To successfully parse, the string must be a lower snake case representation of the `ClientType`
153/// variant.
154fn str_to_client_type(s: &str) -> Result<ClientType, Error> {
155    Ok(match s {
156        "wlan" => ClientType::Wlan,
157        _ => bail!("Unsupported client type '{}'", s),
158    })
159}
160
161/// Parses a string into a `SystemMode` variant.
162///
163/// To successfully parse, the string must be a lower snake case representation of the `SystemMode`
164/// variant.
165fn str_to_system_mode(s: &str) -> Result<SystemMode, Error> {
166    // Since `SystemMode` doesn't actually contain any variants today, we bail unconditionally here.
167    // Once `SystemMode` grows one or more variants then we should string match to return the
168    // correct one (same as in `str_to_client_type`).
169
170    bail!("Unsupported system mode '{}'", s)
171}
172
173/// A trait that adds useful test-only methods to the `SystemPowerModeConfig` struct.
174///
175/// This trait and its methods are not marked `cfg(test)` so that they may be used outside of this
176/// crate.
177pub trait SystemPowerModeConfigTestExt {
178    fn new() -> Self;
179    fn add_client_config(self, client_type: ClientType, config: ClientConfig) -> Self;
180}
181
182impl SystemPowerModeConfigTestExt for SystemPowerModeConfig {
183    /// Creates an empty `SystemPowerModeConfig` (no configured clients).
184    fn new() -> Self {
185        Self { clients: HashMap::new() }
186    }
187
188    /// Adds a configuration entry for the specified client.
189    fn add_client_config(mut self, client_type: ClientType, config: ClientConfig) -> Self {
190        self.clients.insert(client_type, config);
191        self
192    }
193}
194
195/// A trait that adds useful test-only methods to the `ClientConfig` struct.
196///
197/// This trait and its methods are not marked `cfg(test)` so that they may be used outside of this
198/// crate.
199pub trait ClientConfigTestExt {
200    fn new(default_level: u64) -> Self;
201    fn append_mode_match(self, mode: SystemMode, level: u64) -> Self;
202}
203
204impl ClientConfigTestExt for ClientConfig {
205    /// Creates an empty `ClientConfig` which consists of a default level and no `ModeMatch`
206    /// entries.
207    fn new(default_level: u64) -> Self {
208        Self { mode_matches: Vec::new(), default_level }
209    }
210
211    /// Appends a mode match entry to the end of the existing entries.
212    fn append_mode_match(mut self, mode: SystemMode, power_level: u64) -> Self {
213        self.mode_matches.push(ModeMatch { mode, power_level });
214        self
215    }
216}
217
218pub trait ClientConfigExt {
219    fn validate(&self) -> Result<(), Error>;
220}
221
222impl ClientConfigExt for ClientConfig {
223    /// Validates a `ClientConfig` instance.
224    ///
225    /// Performs a series of validations to check if the configuration defined by the `ClientConfig`
226    /// instance is valid. The instance is valid if:
227    ///   1) a given `SystemMode` is not repeating in multiple `ModeMatch` entries
228    fn validate(&self) -> Result<(), Error> {
229        // Ensure a given `SystemMode` is not repeating in multiple `ModeMatch` entries
230        {
231            let mut modes_set = HashSet::new();
232            for mode in self.mode_matches.iter().map(|mode_match| mode_match.mode) {
233                ensure!(
234                    modes_set.insert(mode),
235                    "A given mode may only be specified once (violated by {:?})",
236                    mode
237                );
238            }
239        }
240
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use crate::*;
248    use assert_matches::assert_matches;
249
250    /// Tests that valid `SystemPowerModeConfig` instances pass the validation.
251    #[test]
252    fn test_system_power_mode_config_validation_success() {
253        // Empty config
254        let config = SystemPowerModeConfig::new();
255        assert_matches!(config.validate(), Ok(()));
256
257        // A single configured client with an arbitrary default power level
258        let config =
259            SystemPowerModeConfig::new().add_client_config(ClientType::Wlan, ClientConfig::new(0));
260        assert_matches!(config.validate(), Ok(()));
261    }
262
263    /// Tests that invalid `SystemPowerModeConfig` instances fail the validation.
264    #[test]
265    fn test_system_power_mode_config_validation_failures() {
266        // The only way validation can fail today is by having multiple `ModeMade` entries with a
267        // repeated `SystemMode`. We won't be able to test this path until `SystemMode` variants are
268        // added (currently there are none).
269    }
270
271    /// Tests for proper parsing by the `str_to_client_type` function.
272    ///
273    /// The test tries to parse each supported `ClientType` variant. Parsing should succeed for the
274    /// required lower snake case string representation and fail otherwise.
275    ///
276    /// Additional test cases should be added for each new `ClientType` variant as it grows.
277    #[test]
278    fn test_parse_client_types() {
279        assert_eq!(str_to_client_type("wlan").unwrap(), ClientType::Wlan);
280        assert!(str_to_client_type("Wlan").is_err());
281        assert!(str_to_client_type("WLAN").is_err());
282    }
283
284    /// Tests for proper parsing by the `str_to_system_mode` function.
285    ///
286    /// The test tries to parse each supported `SystemMode` variant. Parsing should succeed for the
287    /// required lower snake case string representation and fail otherwise.
288    ///
289    /// Additional test cases should be added for each new `SystemMode` variant as it grows.
290    #[test]
291    fn test_parse_system_modes() {
292        assert!(str_to_system_mode("").is_err());
293    }
294}