use std::{fmt::Display, str::FromStr};
use gix::ObjectId;
use operation_type::IssueOperationType;
use serde::{Deserialize, Serialize};
use simd_json::{base::ValueAsScalar, derived::ValueTryIntoObject, owned};
use super::{
Issue,
data::{label::Label, status::Status},
};
use crate::replica::entity::{
id::{Id, entity_id::EntityId},
operation::operation_data::OperationData,
};
mod op;
pub mod operation_type;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct File {
git_id: Vec<u8>,
}
impl TryFrom<&owned::Value> for File {
type Error = file_parse::Error;
fn try_from(value: &owned::Value) -> Result<Self, Self::Error> {
let s = value
.as_str()
.ok_or_else(|| file_parse::Error::ExpectedStr {
data: value.to_owned(),
})?;
let object_id = ObjectId::from_str(s)?;
Ok(Self {
git_id: object_id.as_slice().to_owned(),
})
}
}
#[allow(missing_docs)]
pub mod file_parse {
use simd_json::owned;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Expected the '{data}' data, to be a string, but was not.")]
ExpectedStr { data: owned::Value },
#[error("Failed to parse the object id from json data for file: {0}")]
ObjectIdParse(#[from] gix::hash::decode::Error),
}
}
impl Display for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
ObjectId::from_bytes_or_panic(self.git_id.as_slice()).fmt(f)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum IssueOperationData {
AddComment {
message: String,
files: Vec<File>,
},
Create {
title: String,
message: String,
files: Vec<File>,
},
EditComment {
target: EntityId<Issue>,
message: String,
files: Vec<File>,
},
LabelChange {
added: Vec<Label>,
removed: Vec<Label>,
},
SetMetadata {
target: Id,
new_metadata: Vec<(String, String)>,
},
SetStatus {
status: Status,
},
SetTitle {
title: String,
was: String,
},
Noop {},
}
impl OperationData for IssueOperationData {
type DecodeError = decode::Error;
fn is_root(&self) -> bool {
matches!(self, IssueOperationData::Create { .. })
}
fn from_value(raw: owned::Value, predicted_type: u64) -> Result<Self, Self::DecodeError>
where
Self: Sized,
{
let r#type = IssueOperationType::try_from(predicted_type)?;
let object = raw
.try_into_object()
.map_err(|err| decode::Error::ObjectExpected { err })?;
match r#type {
IssueOperationType::AddComment => op::add_comment(object),
IssueOperationType::Create => op::create(object),
IssueOperationType::EditComment => op::edit_comment(object),
IssueOperationType::LabelChange => op::label_change(object),
IssueOperationType::NoOp => Ok(op::noop()),
IssueOperationType::SetMetadata => op::set_metadata(object),
IssueOperationType::SetStatus => op::set_status(object),
IssueOperationType::SetTitle => op::set_title(object),
}
}
fn as_value(&self) -> simd_json::borrowed::Object<'_> {
match self {
IssueOperationData::AddComment { message, files } => {
op::add_comment_value(message, files)
}
IssueOperationData::Create {
title,
message,
files,
} => op::create_value(title, message, files),
IssueOperationData::EditComment {
target,
message,
files,
} => op::edit_comment_value(target, message, files),
IssueOperationData::LabelChange { added, removed } => {
op::label_change_value(added, removed)
}
IssueOperationData::SetMetadata {
target,
new_metadata,
} => op::set_metadata_value(target, new_metadata),
IssueOperationData::SetStatus { status } => op::set_status_value(status),
IssueOperationData::SetTitle { title, was } => op::set_title_value(title, was),
IssueOperationData::Noop {} => op::noop_value(),
}
}
fn to_json_type(&self) -> u64 {
IssueOperationType::from(self).into()
}
}
#[allow(missing_docs)]
pub mod decode {
use super::file_parse;
use crate::{
entities::issue::data::{label, status},
replica::entity::id,
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("The json value did not contain the expected field '{field}'.")]
MissingJsonField { field: &'static str },
#[error("Expected to parse the '{field}' field in the json data: {err}")]
WrongJsonType {
err: simd_json::TryTypeError,
field: &'static str,
},
#[error("Failed to decode an object id (e.g., for a file blob): {0}")]
ObjectIdDecode(#[from] gix::hash::decode::Error),
#[error("Failed to decode an entity id (e.g., for a target field): {0}")]
IdDecode(#[from] id::decode::Error),
#[error(transparent)]
InvalidType(#[from] crate::entities::issue::issue_operation::operation_type::decode::Error),
#[error("Failed to parse the `files` field: {0}")]
FileParse(#[from] file_parse::Error),
#[error("Failed to parse the `added` or `removed` label fields: {0}")]
LabelParse(#[from] label::value_parse::Error),
#[error("Failed to parse the status fields: {0}")]
StatusParse(#[from] status::decode::Error),
#[error("Expected this operation json data to be a object, but was not: {err}")]
ObjectExpected { err: simd_json::TryTypeError },
}
}