use std::collections::BTreeSet;
use serde_json::{Map, Value};
use crate::lookout::field::{
Entries, Field, FieldKind, Flattened, ListItem, Union, flattened, segment,
};
use super::numbers::{integer_refusal, number_refusal};
use super::{
item_refusal, not_a_number, not_a_whole_number, refusal, required_field, shown_value,
takes_one_of,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRefusal {
pub path: Vec<String>,
pub text: String,
}
impl TableRefusal {
fn at(path: &[String], text: String) -> Self {
Self {
path: path.to_vec(),
text,
}
}
}
#[derive(Clone, Copy)]
enum Scope<'a> {
Whole,
Touched(&'a BTreeSet<Vec<String>>),
}
impl Scope<'_> {
fn holds(self, path: &[String]) -> bool {
match self {
Scope::Whole => true,
Scope::Touched(touched) => touched.contains(path),
}
}
}
fn report(scope: Scope<'_>, path: &[String], text: String) -> Option<TableRefusal> {
scope.holds(path).then(|| TableRefusal::at(path, text))
}
#[must_use]
pub fn table_refusal(schema: &Value, table: &Map<String, Value>) -> Option<TableRefusal> {
flattened_refusal(&flattened(schema), table, &[], "", Scope::Whole)
}
#[must_use]
pub fn touched_refusal(
schema: &Value,
table: &Map<String, Value>,
touched: &BTreeSet<Vec<String>>,
) -> Option<TableRefusal> {
flattened_refusal(&flattened(schema), table, &[], "", Scope::Touched(touched))
}
fn flattened_refusal(
flat: &Flattened,
table: &Map<String, Value>,
base_path: &[String],
base_key: &str,
scope: Scope<'_>,
) -> Option<TableRefusal> {
for field in flat.fields.fields() {
let Some(rest) = flat.paths.get(&field.key) else {
continue;
};
if let Some(refused) = leaf_refusal(flat, field, rest, base_path, base_key, table, scope) {
return Some(refused);
}
}
None
}
fn leaf_refusal(
flat: &Flattened,
field: &Field,
rest: &[String],
base_path: &[String],
base_key: &str,
table: &Map<String, Value>,
scope: Scope<'_>,
) -> Option<TableRefusal> {
let path: Vec<String> = base_path.iter().chain(rest.iter()).cloned().collect();
let key = below(base_key, &field.key);
match held(table, rest) {
Held::Unset => None,
Held::Blocked { value, depth } => {
let prefix: Vec<String> = rest
.iter()
.take(depth + 1)
.map(|name| segment(name))
.collect();
report(
scope,
&base_path
.iter()
.chain(rest[..=depth].iter())
.cloned()
.collect::<Vec<_>>(),
not_a_table(
&below(base_key, &prefix.join(".")),
&shown_for(field.secret, value),
),
)
}
Held::Leaf(value) => match &field.kind {
FieldKind::Entries(kind) => {
entries_refusal(flat, field, kind, value, &path, &key, scope)
}
FieldKind::Union(union) => union_refusal(union, value, &path, &key, scope),
_ => scalar_refusal(field, value, &key, &path, scope),
},
}
}
fn scalar_refusal(
field: &Field,
value: &Value,
key: &str,
path: &[String],
scope: Scope<'_>,
) -> Option<TableRefusal> {
let shown = shown_for(field.secret, value);
match &field.kind {
FieldKind::Integer if !value.is_number() => {
report(scope, path, not_a_whole_number(key, &shown).text)
}
FieldKind::Integer => integer_refusal(key, &field.bounds, &text_of(value), &shown)
.and_then(|r| report(scope, path, r.text)),
FieldKind::Number if !value.is_number() => {
report(scope, path, not_a_number(key, &shown).text)
}
FieldKind::Number => number_refusal(key, &field.bounds, &text_of(value), &shown)
.and_then(|r| report(scope, path, r.text)),
FieldKind::Bool => value
.as_bool()
.is_none()
.then(|| not_a_toggle(key, &shown))
.and_then(|text| report(scope, path, text)),
FieldKind::Text | FieldKind::Suggested(_) => match value.as_str() {
Some(text) => {
refusal(&named(field, key), text).and_then(|r| report(scope, path, r.text))
}
None => report(scope, path, not_a_string(key, &shown)),
},
FieldKind::Choice(names) => match value.as_str() {
Some(text) if names.iter().any(|name| name == text) => None,
_ => report(scope, path, takes_one_of(key, &shown, names).text),
},
FieldKind::List(item) => {
let Some(elements) = value.as_array() else {
return report(scope, path, not_a_list(key, &shown));
};
let bounds = field.bounds.items.as_deref().cloned().unwrap_or_default();
for (index, element) in elements.iter().enumerate() {
if element.is_null() {
continue;
}
let slot = below(key, &index.to_string());
let element_path = below_path(path, &index.to_string());
let shown_element = shown_for(field.secret, element);
let wrong_type = match item {
ListItem::Integer if !element.is_number() => {
Some(not_a_whole_number(&slot, &shown_element).text)
}
ListItem::Text | ListItem::Choice(_) if element.as_str().is_none() => {
Some(not_a_string(&slot, &shown_element))
}
ListItem::Integer | ListItem::Text | ListItem::Choice(_) => None,
};
if let Some(refused) =
wrong_type.and_then(|text| report(scope, &element_path, text))
{
return Some(refused);
}
let item_rule = item_refusal(&slot, item, &bounds, &text_of(element), field.secret)
.and_then(|refused| report(scope, &element_path, refused.text));
if let Some(refused) = item_rule {
return Some(refused);
}
}
None
}
FieldKind::Map | FieldKind::Opaque | FieldKind::Entries(_) | FieldKind::Union(_) => None,
}
}
fn entries_refusal(
flat: &Flattened,
field: &Field,
kind: &Entries,
value: &Value,
path: &[String],
key: &str,
scope: Scope<'_>,
) -> Option<TableRefusal> {
let shape = flat.shapes.get(&field.key)?;
let shown = shown_for(field.secret, value);
match kind {
Entries::Array => {
let Some(entries) = value.as_array() else {
return report(scope, path, not_a_list(key, &shown));
};
for (index, entry) in entries.iter().enumerate() {
let slot = below(key, &index.to_string());
let entry_path = below_path(path, &index.to_string());
let Some(table) = entry.as_object() else {
let refused = not_a_table(&slot, &shown_for(field.secret, entry));
if let Some(refused) = report(scope, &entry_path, refused) {
return Some(refused);
}
continue;
};
if let Some(refused) = flattened_refusal(shape, table, &entry_path, &slot, scope) {
return Some(refused);
}
}
None
}
Entries::Map => {
let Some(entries) = value.as_object() else {
return report(scope, path, not_a_table(key, &shown));
};
for (name, entry) in entries {
let slot = below(key, &segment(name));
let entry_path = below_path(path, name);
let Some(table) = entry.as_object() else {
let refused = not_a_table(&slot, &shown_for(field.secret, entry));
if let Some(refused) = report(scope, &entry_path, refused) {
return Some(refused);
}
continue;
};
if let Some(refused) = flattened_refusal(shape, table, &entry_path, &slot, scope) {
return Some(refused);
}
}
None
}
}
}
fn union_refusal(
union: &Union,
value: &Value,
path: &[String],
key: &str,
scope: Scope<'_>,
) -> Option<TableRefusal> {
let Some(table) = value.as_object() else {
return report(scope, path, not_a_table(key, &text_of(value)));
};
let tags: Vec<&str> = union.tags().collect();
let Some(tag) = table.get(&union.tag).and_then(Value::as_str) else {
return report(
scope,
path,
format!(
"{key} names no {} of its own; try one of {}",
union.tag,
tags.join(", ")
),
);
};
let Some(arm) = union.arm(tag) else {
return report(
scope,
path,
format!(
"{key} is \"{tag}\", which is not a kind {key} takes; try one of {}",
tags.join(", ")
),
);
};
let tag_key = below(key, &segment(&union.tag));
for arm_field in &arm.fields {
let arm_key = below(key, &segment(&arm_field.key));
let Some(held) = table.get(&arm_field.key) else {
if arm_field.editable && arm.requires(&arm_field.key) {
let refused = required_field(&arm_key, &tag_key, &arm.tag);
if let Some(refused) = report(scope, path, refused.text) {
return Some(refused);
}
}
continue;
};
if held.is_null() {
continue;
}
let arm_path = below_path(path, &arm_field.key);
if let Some(refused) = scalar_refusal(arm_field, held, &arm_key, &arm_path, scope) {
return Some(refused);
}
}
None
}
enum Held<'a> {
Unset,
Leaf(&'a Value),
Blocked { value: &'a Value, depth: usize },
}
fn held<'a>(table: &'a Map<String, Value>, path: &[String]) -> Held<'a> {
let mut nested = Some(table);
for (depth, name) in path.iter().enumerate() {
let Some(value) = nested.and_then(|table| table.get(name)) else {
return Held::Unset;
};
if value.is_null() {
return Held::Unset;
}
if depth + 1 == path.len() {
return Held::Leaf(value);
}
match value.as_object() {
Some(next) => nested = Some(next),
None => return Held::Blocked { value, depth },
}
}
Held::Unset
}
fn below(key: &str, segment: &str) -> String {
if key.is_empty() {
segment.to_owned()
} else {
format!("{key}.{segment}")
}
}
fn below_path(path: &[String], segment: &str) -> Vec<String> {
let mut out = path.to_vec();
out.push(segment.to_owned());
out
}
fn named(field: &Field, key: &str) -> Field {
let mut clone = field.clone();
clone.key = key.to_owned();
clone
}
fn shown_for(secret: bool, value: &Value) -> String {
shown_value(secret, &text_of(value)).to_owned()
}
fn text_of(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
fn not_a_string(key: &str, shown: &str) -> String {
format!("{key} is \"{shown}\", which is not a string shep accepts")
}
fn not_a_toggle(key: &str, shown: &str) -> String {
format!("{key} is \"{shown}\", which is not true or false")
}
fn not_a_list(key: &str, shown: &str) -> String {
format!("{key} is \"{shown}\", which is not a list shep accepts")
}
fn not_a_table(key: &str, shown: &str) -> String {
format!("{key} is \"{shown}\", which is not a table shep accepts")
}