use std::borrow::Cow;
use std::fmt;
use indexmap::IndexMap;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use super::names::InputName;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Input {
#[serde(default = "yes")]
pub sensitive: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Default for Input {
fn default() -> Input {
Input {
sensitive: true,
description: None,
}
}
}
fn yes() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Inputs(#[serde(deserialize_with = "inputs")] pub IndexMap<InputName, Input>);
impl Inputs {
#[must_use]
pub fn get(&self, name: &InputName) -> Option<&Input> {
self.0.get(name)
}
}
#[derive(JsonSchema)]
#[serde(untagged)]
enum InputsInput {
Names(Vec<InputName>),
Full(IndexMap<InputName, Input>),
}
impl<'de> Deserialize<'de> for InputsInput {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<InputsInput, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = InputsInput;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a list of input names, or a map of name to `{ sensitive, description }`")
}
fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<InputsInput, A::Error> {
Vec::<InputName>::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(InputsInput::Names)
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<InputsInput, A::Error> {
let raw = IndexMap::<InputName, Option<Input>>::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(InputsInput::Full(raw.into_iter().map(|(k, v)| (k, v.unwrap_or_default())).collect()))
}
fn visit_unit<E: de::Error>(self) -> Result<InputsInput, E> {
Ok(InputsInput::Names(Vec::new()))
}
}
d.deserialize_any(V)
}
}
fn inputs<'de, D: Deserializer<'de>>(d: D) -> Result<IndexMap<InputName, Input>, D::Error> {
match InputsInput::deserialize(d)? {
InputsInput::Full(inputs) => Ok(inputs),
InputsInput::Names(names) => {
let mut out = IndexMap::new();
for name in names {
if out.contains_key(&name) {
return Err(de::Error::custom(format!("`{name}` is listed twice")));
}
out.insert(name, Input::default());
}
Ok(out)
}
}
}
impl JsonSchema for Inputs {
fn schema_name() -> Cow<'static, str> {
"Inputs".into()
}
fn json_schema(g: &mut SchemaGenerator) -> Schema {
InputsInput::json_schema(g)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_input_is_sensitive_unless_it_says_otherwise() {
let map: Inputs = serde_yaml_ng::from_str("{ FLAG: { description: the flag } }").unwrap();
assert!(map.0.values().all(|i| i.sensitive));
let none: Inputs = serde_yaml_ng::from_str("~").unwrap();
assert_eq!(none, Inputs::default());
let wrong = serde_yaml_ng::from_str::<Inputs>("5").unwrap_err().to_string();
assert!(
wrong.contains("a list of input names, or a map of name to `{ sensitive, description }`"),
"{wrong}"
);
}
#[test]
fn inputs_are_a_list_or_a_map() {
let list: Inputs = serde_yaml_ng::from_str("[FLAG, API_URL]").unwrap();
assert!(list.0.values().all(|i| i.sensitive));
let map: Inputs = serde_yaml_ng::from_str("{ FLAG: , API_URL: { sensitive: false } }").unwrap();
let flags: Vec<bool> = map.0.values().map(|i| i.sensitive).collect();
assert_eq!(flags, [true, false]);
assert!(serde_yaml_ng::from_str::<Inputs>("[FLAG, FLAG]").unwrap_err().to_string().contains("twice"));
assert!(serde_yaml_ng::from_str::<Inputs>("[1FLAG]").is_err());
}
}