add serde_keyvalue crate

Add a crate for deserializing command-line options given as key-values.

This crate leverages serde to deserialize key-value argument strings (a
commonly used pattern in crosvm) into configuration structures. This
will allow us to remove a big part of the manual parsing currently done
in `main.rs`, will provide consistent arguments to the `crosvm run` and
`crosvm device` commands, and results in more precise error reporting.

The use of serde will also allow us to similarly deserialize
configuration structures from configuration files with very little extra
code involved.

As explained in the crate's documentation, its main entry point is a
`from_key_values` function that allows a build a struct implementing
serde's `Deserialize` trait from a key/values string.

In order to integrate transparently with `argh`, there is also a
`FromKeyValue` derive macro that automatically implements `FromArgValue`
for structs deriving from it.

BUG=b:218223240
BUG=b:217480043
TEST=cargo build
TEST=cargo test -p serde_keyvalue

Change-Id: Id6316e40150d5f08a05e6f04e39ecbc73d72dfa0
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/3439669
Reviewed-by: Keiichi Watanabe <keiichiw@chromium.org>
Reviewed-by: Anton Romanov <romanton@google.com>
Tested-by: kokoro <noreply+kokoro@google.com>
Commit-Queue: Alexandre Courbot <acourbot@chromium.org>
This commit is contained in:
Alexandre Courbot 2022-02-03 19:39:10 +09:00 committed by Chromeos LUCI
parent 2558b26297
commit aa043e8c00
7 changed files with 1419 additions and 0 deletions

View file

@ -70,6 +70,7 @@ members = [
"qcow_utils",
"resources",
"rutabaga_gfx",
"serde_keyvalue",
"system_api_stub",
"tpm2",
"tpm2-sys",
@ -172,6 +173,7 @@ remain = "*"
resources = { path = "resources" }
scudo = { version = "0.1", optional = true }
serde_json = "*"
serde_keyvalue = { path = "serde_keyvalue" }
sync = { path = "common/sync" }
tempfile = "3"
thiserror = { version = "1.0.20" }

18
serde_keyvalue/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "serde_keyvalue"
version = "0.1.0"
authors = ["The Chromium OS Authors"]
edition = "2021"
[features]
argh_derive = ["argh", "serde_keyvalue_derive"]
[dependencies]
argh = { version = "0.1.7", optional = true }
serde_keyvalue_derive = { path = "serde_keyvalue_derive", optional = true }
serde = "1"
thiserror = { version = "1.0.20" }
remain = "*"
[dev-dependencies]
serde = { version = "1", features = ["derive"] }

19
serde_keyvalue/README.md Normal file
View file

@ -0,0 +1,19 @@
# Serde deserializer from key=value strings
A lightweight serde deserializer for strings containing key-value pairs separated by commas, as
commonly found in command-line parameters.
Say your program takes a command-line option of the form:
```text
--foo type=bar,active,nb_threads=8
```
This crate provides a `from_key_values` function that deserializes these key-values into a
configuration structure. Since it uses serde, the same configuration structure can also be created
from any other supported source (such as a TOML or YAML configuration file) that uses the same keys.
Integration with the [argh](https://github.com/google/argh) command-line parser is also provided via
the `argh_derive` feature.
See the inline documentation for examples and more details.

View file

@ -0,0 +1,13 @@
[package]
name = "serde_keyvalue_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
argh = "0.1.7"
proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"

View file

@ -0,0 +1,24 @@
// Copyright 2022 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Implement `argh`'s `FromArgValue` trait for a struct or enum using `from_key_values`.
#[proc_macro_derive(FromKeyValues)]
pub fn keyvalues_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let DeriveInput {
ident, generics, ..
} = parse_macro_input!(input);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics ::serde_keyvalue::argh::FromArgValue for #ident #ty_generics #where_clause {
fn from_arg_value(value: &str) -> std::result::Result<Self, std::string::String> {
::serde_keyvalue::from_key_values(value).map_err(|e| e.to_string())
}
}
}
.into()
}

File diff suppressed because it is too large Load diff

295
serde_keyvalue/src/lib.rs Normal file
View file

@ -0,0 +1,295 @@
// Copyright 2022 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! A lightweight serde deserializer for strings containing key-value pairs separated by commas, as
//! commonly found in command-line parameters.
//!
//! Say your program takes a command-line option of the form:
//!
//! ```text
//! --foo type=bar,active,nb_threads=8
//! ```
//!
//! This crate provides a [from_key_values] function that deserializes these key-values into a
//! configuration structure. Since it uses serde, the same configuration structure can also be
//! created from any other supported source (such as a TOML or YAML configuration file) that uses
//! the same keys.
//!
//! Integration with the [argh](https://github.com/google/argh) command-line parser is also
//! provided via the `argh_derive` feature.
//!
//! The deserializer supports parsing signed and unsigned integers, booleans, strings (quoted or
//! not), paths, and enums inside a top-level struct. The order in which the fields appear in the
//! string is not important.
//!
//! Simple example:
//!
//! ```
//! use serde_keyvalue::from_key_values;
//! use serde::Deserialize;
//!
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: String,
//! threads: u8,
//! active: bool,
//! }
//!
//! let config: Config = from_key_values("path=/some/path,threads=16,active=true").unwrap();
//! assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
//!
//! let config: Config = from_key_values("threads=16,active=true,path=/some/path").unwrap();
//! assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
//! ```
//!
//! As a convenience the name of the first field of a struct can be omitted:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: String,
//! threads: u8,
//! active: bool,
//! }
//!
//! let config: Config = from_key_values("/some/path,threads=16,active=true").unwrap();
//! assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
//! ```
//!
//! Fields that are behind an `Option` can be omitted, in which case they will be `None`.
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: Option<String>,
//! threads: u8,
//! active: bool,
//! }
//!
//! let config: Config = from_key_values("path=/some/path,threads=16,active=true").unwrap();
//! assert_eq!(config, Config { path: Some("/some/path".into()), threads: 16, active: true });
//!
//! let config: Config = from_key_values("threads=16,active=true").unwrap();
//! assert_eq!(config, Config { path: None, threads: 16, active: true });
//! ```
//!
//! Alternatively, the serde `default` attribute can be used on select fields or on the whole
//! struct to make unspecified fields be assigned their default value. In the following example only
//! the `path` parameter must be specified.
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: String,
//! #[serde(default)]
//! threads: u8,
//! #[serde(default)]
//! active: bool,
//! }
//!
//! let config: Config = from_key_values("path=/some/path").unwrap();
//! assert_eq!(config, Config { path: "/some/path".into(), threads: 0, active: false });
//! ```
//!
//! A function providing a default value can also be specified, see the [serde documentation for
//! field attributes](https://serde.rs/field-attrs.html) for details.
//!
//! Booleans can be `true` or `false`, or take no value at all, in which case they will be `true`.
//! Combined with default values this allows to implement flags very easily:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, Default, PartialEq, Deserialize)]
//! #[serde(default)]
//! struct Config {
//! active: bool,
//! delayed: bool,
//! pooled: bool,
//! }
//!
//! let config: Config = from_key_values("active=true,delayed=false,pooled=true").unwrap();
//! assert_eq!(config, Config { active: true, delayed: false, pooled: true });
//!
//! let config: Config = from_key_values("active,pooled").unwrap();
//! assert_eq!(config, Config { active: true, delayed: false, pooled: true });
//! ```
//!
//! Strings can be quoted, which is useful if they e.g. need to include a comma. Quoted strings can
//! also contain escaped characters, where any character after a `\` is repeated as-is:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: String,
//! }
//!
//! let config: Config = from_key_values(r#"path="/some/\"strange\"/pa,th""#).unwrap();
//! assert_eq!(config, Config { path: r#"/some/"strange"/pa,th"#.into() });
//! ```
//!
//! Enums can be directly specified by name. It is recommended to use the `rename_all` serde
//! container attribute to make them parseable using snake or kebab case representation. Serde's
//! `rename` and `alias` field attributes can also be used to provide shorter values:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! #[serde(rename_all="kebab-case")]
//! enum Mode {
//! Slow,
//! Fast,
//! #[serde(rename="ludicrous")]
//! LudicrousSpeed,
//! }
//!
//! #[derive(Deserialize, PartialEq, Debug)]
//! struct Config {
//! mode: Mode,
//! }
//!
//! let config: Config = from_key_values("mode=slow").unwrap();
//! assert_eq!(config, Config { mode: Mode::Slow });
//!
//! let config: Config = from_key_values("mode=ludicrous").unwrap();
//! assert_eq!(config, Config { mode: Mode::LudicrousSpeed });
//! ```
//!
//! Enums taking a single value should use the `flatten` field attribute in order to be inferred
//! from their variant key directly:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! #[serde(rename_all="kebab-case")]
//! enum Mode {
//! // Work with a local file.
//! File(String),
//! // Work with a remote URL.
//! Url(String),
//! }
//!
//! #[derive(Deserialize, PartialEq, Debug)]
//! struct Config {
//! #[serde(flatten)]
//! mode: Mode,
//! }
//!
//! let config: Config = from_key_values("file=/some/path").unwrap();
//! assert_eq!(config, Config { mode: Mode::File("/some/path".into()) });
//!
//! let config: Config = from_key_values("url=https://www.google.com").unwrap();
//! assert_eq!(config, Config { mode: Mode::Url("https://www.google.com".into()) });
//! ```
//!
//! The `flatten` attribute can also be used to embed one struct within another one and parse both
//! from the same string:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct BaseConfig {
//! enabled: bool,
//! num_threads: u8,
//! }
//!
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! #[serde(flatten)]
//! base: BaseConfig,
//! path: String,
//! }
//!
//! let config: Config = from_key_values("path=/some/path,enabled,num_threads=16").unwrap();
//! assert_eq!(
//! config,
//! Config {
//! path: "/some/path".into(),
//! base: BaseConfig {
//! num_threads: 16,
//! enabled: true,
//! }
//! }
//! );
//! ```
//!
//! If an enum's variants are made of structs, it should take the `untagged` container attribute so
//! it can be inferred directly from the fields of the embedded structs:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! #[serde(untagged)]
//! enum Mode {
//! // Work with a local file.
//! File {
//! path: String,
//! #[serde(default)]
//! read_only: bool,
//! },
//! // Work with a remote URL.
//! Remote {
//! server: String,
//! port: u16,
//! }
//! }
//!
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! #[serde(flatten)]
//! mode: Mode,
//! }
//!
//! let config: Config = from_key_values("path=/some/path").unwrap();
//! assert_eq!(config, Config { mode: Mode::File { path: "/some/path".into(), read_only: false } });
//!
//! let config: Config = from_key_values("server=google.com,port=80").unwrap();
//! assert_eq!(config, Config { mode: Mode::Remote { server: "google.com".into(), port: 80 } });
//! ```
//!
//! Using this crate, parsing errors and invalid or missing fields are precisely reported:
//!
//! ```
//! # use serde_keyvalue::from_key_values;
//! # use serde::Deserialize;
//! #[derive(Debug, PartialEq, Deserialize)]
//! struct Config {
//! path: String,
//! threads: u8,
//! active: bool,
//! }
//!
//! let config = from_key_values::<Config>("path=/some/path,active=true").unwrap_err();
//! assert_eq!(format!("{}", config), "missing field `threads`");
//! ```
//!
//! Most of the serde [container](https://serde.rs/container-attrs.html) and
//! [field](https://serde.rs/field-attrs.html) attributes can be applied to your configuration
//! struct. Most useful ones include
//! [`deny_unknown_fields`](https://serde.rs/container-attrs.html#deny_unknown_fields) to report an
//! error if an unknown field is met in the input, and
//! [`deserialize_with`](https://serde.rs/field-attrs.html#deserialize_with) to use a custom
//! deserialization function for a specific field.
#![deny(missing_docs)]
mod key_values;
pub use key_values::{from_key_values, ErrorKind, ParseError};
#[cfg(feature = "argh_derive")]
pub use argh;
#[cfg(feature = "argh_derive")]
pub use serde_keyvalue_derive::FromKeyValues;