use cratefield_core::Problem;
use serde_json::Value;
use std::fmt::Write as _;
use crate::schema::{FieldKind, TableDef, TextFormat};
use crate::value::{ErrorCode, check_value};
pub const MAX_DETAIL_ERRORS: usize = 10;
pub const MAX_UNKNOWN_KEY_CHARS: usize = 64;
fn bounded_key(key: &str) -> String {
let mut cut: String = key.chars().take(MAX_UNKNOWN_KEY_CHARS).collect();
if key.chars().nth(MAX_UNKNOWN_KEY_CHARS).is_some() {
cut.push('…');
}
cut
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowError {
pub field: String,
pub code: ErrorCode,
pub message: String,
}
impl RowError {
fn new(field: impl Into<String>, code: ErrorCode, message: impl Into<String>) -> Self {
Self {
field: field.into(),
code,
message: message.into(),
}
}
#[must_use]
pub fn line(&self) -> String {
if self.field.is_empty() {
self.message.clone()
} else {
format!("{} {}", self.field, self.message)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowErrors {
errors: Vec<RowError>,
}
impl RowErrors {
pub(crate) fn one(field: &str, code: ErrorCode, message: impl Into<String>) -> Self {
Self {
errors: vec![RowError::new(field, code, message)],
}
}
#[must_use]
pub fn errors(&self) -> &[RowError] {
&self.errors
}
#[must_use]
pub fn detail(&self) -> String {
let shown: Vec<String> = self
.errors
.iter()
.take(MAX_DETAIL_ERRORS)
.map(RowError::line)
.collect();
let mut detail = shown.join("; ");
if self.errors.len() > MAX_DETAIL_ERRORS {
let hidden = self.errors.len() - MAX_DETAIL_ERRORS;
let _ = write!(detail, " (and {hidden} more)");
}
detail
}
#[must_use]
pub fn problem(&self) -> Problem {
Problem::validation_failed(self.detail())
}
}
impl From<RowErrors> for Problem {
fn from(errors: RowErrors) -> Self {
errors.problem()
}
}
impl std::fmt::Display for RowErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.detail())
}
}
impl std::error::Error for RowErrors {}
#[must_use]
pub fn normalize_row(table: &TableDef, row: Value) -> Value {
let Value::Object(mut object) = row else {
return row;
};
for field in &table.fields {
if !matches!(
field.kind,
FieldKind::Text {
format: Some(TextFormat::Email),
..
}
) {
continue;
}
if let Some(Value::String(address)) = object.get(&field.name) {
let normalized = cratefield_core::normalize_email(address);
object.insert(field.name.clone(), Value::String(normalized));
}
}
Value::Object(object)
}
pub fn validate_row(table: &TableDef, row: &Value) -> Result<(), RowErrors> {
let Some(object) = row.as_object() else {
return Err(RowErrors {
errors: vec![RowError::new(
"",
ErrorCode::NotAnObject,
format!("the row must be a JSON object, not {}", type_name(row)),
)],
});
};
let mut errors = Vec::new();
for field in &table.fields {
match object.get(&field.name) {
None | Some(Value::Null) => {
if field.must_be_present() {
errors.push(RowError::new(
&field.name,
ErrorCode::Required,
"is required",
));
}
}
Some(value) => {
if let Err(error) = check_value(&field.kind, value) {
errors.push(RowError::new(&field.name, error.code, error.message));
}
}
}
}
let mut unknown: Vec<&String> = object
.keys()
.filter(|key| table.field(key).is_none())
.collect();
unknown.sort();
for key in unknown {
errors.push(RowError::new(
bounded_key(key),
ErrorCode::UnknownField,
format!("is not a declared field of `{}`", table.name),
));
}
if errors.is_empty() {
Ok(())
} else {
Err(RowErrors { errors })
}
}
fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}