use connectrpc::{ConnectError, ErrorCode, ErrorDetail};
use polyc_proto::proto::polychrome::state::v1 as pb;
use polyc_state::{
error::StateError,
immutable::{Generation, ObjectError, ObjectId, Retention},
};
use crate::{
error::{TransportFallback, decode_detail, from_connect_error as state_from_connect},
immutable::wire::{classification, protection},
wire::Kernel,
};
pub const OBJECT_ERROR_DETAIL_TYPE: &str = "polychrome.state.v1.ObjectErrorDetail";
impl From<Kernel<&ObjectError>> for pb::ObjectErrorDetail {
fn from(value: Kernel<&ObjectError>) -> Self {
use pb::__buffa::oneof::object_error_detail::Outcome;
let unknown = buffa::UnknownFields::default;
let outcome = match value.0 {
ObjectError::State(error) => Outcome::from(pb::StateErrorDetail::from(Kernel(error))),
ObjectError::StaleGeneration {
object,
expected,
current,
} => Outcome::from(pb::StaleObjectGenerationDetail {
object: object.as_str().to_owned(),
expected: expected.get(),
current: current.get(),
__buffa_unknown_fields: unknown(),
}),
ObjectError::NotMonotonic {
object,
current,
requested,
} => Outcome::from(pb::NonMonotonicObjectGenerationDetail {
object: object.as_str().to_owned(),
current: current.get(),
requested: requested.get(),
__buffa_unknown_fields: unknown(),
}),
ObjectError::UnknownGeneration { object, generation } => {
Outcome::from(pb::UnknownObjectGenerationDetail {
object: object.as_str().to_owned(),
generation: generation.get(),
__buffa_unknown_fields: unknown(),
})
}
ObjectError::Retained {
object,
generation,
retention,
} => Outcome::from(pb::RetainedObjectGenerationDetail {
object: object.as_str().to_owned(),
generation: generation.get(),
retention: buffa::MessageField::some(pb::ObjectRetention::from(Kernel(*retention))),
__buffa_unknown_fields: unknown(),
}),
ObjectError::CurrentGeneration { object, generation } => {
Outcome::from(pb::CurrentObjectGenerationDetail {
object: object.as_str().to_owned(),
generation: generation.get(),
__buffa_unknown_fields: unknown(),
})
}
ObjectError::ProtectionUnavailable {
object,
classification,
declared,
} => Outcome::from(pb::ObjectProtectionUnavailableDetail {
object: object.as_str().to_owned(),
classification: pb::ObjectClassification::from(Kernel(*classification)).into(),
declared_protection: pb::ObjectAtRestProtection::from(Kernel(*declared)).into(),
__buffa_unknown_fields: unknown(),
}),
};
Self {
outcome: Some(outcome),
__buffa_unknown_fields: unknown(),
}
}
}
impl TryFrom<pb::ObjectErrorDetail> for Kernel<ObjectError> {
type Error = pb::ObjectErrorDetail;
fn try_from(value: pb::ObjectErrorDetail) -> Result<Self, Self::Error> {
use pb::__buffa::oneof::object_error_detail::Outcome;
let Some(outcome) = value.outcome.clone() else {
return Err(value);
};
let decoded = match outcome {
Outcome::State(detail) => {
let Ok(error) = Kernel::<StateError>::try_from(*detail) else {
return Err(value);
};
ObjectError::State(error.into_inner())
}
Outcome::StaleGeneration(detail) => ObjectError::StaleGeneration {
object: ObjectId::new(detail.object),
expected: Generation::new(detail.expected),
current: Generation::new(detail.current),
},
Outcome::NotMonotonic(detail) => ObjectError::NotMonotonic {
object: ObjectId::new(detail.object),
current: Generation::new(detail.current),
requested: Generation::new(detail.requested),
},
Outcome::UnknownGeneration(detail) => ObjectError::UnknownGeneration {
object: ObjectId::new(detail.object),
generation: Generation::new(detail.generation),
},
Outcome::Retained(detail) => {
let retention = detail
.retention
.into_option()
.and_then(|value| Kernel::<Retention>::try_from(value).ok())
.map_or(Retention::UntilReleased, Kernel::into_inner);
ObjectError::Retained {
object: ObjectId::new(detail.object),
generation: Generation::new(detail.generation),
retention,
}
}
Outcome::CurrentGeneration(detail) => ObjectError::CurrentGeneration {
object: ObjectId::new(detail.object),
generation: Generation::new(detail.generation),
},
Outcome::ProtectionUnavailable(detail) => {
let Ok(classification) = classification("classification", detail.classification)
else {
return Err(value);
};
let Ok(declared) = protection("declared_protection", detail.declared_protection)
else {
return Err(value);
};
ObjectError::ProtectionUnavailable {
object: ObjectId::new(detail.object),
classification,
declared,
}
}
};
Ok(Self(decoded))
}
}
#[must_use]
pub fn to_connect_error(error: &ObjectError) -> ConnectError {
let code = match error {
ObjectError::State(state) => crate::error::code_for(state),
ObjectError::UnknownGeneration { .. } => ErrorCode::NotFound,
ObjectError::StaleGeneration { .. }
| ObjectError::NotMonotonic { .. }
| ObjectError::Retained { .. }
| ObjectError::CurrentGeneration { .. }
| ObjectError::ProtectionUnavailable { .. } => ErrorCode::FailedPrecondition,
};
ConnectError::new(code, error.to_string()).with_detail(ErrorDetail::from_message(
OBJECT_ERROR_DETAIL_TYPE,
&pb::ObjectErrorDetail::from(Kernel(error)),
))
}
#[must_use]
pub fn from_connect_error(error: &ConnectError, fallback: &TransportFallback) -> ObjectError {
for detail in &error.details {
if detail.type_url != OBJECT_ERROR_DETAIL_TYPE {
continue;
}
let Some(encoded) = detail.value.as_deref() else {
continue;
};
let Ok(bytes) = decode_detail(encoded) else {
continue;
};
let Ok(message) = <pb::ObjectErrorDetail as buffa::Message>::decode_from_slice(&bytes)
else {
continue;
};
if let Ok(typed) = Kernel::<ObjectError>::try_from(message) {
return typed.into_inner();
}
}
ObjectError::State(state_from_connect(error, fallback))
}
#[cfg(test)]
mod tests {
use super::*;
use polyc_state::immutable::{AtRestProtection, Classification};
#[test]
fn every_family_variant_round_trips() {
let variants = [
ObjectError::StaleGeneration {
object: ObjectId::new("o"),
expected: Generation::new(1),
current: Generation::new(2),
},
ObjectError::NotMonotonic {
object: ObjectId::new("o"),
current: Generation::new(2),
requested: Generation::new(1),
},
ObjectError::UnknownGeneration {
object: ObjectId::new("o"),
generation: Generation::new(9),
},
ObjectError::Retained {
object: ObjectId::new("o"),
generation: Generation::new(1),
retention: Retention::For(std::time::Duration::from_secs(4)),
},
ObjectError::CurrentGeneration {
object: ObjectId::new("o"),
generation: Generation::new(2),
},
ObjectError::ProtectionUnavailable {
object: ObjectId::new("o"),
classification: Classification::Restricted,
declared: AtRestProtection::ManagedKey,
},
];
for original in variants {
let wire = to_connect_error(&original);
let back = from_connect_error(
&wire,
&TransportFallback::new(polyc_state::immutable::family(), 65_536, 1),
);
assert_eq!(back, original);
}
}
}