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}