use std::collections::BTreeMap;
use std::path::Path;
use onetaskgraph_plugin_api::{
Capabilities, Comment, CommentBody, Direction, Document, DocumentQuery, ItemWrite, MetadataKey,
Metering, NativeId, NewComment, Page, PageRequest, Priority, Project, ProjectQuery,
SourceError, Status, StatusCategory, Task, TaskQuery, TaskRef, TaskUpdate, TaskUpdateOutcome,
WriteSupport,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub(crate) const PROTOCOL_VERSION: u32 = 2;
pub(crate) const STATUS_VOCABULARY: [StatusCategory; 8] = [
StatusCategory::Draft,
StatusCategory::Backlog,
StatusCategory::Todo,
StatusCategory::Queued,
StatusCategory::InProgress,
StatusCategory::Done,
StatusCategory::Cancelled,
StatusCategory::Unknown,
];
pub(crate) const fn after_the_first_vocabulary(category: StatusCategory) -> bool {
match category {
StatusCategory::Queued => true,
StatusCategory::Draft
| StatusCategory::Backlog
| StatusCategory::Todo
| StatusCategory::InProgress
| StatusCategory::Done
| StatusCategory::Cancelled
| StatusCategory::Unknown => false,
}
}
pub(crate) fn spelled(category: StatusCategory) -> String {
serde_json::to_value(category)
.ok()
.and_then(|word| word.as_str().map(str::to_owned))
.expect("a status category serialises as its own word")
}
pub(crate) fn vocabulary() -> Vec<String> {
STATUS_VOCABULARY.into_iter().map(spelled).collect()
}
pub(crate) fn knows_every_category(statuses: Option<&[String]>) -> bool {
let Some(listed) = statuses else {
return false;
};
STATUS_VOCABULARY
.into_iter()
.filter(|category| after_the_first_vocabulary(*category))
.all(|category| listed.contains(&spelled(category)))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Request {
pub(crate) id: String,
pub(crate) method: String,
pub(crate) params: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Response {
pub(crate) id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) result: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<SourceError>,
}
impl Response {
pub(crate) fn ok(id: String, result: Value) -> Self {
Self {
id,
result: Some(result),
error: None,
}
}
pub(crate) fn failed(id: String, error: SourceError) -> Self {
Self {
id,
result: None,
error: Some(error),
}
}
pub(crate) fn outcome(self) -> Option<Result<Value, SourceError>> {
match (self.result, self.error) {
(Some(result), None) => Some(Ok(result)),
(None, Some(error)) => Some(Err(error)),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InitializeParams {
pub(crate) protocol_version: u32,
pub(crate) engine: EngineIdentity,
pub(crate) source_name: String,
pub(crate) config: Value,
pub(crate) secrets: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) statuses: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) document_dir: Option<DocumentDir>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub(crate) struct DocumentDir(String);
impl DocumentDir {
pub(crate) fn new(directory: &Path) -> Result<Self, String> {
let spelled = directory.to_str().ok_or_else(|| {
format!(
"the document directory {} is not valid UTF-8, so it cannot be written into \
the handshake",
directory.display()
)
})?;
if !directory.is_absolute() {
return Err(format!(
"the document directory {spelled:?} is not an absolute path, so there is no \
one directory to measure this source's relative paths from"
));
}
Ok(Self(spelled.to_owned()))
}
pub(crate) fn as_path(&self) -> &Path {
Path::new(&self.0)
}
}
impl TryFrom<String> for DocumentDir {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(Path::new(&value))
}
}
impl From<DocumentDir> for String {
fn from(value: DocumentDir) -> Self {
value.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct EngineIdentity {
pub(crate) name: String,
pub(crate) version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InitializeResult {
#[serde(default)]
pub(crate) protocol_version: Option<u32>,
pub(crate) kind: HandshakePluginKind,
pub(crate) capabilities: Capabilities,
#[serde(default)]
pub(crate) writes: Option<WriteSupport>,
#[serde(default)]
pub(crate) meters: bool,
#[serde(default)]
pub(crate) statuses: Option<Vec<String>>,
#[serde(default)]
pub(crate) task_updates: bool,
#[serde(default)]
pub(crate) metadata_updates: bool,
#[serde(default)]
pub(crate) content_updates: bool,
#[serde(default)]
pub(crate) targeted_updates: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MeteringResult {
#[serde(default)]
pub(crate) metering: Option<Metering>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub(crate) struct HandshakePluginKind(String);
impl HandshakePluginKind {
pub(crate) fn new(kind: impl Into<String>) -> Result<Self, &'static str> {
let kind = kind.into();
if kind.trim().is_empty() {
Err("plugin kind must contain a non-whitespace character")
} else {
Ok(Self(kind))
}
}
pub(crate) fn into_string(self) -> String {
self.0
}
}
impl TryFrom<String> for HandshakePluginKind {
type Error = &'static str;
fn try_from(kind: String) -> Result<Self, Self::Error> {
Self::new(kind)
}
}
impl From<HandshakePluginKind> for String {
fn from(kind: HandshakePluginKind) -> Self {
kind.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct IdParams {
pub(crate) id: NativeId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TaskResult {
#[serde(default)]
pub(crate) task: Option<Task>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProjectResult {
#[serde(default)]
pub(crate) project: Option<Project>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DocumentResult {
#[serde(default)]
pub(crate) document: Option<Document>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TaskQueryParams {
pub(crate) query: TaskQuery,
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProjectQueryParams {
pub(crate) query: ProjectQuery,
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LabelParams {
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DocumentQueryParams {
pub(crate) query: DocumentQuery,
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DependencyParams {
pub(crate) id: NativeId,
pub(crate) direction: Direction,
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TaskWriteParams {
pub(crate) write: ItemWrite<Task>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProjectWriteParams {
pub(crate) write: ItemWrite<Project>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DocumentWriteParams {
pub(crate) write: ItemWrite<Document>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DeleteParams {
pub(crate) id: NativeId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CommentsParams {
pub(crate) task: NativeId,
pub(crate) page: PageRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CommentsResult {
#[serde(default)]
pub(crate) page: Option<Page<Comment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct AddCommentParams {
pub(crate) task: NativeId,
pub(crate) comment: NewComment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct EditCommentParams {
pub(crate) task: NativeId,
pub(crate) comment: NativeId,
pub(crate) body: CommentBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DeleteCommentParams {
pub(crate) task: NativeId,
pub(crate) comment: NativeId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CommentResult {
#[serde(default)]
pub(crate) comment: Option<Comment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DeletedCommentResult {
#[serde(default)]
pub(crate) deleted: Option<NativeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct StatusParams {
pub(crate) id: NativeId,
pub(crate) category: StatusCategory,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct StatusResult {
#[serde(default)]
pub(crate) status: Option<Status>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PriorityParams {
pub(crate) id: NativeId,
pub(crate) priority: Priority,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PriorityResult {
#[serde(deserialize_with = "present")]
pub(crate) priority: Option<Priority>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ContentParams {
pub(crate) id: NativeId,
pub(crate) content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ContentResult {
#[serde(deserialize_with = "present")]
pub(crate) id: Option<NativeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UpdateParams {
pub(crate) id: NativeId,
pub(crate) update: TaskUpdate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UpdateResult {
#[serde(deserialize_with = "present")]
pub(crate) outcome: Option<TaskUpdateOutcome>,
}
fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DeliveredByParams {
pub(crate) id: NativeId,
pub(crate) delivered_by: Vec<TaskRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct DeliveredByResult {
#[serde(default)]
pub(crate) delivered_by: Option<Vec<TaskRef>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "UncheckedMetadataParams")]
pub(crate) struct MetadataParams {
id: NativeId,
key: MetadataKey,
value: Value,
}
impl MetadataParams {
pub(crate) fn new(id: NativeId, key: MetadataKey, value: Value) -> Result<Self, String> {
if key.is_copies()
&& let Some(malformed) = crate::engine::malformed_links(&value)
{
return Err(malformed);
}
Ok(Self { id, key, value })
}
pub(crate) fn id(&self) -> &NativeId {
&self.id
}
pub(crate) fn key(&self) -> &MetadataKey {
&self.key
}
pub(crate) fn value(&self) -> &Value {
&self.value
}
}
#[derive(Deserialize)]
struct UncheckedMetadataParams {
id: NativeId,
#[serde(deserialize_with = "caller_or_copies")]
key: MetadataKey,
value: Value,
}
impl TryFrom<UncheckedMetadataParams> for MetadataParams {
type Error = String;
fn try_from(params: UncheckedMetadataParams) -> Result<Self, Self::Error> {
Self::new(params.id, params.key, params.value)
}
}
fn caller_or_copies<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<MetadataKey, D::Error> {
let key = String::deserialize(deserializer)?;
if key == MetadataKey::COPIES_KEY {
return Ok(MetadataKey::copies());
}
MetadataKey::new(key).map_err(serde::de::Error::custom)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WriteResult {
pub(crate) id: NativeId,
}