use std::{error, fmt};
use tephra_types::{AppendCondition, EventType, NameError, Position, Query, QueryItem, Tag, Tags};
use crate::tephra as pb;
#[derive(Debug)]
pub enum ConvertError {
InvalidUtf8,
Name(NameError),
Tags(tephra_types::TagsError),
}
impl fmt::Display for ConvertError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConvertError::InvalidUtf8 => write!(f, "string field is not valid utf-8"),
ConvertError::Name(err) => write!(f, "invalid name: {err}"),
ConvertError::Tags(err) => write!(f, "invalid tags: {err}"),
}
}
}
impl error::Error for ConvertError {}
pub fn as_str(view: &protobuf::ProtoStr) -> Result<&str, ConvertError> {
view.to_str().map_err(|_| ConvertError::InvalidUtf8)
}
pub fn tags_from_pb<'a>(
tags: impl IntoIterator<Item = &'a protobuf::ProtoStr>,
) -> Result<Tags, ConvertError> {
let mut out: Vec<Tag> = Vec::new();
for tag in tags {
out.push(Tag::new(as_str(tag)?).map_err(ConvertError::Name)?);
}
Tags::new(out).map_err(ConvertError::Tags)
}
pub fn types_from_pb<'a>(
types: impl IntoIterator<Item = &'a protobuf::ProtoStr>,
) -> Result<Vec<EventType>, ConvertError> {
let mut out: Vec<EventType> = Vec::new();
for ty in types {
out.push(EventType::new(as_str(ty)?).map_err(ConvertError::Name)?);
}
Ok(out)
}
pub fn query_from_pb(query: pb::QueryView<'_>) -> Result<Query, ConvertError> {
if query.all() {
return Ok(Query::All);
}
let mut items = Vec::with_capacity(query.items().len());
for item in query.items().iter() {
let types = types_from_pb(item.types().iter())?;
let tags = tags_from_pb(item.tags().iter())?;
items.push(QueryItem::new(types, tags));
}
Ok(Query::items(items))
}
pub fn query_to_pb(query: &Query) -> pb::Query {
let mut out = pb::Query::new();
match query {
Query::All => out.set_all(true),
Query::Items(items) => {
for item in items {
let mut pb_item = pb::QueryItem::new();
for ty in &item.types {
pb_item.types_mut().push(ty.as_str());
}
for tag in item.tags.iter() {
pb_item.tags_mut().push(tag.as_str());
}
out.items_mut().push(pb_item);
}
}
}
out
}
pub fn condition_from_pb(
condition: pb::AppendConditionView<'_>,
) -> Result<AppendCondition, ConvertError> {
let query = query_from_pb(condition.fail_if_events_match())?;
Ok(AppendCondition::new(query).after(Position::new(condition.after())))
}
pub fn condition_to_pb(condition: &AppendCondition) -> pb::AppendCondition {
let mut out = pb::AppendCondition::new();
out.set_fail_if_events_match(query_to_pb(&condition.fail_if_events_match));
out.set_after(condition.after.get());
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorCode {
Conflict,
AfterBeyondTip,
Empty,
TooLarge,
BadRequest,
Internal,
Shutdown,
Unknown,
}
impl From<pb::ErrorCode> for ErrorCode {
fn from(code: pb::ErrorCode) -> Self {
match code {
pb::ErrorCode::Conflict => ErrorCode::Conflict,
pb::ErrorCode::AfterBeyondTip => ErrorCode::AfterBeyondTip,
pb::ErrorCode::Empty => ErrorCode::Empty,
pb::ErrorCode::TooLarge => ErrorCode::TooLarge,
pb::ErrorCode::BadRequest => ErrorCode::BadRequest,
pb::ErrorCode::Internal => ErrorCode::Internal,
pb::ErrorCode::Shutdown => ErrorCode::Shutdown,
_ => ErrorCode::Unknown,
}
}
}
impl ErrorCode {
pub fn to_pb(self) -> pb::ErrorCode {
match self {
ErrorCode::Conflict => pb::ErrorCode::Conflict,
ErrorCode::AfterBeyondTip => pb::ErrorCode::AfterBeyondTip,
ErrorCode::Empty => pb::ErrorCode::Empty,
ErrorCode::TooLarge => pb::ErrorCode::TooLarge,
ErrorCode::BadRequest => pb::ErrorCode::BadRequest,
ErrorCode::Internal => pb::ErrorCode::Internal,
ErrorCode::Shutdown => pb::ErrorCode::Shutdown,
ErrorCode::Unknown => pb::ErrorCode::Unspecified,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_all_round_trips() {
let pb = query_to_pb(&Query::All);
assert!(pb.all());
assert_eq!(query_from_pb(pb.as_view()).unwrap(), Query::All);
}
#[test]
fn query_items_round_trip() {
let query = Query::items(vec![
QueryItem::of_types(vec![EventType::new("Registered").unwrap()]),
QueryItem::new(
vec![EventType::new("Enrolled").unwrap()],
Tags::new(vec![
Tag::new("course:c1").unwrap(),
Tag::new("student:s1").unwrap(),
])
.unwrap(),
),
]);
let pb = query_to_pb(&query);
assert_eq!(query_from_pb(pb.as_view()).unwrap(), query);
}
#[test]
fn empty_items_query_round_trips_as_items_not_all() {
let pb = query_to_pb(&Query::items(Vec::new()));
assert!(!pb.all());
assert_eq!(
query_from_pb(pb.as_view()).unwrap(),
Query::items(Vec::new())
);
}
#[test]
fn condition_round_trips() {
let cond = AppendCondition::new(Query::item(QueryItem::with_tags(
Tags::new(vec![Tag::new("course:c1").unwrap()]).unwrap(),
)))
.after(Position::new(42));
let pb = condition_to_pb(&cond);
assert_eq!(condition_from_pb(pb.as_view()).unwrap(), cond);
}
#[test]
fn empty_tag_is_rejected_through_core_validation() {
let mut item = pb::QueryItem::new();
item.tags_mut().push("");
let mut query = pb::Query::new();
query.items_mut().push(item);
assert!(matches!(
query_from_pb(query.as_view()),
Err(ConvertError::Name(NameError::Empty { .. }))
));
}
#[test]
fn error_code_round_trips() {
for code in [
ErrorCode::Conflict,
ErrorCode::AfterBeyondTip,
ErrorCode::Empty,
ErrorCode::TooLarge,
ErrorCode::BadRequest,
ErrorCode::Internal,
ErrorCode::Shutdown,
ErrorCode::Unknown,
] {
assert_eq!(ErrorCode::from(code.to_pb()), code);
}
assert_eq!(
ErrorCode::from(pb::ErrorCode::Unspecified),
ErrorCode::Unknown
);
}
}