Add map default values (e.g. default = { "foo": 33 })

This commit also adds a bunch of tests testing maps in several
situations.
This commit is contained in:
Lukas Kalbertodt
2022-10-21 18:13:59 +02:00
parent 093957b515
commit d1a62e47eb
21 changed files with 381 additions and 47 deletions

View File

@@ -2,7 +2,7 @@ use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::Ident;
use crate::ir::{self, Expr, FieldKind, LeafKind};
use crate::ir::{self, Expr, FieldKind, LeafKind, MapKey};
@@ -21,7 +21,7 @@ pub(super) fn gen(input: &ir::Input) -> TokenStream {
let name = f.name.to_string();
let doc = &f.doc;
let kind = match &f.kind {
FieldKind::Nested { ty }=> {
FieldKind::Nested { ty } => {
quote! {
confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META }
}
@@ -73,27 +73,50 @@ pub(super) fn gen(input: &ir::Input) -> TokenStream {
}
}
/// Helper macro to deduplicate logic for literals. Only used in the function
/// below.
macro_rules! match_literals {
($v:expr, $ty:expr, $ns:ident, { $($other_arms:tt)* }) => {
match $v {
$ns::Bool(v) => quote! { confique::meta::$ns::Bool(#v) },
$ns::Str(s) => quote! { confique::meta::$ns::Str(#s) },
$ns::Int(i) => {
let variant = infer_type(i.suffix(), $ty, "I32", int_type_to_variant);
quote! { confique::meta::$ns::Integer(confique::meta::Integer::#variant(#i)) }
}
$ns::Float(f) => {
let variant = infer_type(f.suffix(), $ty, "F64", float_type_to_variant);
quote! { confique::meta::$ns::Float(confique::meta::Float::#variant(#f)) }
}
$($other_arms)*
}
};
}
/// Generates the meta expression of type `meta::Expr` to be used for the
/// `default` field. `ty` is the type of the field that is used to better infer
/// the exact type of the default value.
fn default_value_to_meta_expr(default: &Expr, ty: Option<&syn::Type>) -> TokenStream {
match default {
Expr::Bool(v) => quote! { confique::meta::Expr::Bool(#v) },
Expr::Str(s) => quote! { confique::meta::Expr::Str(#s) },
Expr::Int(i) => {
let variant = infer_type(i.suffix(), ty, "I32", int_type_to_variant);
quote! { confique::meta::Expr::Integer(confique::meta::Integer::#variant(#i)) }
}
Expr::Float(f) => {
let variant = infer_type(f.suffix(), ty, "F64", float_type_to_variant);
quote! { confique::meta::Expr::Float(confique::meta::Float::#variant(#f)) }
}
match_literals!(default, ty, Expr, {
Expr::Array(items) => {
let item_type = ty.and_then(get_item_ty);
let item_type = ty.and_then(get_array_item_type);
let items = items.iter().map(|item| default_value_to_meta_expr(item, item_type));
quote! { confique::meta::Expr::Array(&[#( #items ),*]) }
}
}
Expr::Map(entries) => {
// TODO: use `Option::unzip` once stable
let types = ty.and_then(get_map_entry_types);
let key_type = types.map(|(t, _)| t);
let value_type = types.map(|(_, v)| v);
let pairs = entries.iter().map(|e| {
let key = match_literals!(&e.key, key_type, MapKey, {});
let value = default_value_to_meta_expr(&e.value, value_type);
quote! { confique::meta::MapEntry { key: #key, value: #value } }
});
quote! { confique::meta::Expr::Map(&[#( #pairs ),*]) }
}
})
}
/// Maps an integer type to the `meta::Expr` variant (e.g. `u32` -> `U32`).
@@ -149,10 +172,9 @@ fn infer_type(
Ident::new(variant, Span::call_site())
}
/// Tries to extract the type of the item of a field with an array default
/// value. Examples: `&[u32]` -> `u32`, `Vec<String>` -> `String`.
fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> {
fn get_array_item_type(ty: &syn::Type) -> Option<&syn::Type> {
match ty {
// The easy types.
syn::Type::Slice(slice) => Some(&slice.elem),
@@ -184,9 +206,40 @@ fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> {
},
// Just recurse on inner type.
syn::Type::Reference(r) => get_item_ty(&r.elem),
syn::Type::Group(g) => get_item_ty(&g.elem),
syn::Type::Paren(p) => get_item_ty(&p.elem),
syn::Type::Reference(r) => get_array_item_type(&r.elem),
syn::Type::Group(g) => get_array_item_type(&g.elem),
syn::Type::Paren(p) => get_array_item_type(&p.elem),
_ => None,
}
}
/// Tries to extract the key and value types from a map value. Examples:
/// `HashMap<String, u32>` -> `(String, u32)`.
fn get_map_entry_types(ty: &syn::Type) -> Option<(&syn::Type, &syn::Type)> {
match ty {
// We simply check if the last element in the path has exactly two
// generic type arguments, in which case we use those. Otherwise we
// can't really know.
syn::Type::Path(p) => {
let args = match &p.path.segments.last().expect("empty type path").arguments {
syn::PathArguments::AngleBracketed(args) => &args.args,
_ => return None,
};
if args.len() != 2 {
return None;
}
match (&args[0], &args[1]) {
(syn::GenericArgument::Type(k), syn::GenericArgument::Type(v)) => Some((k, v)),
_ => None,
}
},
// Just recurse on inner type.
syn::Type::Group(g) => get_map_entry_types(&g.elem),
syn::Type::Paren(p) => get_map_entry_types(&p.elem),
_ => None,
}

View File

@@ -304,6 +304,23 @@ fn default_value_to_deserializable_expr(expr: &ir::Expr) -> TokenStream {
};
quote! { confique::internal::ArrayIntoDeserializer([ #(#items),* ] #type_annotation) }
},
ir::Expr::Map(entries) => {
let items = entries.iter().map(|e| {
let key = default_value_to_deserializable_expr(&e.key.clone().into());
let value = default_value_to_deserializable_expr(&e.value);
quote! { (#key, #value) }
});
// Empty arrays cause "cannot infer type" errors here. However, it
// really doesn't matter what type the array has as there are 0
// elements anyway. So we just pick `()`.
let type_annotation = if entries.is_empty() {
quote! { as Vec<((), ())> }
} else {
quote! {}
};
quote! { confique::internal::MapIntoDeserializer(vec![ #(#items),* ] #type_annotation) }
},
}
}

View File

@@ -67,4 +67,18 @@ pub(crate) enum Expr {
Float(syn::LitFloat),
Bool(syn::LitBool),
Array(Vec<Expr>),
Map(Vec<MapEntry>),
}
pub(crate) struct MapEntry {
pub(crate) key: MapKey,
pub(crate) value: Expr,
}
#[derive(Clone)]
pub(crate) enum MapKey {
Str(syn::LitStr),
Int(syn::LitInt),
Float(syn::LitFloat),
Bool(syn::LitBool),
}

View File

@@ -1,7 +1,7 @@
use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned, punctuated::Punctuated};
use crate::{
ir::{Input, Field, FieldKind, LeafKind, Expr},
ir::{Input, Field, FieldKind, LeafKind, Expr, MapEntry, MapKey},
util::{unwrap_option, is_option},
};
@@ -112,21 +112,54 @@ impl Parse for Expr {
(string, integer, float, bool), and arrays";
if input.peek(syn::token::Bracket) {
// ----- Array -----
let content;
syn::bracketed!(content in input);
let items = <Punctuated<Expr, Token![,]>>::parse_terminated(&content)?;
Ok(Self::Array(items.into_iter().collect()))
} else if input.peek(syn::token::Brace) {
// ----- Map -----
let content;
syn::braced!(content in input);
let items = <Punctuated<MapEntry, Token![,]>>::parse_terminated(&content)?;
Ok(Self::Map(items.into_iter().collect()))
} else {
let lit: syn::Lit = input.parse()
.map_err(|_| Error::new(input.span(), msg))?;
match lit {
syn::Lit::Str(l) => Ok(Self::Str(l)),
syn::Lit::Int(l) => Ok(Self::Int(l)),
syn::Lit::Float(l) => Ok(Self::Float(l)),
syn::Lit::Bool(l) => Ok(Self::Bool(l)),
_ => Err(Error::new(lit.span(), msg)),
}
// ----- Literal -----
// We just use `MapKey` here as it's exactly what we want, despite
// this not having anything to do with maps.
input.parse::<MapKey>()
.map_err(|_| Error::new(input.span(), msg))
.map(Into::into)
}
}
}
impl Parse for MapEntry {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let key: MapKey = input.parse()?;
let _: syn::Token![:] = input.parse()?;
let value: Expr = input.parse()?;
Ok(Self { key, value })
}
}
impl Parse for MapKey {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let lit: syn::Lit = input.parse()?;
match lit {
syn::Lit::Str(l) => Ok(Self::Str(l)),
syn::Lit::Int(l) => Ok(Self::Int(l)),
syn::Lit::Float(l) => Ok(Self::Float(l)),
syn::Lit::Bool(l) => Ok(Self::Bool(l)),
_ => Err(Error::new(
lit.span(),
"only string, integer, float, and Boolean literals allowed as map key",
)),
}
}
}
@@ -297,3 +330,14 @@ fn assert_empty_or_comma(input: ParseStream) -> Result<(), Error> {
Err(Error::new(input.span(), "unexpected tokens, expected no more tokens in this context"))
}
}
impl From<MapKey> for Expr {
fn from(src: MapKey) -> Self {
match src {
MapKey::Str(v) => Self::Str(v),
MapKey::Int(v) => Self::Int(v),
MapKey::Float(v) => Self::Float(v),
MapKey::Bool(v) => Self::Bool(v),
}
}
}

View File

@@ -82,3 +82,21 @@ where
serde::de::value::SeqDeserializer::new(self.0.into_iter())
}
}
/// `serde` does implement `IntoDeserializer` for `HashMap` and `BTreeMap` but
/// we want to keep the exact source code order of entries, so we need our own
/// type.
pub struct MapIntoDeserializer<K, V>(pub Vec<(K, V)>);
impl<'de, K, V, E> serde::de::IntoDeserializer<'de, E> for MapIntoDeserializer<K, V>
where
K: serde::de::IntoDeserializer<'de, E>,
V: serde::de::IntoDeserializer<'de, E>,
E: serde::de::Error,
{
type Deserializer = serde::de::value::MapDeserializer<'de, std::vec::IntoIter<(K, V)>, E>;
fn into_deserializer(self) -> Self::Deserializer {
serde::de::value::MapDeserializer::new(self.0.into_iter())
}
}

View File

@@ -284,9 +284,19 @@ pub use crate::{
/// - **`#[config(default = ...)]`**: sets a default value for this field. This
/// is returned by [`Partial::default_values`] and, in most circumstances,
/// used as a last "layer" to pull values from that have not been set in a
/// layer of higher-priority. Currently, Boolean, float, integer, string, and
/// array values are allowed. The same set of types is allowed as type for
/// array items.
/// layer of higher-priority. Currently, the following expressions are
/// allowed:
///
/// - Booleans, e.g. `default = true`
/// - Integers, e.g. `default = 900`
/// - Floats, e.g. `default = 3.14`
/// - Strings, e.g. `default = "fox"`
/// - Arrays, e.g. `default = ["foo", "bar"]`
/// - Key value maps, e.g. `default = { "cat": 3.14, "bear": 9.0 }`
///
/// Map keys can be Booleans, integers, floats, and strings. For array and map
/// values, you can use any of the expressions in the list above (i.e. you
/// can nest arrays/maps).
///
/// The field value is deserialized from the specified default value
/// (via `serde::de::IntoDeserializer`). So the expression after `default =`

View File

@@ -56,6 +56,26 @@ pub enum Expr {
Integer(Integer),
Bool(bool),
Array(&'static [Expr]),
/// A key value map, stored as slice in source code order.
#[serde(serialize_with = "serialize_map")]
Map(&'static [MapEntry])
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum MapKey {
Str(&'static str),
Float(Float),
Integer(Integer),
Bool(bool),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MapEntry {
pub key: MapKey,
pub value: Expr,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
@@ -110,3 +130,27 @@ impl fmt::Display for Integer {
}
}
}
impl From<MapKey> for Expr {
fn from(src: MapKey) -> Self {
match src {
MapKey::Str(v) => Self::Str(v),
MapKey::Integer(v) => Self::Integer(v),
MapKey::Float(v) => Self::Float(v),
MapKey::Bool(v) => Self::Bool(v),
}
}
}
fn serialize_map<S>(map: &&'static [MapEntry], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut s = serializer.serialize_map(Some(map.len()))?;
for entry in *map {
s.serialize_entry(&entry.key, &entry.value)?;
}
s.end()
}

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::{
net::IpAddr,
path::PathBuf,
@@ -50,6 +51,10 @@ pub struct Headers {
/// Headers that are allowed.
#[config(default = ["content-type", "content-encoding"])]
pub allowed: Vec<String>,
/// Assigns a score to some headers.
#[config(default = { "cookie": 1.5, "server": 12.7 })]
pub score: HashMap<String, f32>,
}

View File

@@ -6,7 +6,7 @@ use std::fmt::{self, Write};
use crate::{
Config,
template::{self, Formatter},
meta::Expr,
meta::{Expr, MapKey},
};
@@ -119,7 +119,7 @@ impl TomlFormatter {
}
impl Formatter for TomlFormatter {
type ExprPrinter = PrintExpr;
type ExprPrinter = PrintExpr<'static>;
fn buffer(&mut self) -> &mut String {
&mut self.buffer
@@ -159,22 +159,50 @@ impl Formatter for TomlFormatter {
}
/// Helper to emit `meta::Expr` into TOML.
struct PrintExpr(&'static Expr);
struct PrintExpr<'a>(&'a Expr);
impl From<&'static Expr> for PrintExpr {
impl From<&'static Expr> for PrintExpr<'static> {
fn from(expr: &'static Expr) -> Self {
Self(expr)
}
}
impl fmt::Display for PrintExpr {
impl fmt::Display for PrintExpr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
toml::to_string(&self.0)
.expect("string serialization to TOML failed")
.fmt(f)
match self.0 {
Expr::Map(entries) => {
// TODO: pretty printing of long arrays onto multiple lines?
f.write_str("{ ")?;
for (i, entry) in entries.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
match entry.key {
MapKey::Str(s) if is_valid_bare_key(s) => f.write_str(s)?,
_ => PrintExpr(&entry.key.clone().into()).fmt(f)?,
}
f.write_str(" = ")?;
PrintExpr(&entry.value).fmt(f)?;
}
f.write_str(" }")?;
Ok(())
},
// All these other types can simply be serialized as is.
Expr::Str(_) | Expr::Float(_) | Expr::Integer(_) | Expr::Bool(_) | Expr::Array(_) => {
toml::to_string(&self.0)
.expect("string serialization to TOML failed")
.fmt(f)
}
}
}
}
fn is_valid_bare_key(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
#[cfg(test)]
mod tests {
use crate::test_utils::{self, include_format_output};

View File

@@ -121,7 +121,7 @@ impl YamlFormatter {
}
impl Formatter for YamlFormatter {
type ExprPrinter = PrintExpr;
type ExprPrinter = PrintExpr<'static>;
fn buffer(&mut self) -> &mut String {
&mut self.buffer
@@ -161,15 +161,15 @@ impl Formatter for YamlFormatter {
}
/// Helper to emit `meta::Expr` into YAML.
struct PrintExpr(&'static Expr);
struct PrintExpr<'a>(&'a Expr);
impl From<&'static Expr> for PrintExpr {
impl From<&'static Expr> for PrintExpr<'static> {
fn from(expr: &'static Expr) -> Self {
Self(expr)
}
}
impl fmt::Display for PrintExpr {
impl fmt::Display for PrintExpr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self.0 {
// We have to special case arrays as the normal formatter only emits
@@ -187,6 +187,21 @@ impl fmt::Display for PrintExpr {
Ok(())
}
Expr::Map(entries) => {
// TODO: pretty printing of long arrays onto multiple lines?
f.write_str("{ ")?;
for (i, entry) in entries.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
PrintExpr(&entry.key.clone().into()).fmt(f)?;
f.write_str(": ")?;
PrintExpr(&entry.value).fmt(f)?;
}
f.write_str(" }")?;
Ok(())
},
// All these other types can simply be serialized as is.
Expr::Str(_) | Expr::Float(_) | Expr::Integer(_) | Expr::Bool(_) => {
let out = serde_yaml::to_string(&self.0).expect("string serialization to YAML failed");

View File

@@ -92,4 +92,3 @@ fn inferred_type() {
assert_eq!(def.parens, vec![1.0, 2.0]);
assert_eq!(def.fallback, std::time::Duration::new(13, 27));
}

View File

@@ -35,6 +35,11 @@
//
// Default value: ["content-type","content-encoding"]
//allowed: ["content-type","content-encoding"],
// Assigns a score to some headers.
//
// Default value: {"cookie":1.5,"server":12.7}
//score: {"cookie":1.5,"server":12.7},
},
},

View File

@@ -36,6 +36,11 @@
# Default value: ["content-type", "content-encoding"]
#allowed = ["content-type", "content-encoding"]
# Assigns a score to some headers.
#
# Default value: { cookie = 1.5, server = 12.7 }
#score = { cookie = 1.5, server = 12.7 }
# Configuring the logging.
[log]
# If set to `true`, the app will log to stdout.

View File

@@ -36,6 +36,11 @@ http:
# Default value: [content-type, content-encoding]
#allowed: [content-type, content-encoding]
# Assigns a score to some headers.
#
# Default value: { cookie: 1.5, server: 12.7 }
#score: { cookie: 1.5, server: 12.7 }
# Configuring the logging.
log:
# If set to `true`, the app will log to stdout.

View File

@@ -36,6 +36,11 @@
# Default value: ["content-type", "content-encoding"]
#allowed = ["content-type", "content-encoding"]
# Assigns a score to some headers.
#
# Default value: { cookie = 1.5, server = 12.7 }
#score = { cookie = 1.5, server = 12.7 }
# Configuring the logging.
[log]
# If set to `true`, the app will log to stdout.

View File

@@ -38,6 +38,11 @@
# Default value: ["content-type", "content-encoding"]
#allowed = ["content-type", "content-encoding"]
# Assigns a score to some headers.
#
# Default value: { cookie = 1.5, server = 12.7 }
#score = { cookie = 1.5, server = 12.7 }
# Configuring the logging.
[log]

View File

@@ -9,6 +9,7 @@
//username: "x-username",
//display_name: "x-display-name",
//allowed: ["content-type","content-encoding"],
//score: {"cookie":1.5,"server":12.7},
},
},

View File

@@ -8,6 +8,7 @@
#username = "x-username"
#display_name = "x-display-name"
#allowed = ["content-type", "content-encoding"]
#score = { cookie = 1.5, server = 12.7 }
[log]
#stdout = true

View File

@@ -8,6 +8,7 @@ http:
#username: x-username
#display_name: x-display-name
#allowed: [content-type, content-encoding]
#score: { cookie: 1.5, server: 12.7 }
log:
#stdout: true

View File

@@ -1,4 +1,4 @@
use std::{path::PathBuf, net::IpAddr};
use std::{path::PathBuf, net::IpAddr, collections::HashMap};
use pretty_assertions::assert_eq;
use confique::{Config, meta, Partial};
@@ -279,3 +279,18 @@ pub(crate) fn deserialize_dummy<'de, D>(deserializer: D) -> Result<Dummy, D::Err
let s = String::deserialize(deserializer)?;
Ok(Dummy(format!("dummy {s}")))
}
// This only makes sure this compiles and doesn't result in any "cannot infer
// type" problems.
#[test]
fn empty_array_and_map() {
#[derive(Config)]
#[allow(dead_code)]
struct Animals {
#[config(default = [])]
cat: Vec<String>,
#[config(default = {})]
dog: HashMap<u32, f32>,
}
}

44
tests/map_default.rs Normal file
View File

@@ -0,0 +1,44 @@
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use confique::{Config, meta};
#[test]
fn string_to_u32() {
#[derive(Config)]
struct Foo {
/// A nice doc comment.
#[config(default = { "peter": 3, "anna": 27 })]
bar: HashMap<String, u32>,
}
assert_eq!(Foo::META, meta::Meta {
name: "Foo",
doc: &[],
fields: &[
meta::Field {
name: "bar",
doc: &[" A nice doc comment."],
kind: meta::FieldKind::Leaf {
env: None,
kind: meta::LeafKind::Required {
default: Some(meta::Expr::Map(&[
meta::MapEntry {
key: meta::MapKey::Str("peter"),
value: meta::Expr::Integer(meta::Integer::U32(3)),
},
meta::MapEntry {
key: meta::MapKey::Str("anna"),
value: meta::Expr::Integer(meta::Integer::U32(27)),
},
])),
},
},
},
],
});
let def = Foo::builder().load().unwrap();
assert_eq!(def.bar, HashMap::from([("peter".into(), 3), ("anna".into(), 27)]));
}