use indexmap::IndexMap;
pub type Extra = IndexMap<String, serde_json::Value>;
pub fn json_kind(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "a list",
serde_json::Value::Object(_) => "an object",
}
}
pub fn get_string(name: &str, extra: &Extra, key: &str) -> Result<Option<String>, String> {
match extra.get(key) {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
Some(v) => Err(format!(
"files '{name}': '{key}' must be a string, but it is {}.",
json_kind(v)
)),
}
}
pub fn get_bool(name: &str, extra: &Extra, key: &str) -> Result<Option<bool>, String> {
match extra.get(key) {
None => Ok(None),
Some(serde_json::Value::Bool(b)) => Ok(Some(*b)),
Some(v) => Err(format!(
"files '{name}': '{key}' must be true or false, but it is {}.",
json_kind(v)
)),
}
}
pub fn get_usize(
name: &str,
extra: &Extra,
key: &str,
unit: &str,
) -> Result<Option<usize>, String> {
match extra.get(key) {
None => Ok(None),
Some(serde_json::Value::Number(n)) => match n.as_u64() {
Some(n) => Ok(Some(n as usize)),
None => Err(format!(
"files '{name}': '{key}' must be a whole number of {unit} that is not negative, \
but it is {n}."
)),
},
Some(v) => Err(format!(
"files '{name}': '{key}' must be a whole number of {unit}, but it is {}.",
json_kind(v)
)),
}
}
pub fn get_string_list(
name: &str,
extra: &Extra,
key: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(v) = extra.get(key) else {
return Ok(None);
};
let serde_json::Value::Array(items) = v else {
return Err(format!(
"files '{name}': '{key}' must be a list of column names, but it is {}.",
json_kind(v)
));
};
let mut out = Vec::with_capacity(items.len());
for item in items {
match item {
serde_json::Value::String(s) => out.push(s.clone()),
other => {
return Err(format!(
"files '{name}': every entry of '{key}' must be a column name, but one is {}.",
json_kind(other)
))
}
}
}
Ok(Some(out))
}
pub fn get_string_map(
name: &str,
extra: &Extra,
key: &str,
) -> Result<Option<Vec<(String, String)>>, String> {
let Some(v) = extra.get(key) else {
return Ok(None);
};
let serde_json::Value::Object(map) = v else {
return Err(format!(
"files '{name}': '{key}' must be an object of column name → text, but it is {}.",
json_kind(v)
));
};
let mut out = Vec::with_capacity(map.len());
for (column, value) in map {
match value {
serde_json::Value::String(s) => out.push((column.clone(), s.clone())),
other => {
return Err(format!(
"files '{name}': '{key}' entry '{column}' must be text, but it is {}.",
json_kind(other)
))
}
}
}
Ok(Some(out))
}
pub fn first_duplicate(names: &[String]) -> Option<&str> {
for (i, name) in names.iter().enumerate() {
if names[..i].iter().any(|earlier| earlier == name) {
return Some(name);
}
}
None
}