use std::path::PathBuf;
use thiserror::Error;
use crate::InputFormat;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FaceError {
#[error("input I/O: {source}")]
Io {
#[source]
source: std::io::Error,
},
#[error("input parse failed ({format:?}): {message}")]
InputParse {
format: InputFormat,
message: String,
},
#[error("items path `{path}` not found in input")]
UnknownItemsPath {
path: String,
},
#[error("score path `{path}` is not numeric in any sampled item")]
UnknownScorePath {
path: String,
},
#[error("{}", render_ambiguous_detection(candidates, numeric_fields))]
AmbiguousDetection {
candidates: Vec<DetectionCandidate>,
numeric_fields: Vec<String>,
},
#[error("invalid cluster id at segment {segment}: {reason}")]
InvalidClusterId {
segment: usize,
reason: String,
},
#[error("cluster id refers to unknown axis `{axis}`")]
UnknownAxis {
axis: String,
},
#[error("conflicting flags: {message}")]
ConflictingFlags {
message: String,
},
#[error("config error in `{}`: {message}", path.display())]
Config {
path: PathBuf,
message: String,
},
#[error("unsupported: {feature}")]
Unsupported {
feature: String,
},
}
impl From<std::io::Error> for FaceError {
fn from(source: std::io::Error) -> Self {
FaceError::Io { source }
}
}
fn render_ambiguous_detection(
candidates: &[DetectionCandidate],
numeric_fields: &[String],
) -> String {
let mut out = String::from("cannot auto-pick a grouping field");
if candidates.is_empty() && numeric_fields.is_empty() {
return out;
}
if !candidates.is_empty() {
out.push_str("\n string fields: ");
let parts: Vec<String> = candidates
.iter()
.map(|c| format!("{} ({} distinct)", c.field, c.cardinality))
.collect();
out.push_str(&parts.join(", "));
}
if !numeric_fields.is_empty() {
out.push_str("\n numeric fields: ");
out.push_str(&numeric_fields.join(", "));
}
out.push_str(
"\n hint: pass --score=FIELD to rank by a numeric field, or --by=FIELD to group by a string field",
);
out
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DetectionCandidate {
pub field: String,
pub cardinality: usize,
}
impl DetectionCandidate {
pub fn new(field: impl Into<String>, cardinality: usize) -> Self {
Self {
field: field.into(),
cardinality,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SkipReport {
pub record_index: usize,
pub reason: SkipReason,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SkipReason {
InvalidJson {
column: usize,
message: String,
},
MissingField {
field: String,
},
WrongType {
field: String,
kind: String,
},
NonNumericScore {
path: String,
value_summary: String,
},
}