Skip to main content

onetaskgraph_plugin_api/
write.rs

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