use onetaskgraph_plugin_api::{SourceError, SourceName};
use schemars::JsonSchema;
use serde::Serialize;
use crate::config::ConfigError;
use crate::engine::EngineError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum FailureClass {
Refused,
Transient,
}
#[must_use]
pub fn classify(cause: Option<&SourceError>) -> FailureClass {
match cause {
Some(SourceError::RateLimited { .. } | SourceError::Unavailable { .. }) => {
FailureClass::Transient
}
Some(
SourceError::Refused { .. }
| SourceError::Config { .. }
| SourceError::Auth { .. }
| SourceError::Malformed { .. },
)
| None => FailureClass::Refused,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
pub struct FailureDocument {
pub failure: Failure,
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[schemars(transform = every_member_required)]
pub struct Failure {
class: FailureClass,
kind: String,
source: Option<SourceName>,
message: String,
retry_after_seconds: Option<u64>,
}
fn every_member_required(schema: &mut schemars::Schema) {
let members: Vec<serde_json::Value> = schema
.get("properties")
.and_then(serde_json::Value::as_object)
.map(|properties| {
properties
.keys()
.cloned()
.map(serde_json::Value::from)
.collect()
})
.unwrap_or_default();
schema.insert("required".to_owned(), serde_json::Value::Array(members));
}
impl Failure {
#[must_use]
pub fn decided(kind: &str, message: impl Into<String>) -> Self {
Self::caused(kind.to_owned(), None, None, message.into())
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
fn caused(
kind: String,
source: Option<SourceName>,
cause: Option<&SourceError>,
message: String,
) -> Self {
Self {
class: classify(cause),
kind,
source,
message,
retry_after_seconds: match cause {
Some(SourceError::RateLimited {
retry_after_seconds,
..
}) => *retry_after_seconds,
_ => None,
},
}
}
}
fn source_kind(error: &SourceError) -> String {
serde_json::to_value(error)
.ok()
.and_then(|wire| wire.get("kind")?.as_str().map(str::to_owned))
.expect("a source error is a kind-tagged object")
}
fn cause(error: &EngineError) -> (String, Option<SourceName>, Option<&SourceError>) {
let configured = |name: &str| SourceName::new(name.to_owned()).ok();
match error {
EngineError::UnknownSource { .. } => ("unknown-source".to_owned(), None, None),
EngineError::Token { .. } => ("page-token".to_owned(), None, None),
EngineError::NoSources => ("no-sources".to_owned(), None, None),
EngineError::NotWritable { name, .. } => {
("not-writable".to_owned(), configured(name), None)
}
EngineError::NoDocuments { name, .. } => {
("no-documents".to_owned(), configured(name), None)
}
EngineError::NoComments { name, .. } => ("no-comments".to_owned(), configured(name), None),
EngineError::CommentsNotWritable { name, .. } => {
("not-writable".to_owned(), configured(name), None)
}
EngineError::NoSuchItem { .. } | EngineError::NoSuchTask { .. } => {
("no-such-item".to_owned(), None, None)
}
EngineError::NoSuchComment { .. } => ("no-such-comment".to_owned(), None, None),
EngineError::StaleOrigin { .. } => ("stale-origin".to_owned(), None, None),
EngineError::NotAMember { .. } => ("not-a-member".to_owned(), None, None),
EngineError::UnrecordedMember { .. } => ("unrecorded-member".to_owned(), None, None),
EngineError::DestinationUnavailable { name, error }
| EngineError::SourceRefused { name, error }
| EngineError::SourceUnavailable { name, error }
| EngineError::SourceFailed { name, error } => {
(source_kind(error), configured(name), Some(error))
}
EngineError::CopyNotUndone { error, .. } => cause(error),
}
}
impl From<&EngineError> for Failure {
fn from(error: &EngineError) -> Self {
let (kind, source, caused_by) = cause(error);
Self::caused(kind, source, caused_by, error.to_string())
}
}
impl From<&ConfigError> for Failure {
fn from(error: &ConfigError) -> Self {
let kind = match error {
ConfigError::Read { .. } => "config-read",
ConfigError::Syntax { .. } => "config-syntax",
ConfigError::Setting { .. } => "config-setting",
};
Self::decided(kind, error.to_string())
}
}