Skip to main content

onetaskgraph_plugin_api/
metadata.rs

1//! The one key a narrow metadata write names, and the refusal a source that cannot make one
2//! answers with.
3//!
4//! A narrow metadata write sets one key of one record's metadata and changes nothing else
5//! about the record — see [`TaskSource::set_task_metadata`](crate::TaskSource::set_task_metadata).
6//! What a caller may name there is narrower than what a record may hold: a record read from a
7//! source carries this product's own `onetaskgraph.` keys, and a caller writing one of them
8//! through this seam would be editing the store's bookkeeping by hand.
9
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13use crate::SourceError;
14
15/// One caller-owned metadata key: `<namespace>.<name>`, at least two non-empty segments
16/// separated by dots, whose first segment is not [`MetadataKey::RESERVED_NAMESPACE`].
17///
18/// Validated wherever one is built, deserialized included, so a plugin handed one never has
19/// to ask whether it names a key this product owns.
20#[derive(
21    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
22)]
23#[serde(try_from = "String", into = "String")]
24pub struct MetadataKey(String);
25
26impl MetadataKey {
27    /// The namespace this product owns, and a narrow write therefore never names.
28    ///
29    /// Every key the contract reserves lives under it — `onetaskgraph.origin`,
30    /// `onetaskgraph.repositories`, `onetaskgraph.depends_on`, `onetaskgraph.delivers`,
31    /// `onetaskgraph.delivered_by`, `onetaskgraph.item_kind` and `onetaskgraph.template`
32    /// ([`Self::TEMPLATE_KEY`]) — and so does any key it
33    /// reserves later, which is why the whole namespace is refused rather than a list.
34    pub const RESERVED_NAMESPACE: &'static str = "onetaskgraph";
35
36    /// The reserved key a task or a document rendered from a template records where it came
37    /// from under: an object holding the template's reference, the chain digest it was
38    /// rendered with, and the SHA-256 of its content and of its resolved answers.
39    ///
40    /// A key of this product's own, so no [`MetadataKey`] can name it: only a rendering write
41    /// — [`TaskSource::write_task_rendered`](crate::TaskSource::write_task_rendered) and
42    /// [`TaskSource::set_task_rendering`](crate::TaskSource::set_task_rendering) and their
43    /// document siblings — ever sets it, and a copy carries it like any other entry. The
44    /// engine builds and reads the value; a plugin only puts it where its metadata lives.
45    pub const TEMPLATE_KEY: &'static str = "onetaskgraph.template";
46
47    /// One key, once it is established it is a caller's own dotted key.
48    ///
49    /// # Errors
50    ///
51    /// Returns a message saying why, and what to write instead, when the key has no dot, has
52    /// an empty segment, or is in [`Self::RESERVED_NAMESPACE`].
53    pub fn new(key: impl Into<String>) -> Result<Self, String> {
54        let key = key.into();
55        let segments: Vec<&str> = key.split('.').collect();
56        if segments.len() < 2 {
57            return Err(format!(
58                "the metadata key {key:?} has no namespace: a key is `<namespace>.<name>`, two \
59                 or more non-empty segments separated by dots; next: name it under a namespace \
60                 of your own, such as `myapp.{key}`"
61            ));
62        }
63        if segments.iter().any(|segment| segment.is_empty()) {
64            return Err(format!(
65                "the metadata key {key:?} has an empty segment: a key is `<namespace>.<name>`, \
66                 two or more non-empty segments separated by dots; next: remove the extra dot"
67            ));
68        }
69        if segments[0] == Self::RESERVED_NAMESPACE {
70            return Err(format!(
71                "the metadata key {key:?} is in the `{}.` namespace, which this product owns \
72                 and keeps in step itself; next: name the key under a namespace of your own",
73                Self::RESERVED_NAMESPACE
74            ));
75        }
76        Ok(Self(key))
77    }
78
79    /// The key as a record's metadata map spells it.
80    #[must_use]
81    pub fn as_str(&self) -> &str {
82        &self.0
83    }
84}
85
86impl TryFrom<String> for MetadataKey {
87    type Error = String;
88
89    fn try_from(key: String) -> Result<Self, Self::Error> {
90        Self::new(key)
91    }
92}
93
94impl From<MetadataKey> for String {
95    fn from(key: MetadataKey) -> Self {
96        key.0
97    }
98}
99
100impl std::fmt::Display for MetadataKey {
101    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        self.0.fmt(formatter)
103    }
104}
105
106/// Which kind of record a narrow metadata write names.
107///
108/// The three a record can be, and no fourth: a refusal, a not-found error or a protocol guard
109/// that names the record takes one of these rather than a noun, so it cannot name something
110/// that is not a record.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum MetadataRecord {
113    /// A task, written through [`TaskSource::set_task_metadata`](crate::TaskSource::set_task_metadata).
114    Task,
115    /// A project, written through
116    /// [`TaskSource::set_project_metadata`](crate::TaskSource::set_project_metadata).
117    Project,
118    /// A document, written through
119    /// [`TaskSource::set_document_metadata`](crate::TaskSource::set_document_metadata).
120    Document,
121}
122
123impl MetadataRecord {
124    /// The record's noun as a message spells it: `task`, `project` or `document`.
125    #[must_use]
126    pub const fn noun(self) -> &'static str {
127        match self {
128            Self::Task => "task",
129            Self::Project => "project",
130            Self::Document => "document",
131        }
132    }
133}
134
135impl std::fmt::Display for MetadataRecord {
136    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        formatter.write_str(self.noun())
138    }
139}
140
141/// The refusal a source answers a narrow metadata write with when it cannot make one on its
142/// own.
143///
144/// It reads `the linear plugin cannot write a document's metadata on its own`, naming the
145/// record. Spelled once beside [`unwritable_field`](crate::unwritable_field) for that
146/// function's reason: the trait's defaults and the engine's refusal of a plugin whose
147/// handshake does not declare the write say the same thing in the same words.
148#[must_use]
149pub fn unwritable_metadata(kind: &str, record: MetadataRecord) -> SourceError {
150    SourceError::Refused {
151        message: format!("the {kind} plugin cannot write a {record}'s metadata on its own"),
152    }
153}