Skip to main content

onetaskgraph_core/engine/
metadata.rs

1//! One key of one record's metadata, set on its own.
2//!
3//! The three verbs — `task`, `project` and `document metadata set` — share one answer and one
4//! path through the engine, which differ only in which of the source's three narrow writes is
5//! called and what a missing record is called. Nothing here reads the record first: the
6//! source's own write says whether it holds the record, and the record it answers with is
7//! what the answer is read off, so what is reported is what the source now holds rather than
8//! what it was handed.
9//!
10//! Metadata is not status, so no delivered task is re-evaluated after one of these writes.
11
12use onetaskgraph_plugin_api::{Location, MetadataKey, MetadataRecord, SourceError};
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use super::delivery::source_failed;
18use super::{Engine, EngineError};
19use crate::GlobalId;
20use crate::resolve::ResolvedSource;
21
22/// What `task`, `project` and `document metadata set` answer with.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
24pub struct MetadataSet {
25    /// The record whose metadata key was set.
26    pub id: GlobalId,
27    /// The key that was set.
28    pub key: MetadataKey,
29    /// The value the source holds under `key` as it reads the record back after the write —
30    /// which is not always the value it was handed.
31    pub value: Value,
32    /// Where the source reports the record to be, left out when it does not say.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub location: Option<Location>,
35}
36
37impl Engine {
38    /// Set one key of one task's metadata, and nothing else about it.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`EngineError::UnknownSource`] for a source nothing configures,
43    /// [`EngineError::MetadataNotWritable`] for one with no write side,
44    /// [`EngineError::NoSuchTask`] when the task is not there, and
45    /// [`EngineError::SourceFailed`] when the source refuses — a plugin that cannot write one
46    /// key on its own included.
47    pub async fn set_task_metadata(
48        &self,
49        id: &GlobalId,
50        key: &MetadataKey,
51        value: &Value,
52    ) -> Result<MetadataSet, EngineError> {
53        let source = self.metadata_writable(id, MetadataRecord::Task)?;
54        let task = source
55            .source()
56            .set_task_metadata(&id.native, key, value)
57            .await
58            .map_err(|error| source_failed(source, error))?
59            .ok_or_else(|| EngineError::NoSuchTask { id: id.to_string() })?;
60        answer(source, id, key, &task.metadata, task.location)
61    }
62
63    /// Set one key of one project's metadata, and nothing else about it.
64    ///
65    /// # Errors
66    ///
67    /// As [`set_task_metadata`](Self::set_task_metadata), with
68    /// [`EngineError::NoSuchProject`] when the project is not there.
69    pub async fn set_project_metadata(
70        &self,
71        id: &GlobalId,
72        key: &MetadataKey,
73        value: &Value,
74    ) -> Result<MetadataSet, EngineError> {
75        let source = self.metadata_writable(id, MetadataRecord::Project)?;
76        let project = source
77            .source()
78            .set_project_metadata(&id.native, key, value)
79            .await
80            .map_err(|error| source_failed(source, error))?
81            .ok_or_else(|| EngineError::NoSuchProject { id: id.to_string() })?;
82        answer(source, id, key, &project.metadata, project.location)
83    }
84
85    /// Set one key of one document's metadata, and nothing else about it.
86    ///
87    /// # Errors
88    ///
89    /// As [`set_task_metadata`](Self::set_task_metadata), with [`EngineError::NoDocuments`]
90    /// for a source declaring it has none — which is never asked — and
91    /// [`EngineError::NoSuchDocument`] when the document is not there.
92    pub async fn set_document_metadata(
93        &self,
94        id: &GlobalId,
95        key: &MetadataKey,
96        value: &Value,
97    ) -> Result<MetadataSet, EngineError> {
98        let source = self.metadata_writable(id, MetadataRecord::Document)?;
99        let document = source
100            .source()
101            .set_document_metadata(&id.native, key, value)
102            .await
103            .map_err(|error| source_failed(source, error))?
104            .ok_or_else(|| EngineError::NoSuchDocument { id: id.to_string() })?;
105        answer(source, id, key, &document.metadata, document.location)
106    }
107
108    /// The built source `id` names, when a metadata write of `record` may be sent to it.
109    fn metadata_writable(
110        &self,
111        id: &GlobalId,
112        record: MetadataRecord,
113    ) -> Result<&ResolvedSource, EngineError> {
114        let source = self.built(&id.source)?;
115        if !source.source().writes().is_supported() {
116            return Err(EngineError::MetadataNotWritable {
117                name: source.name().to_string(),
118                kind: source.kind().to_owned(),
119                record,
120            });
121        }
122        if record == MetadataRecord::Document
123            && !source.source().capabilities().documents.is_native()
124        {
125            return Err(EngineError::NoDocuments {
126                name: source.name().to_string(),
127                kind: source.kind().to_owned(),
128            });
129        }
130        Ok(source)
131    }
132}
133
134/// The answer, read off the record the source answered the write with.
135///
136/// A record that does not hold the key it was just written under is not an answer this can
137/// report a value from, so it is refused as malformed rather than reported as `null`.
138fn answer(
139    source: &ResolvedSource,
140    id: &GlobalId,
141    key: &MetadataKey,
142    metadata: &std::collections::BTreeMap<String, Value>,
143    location: Option<Location>,
144) -> Result<MetadataSet, EngineError> {
145    let value = metadata.get(key.as_str()).cloned().ok_or_else(|| {
146        source_failed(
147            source,
148            SourceError::Malformed {
149                message: format!(
150                    "the record {id} it answered the write with does not hold the key {key} it \
151                     was just written under"
152                ),
153            },
154        )
155    })?;
156    Ok(MetadataSet {
157        id: id.clone(),
158        key: key.clone(),
159        value,
160        location,
161    })
162}