Skip to main content

onetaskgraph_plugin_api/
write.rs

1//! The write seam a destination is reached through, and the two refusals a source with
2//! nothing on one side of the contract answers with.
3//!
4//! [`TaskSource`](crate::TaskSource) is a read interface, and it stays one for every
5//! source that has nothing to write into: both write methods are defaulted, so a source
6//! that cannot be written needs no edit and keeps refusing by saying so. What a source
7//! opts into is [`WriteSupport::Supported`], and what that opt-in owes is real
8//! implementations of [`TaskSource::write_task`](crate::TaskSource::write_task) and
9//! [`TaskSource::write_project`](crate::TaskSource::write_project).
10
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::{DependencyEdge, NativeId, SourceError};
15
16/// Whether a source can be written through at all.
17///
18/// Deliberately its own enum rather than a reuse of [`Support`](crate::Support): that one
19/// says whether a source applies a *predicate* itself, and the engine compensates for an
20/// `Unsupported` there by narrowing a wider result. There is no compensating for a
21/// destination that cannot be written — the copy is refused, naming the source and the
22/// plugin behind it — so conflating the two would invite an engine-side workaround for a
23/// case that has none.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
25#[serde(rename_all = "kebab-case")]
26pub enum WriteSupport {
27    /// The source creates and updates items through its own write interface.
28    Supported,
29    /// The source has no write side; a copy naming it as a destination is refused.
30    Unsupported,
31}
32
33impl WriteSupport {
34    /// Whether this source can be written through.
35    #[must_use]
36    pub fn is_supported(self) -> bool {
37        matches!(self, Self::Supported)
38    }
39}
40
41/// One create-or-update of a work item at a destination.
42///
43/// The two cases are one type because a destination decides between them by exactly one
44/// question — is there an item here to update — and the engine has already answered it.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
46pub struct ItemWrite<T> {
47    /// The destination item to update, or `None` to create one.
48    ///
49    /// A source handed a `target` it does not hold returns [`SourceError::Refused`]
50    /// rather than creating: the engine established that id before asking, so an absent
51    /// one is a race the destination must not paper over.
52    pub target: Option<NativeId>,
53    /// The item as the destination should hold it once the write lands.
54    ///
55    /// Its `id` is the id the item was read under at the **source**. A destination
56    /// updating an item addresses [`target`](Self::target) and ignores it; a destination
57    /// creating one may derive a name from it and is free not to. Its `url`, `location`,
58    /// `created_at` and `updated_at` are the destination's own and are never written —
59    /// where the *source* holds an item says nothing about where the destination does.
60    pub item: T,
61    /// The forward dependency edges the copy read, with their far ends already resolved.
62    ///
63    /// Each edge's `from` is the item being written as the *source* named it, so a
64    /// destination reads the `to` and the `kind` of each and supplies its own near end.
65    /// A `to` naming another source is qualified and stays that way; a `to` inside the
66    /// copied set arrives as the destination's own native id.
67    #[serde(default)]
68    pub depends_on: Vec<DependencyEdge>,
69}
70
71/// The refusal a source with no write side answers a write with.
72///
73/// Spelled once so every unwritten source refuses in the same words, and so a plugin's
74/// refusal and the engine's own message about it cannot describe different things.
75#[must_use]
76pub fn unwritable(kind: &str) -> SourceError {
77    SourceError::Refused {
78        message: format!("the {kind} plugin cannot be written"),
79    }
80}
81
82/// The refusal a source with no documents answers a document read with.
83///
84/// Spelled once beside [`unwritable`], and here rather than beside the reads it answers,
85/// because the two are the same kind of thing: a source saying it does not have that side
86/// of the contract at all. Every document-free source therefore refuses in the same words,
87/// and a plugin's refusal cannot describe something different from the engine's own
88/// message about it.
89///
90/// The two document *writes* reuse [`unwritable`]: a source with no write side refuses a
91/// document write for the reason it refuses every other write, and saying it twice in two
92/// wordings would make one refusal read as two.
93#[must_use]
94pub fn documentless(kind: &str) -> SourceError {
95    SourceError::Refused {
96        message: format!("the {kind} plugin has no documents"),
97    }
98}