Skip to main content

onetaskgraph_core/
global_id.rs

1//! Qualifying a source's own id into one a user can type.
2//!
3//! A plugin never sees a [`GlobalId`]; that is why this type lives in the engine
4//! and not in the contract.
5
6use std::fmt;
7use std::str::FromStr;
8
9use onetaskgraph_plugin_api::{NativeId, SourceError, SourceName};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13/// One item, qualified by the source it came from.
14///
15/// Rendered `<source>:<native>` and parsed by splitting on the **first** colon,
16/// so a native id may contain colons freely.
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
18#[serde(try_from = "String", into = "String")]
19pub struct GlobalId {
20    /// The configured source the item came from.
21    pub source: SourceName,
22    /// The source's own opaque id for it.
23    pub native: NativeId,
24}
25
26impl GlobalId {
27    /// The reserved metadata key a copied item records the id it was copied from under.
28    ///
29    /// The correspondence between an item and the one it came from lives on the item, in
30    /// the plugin that owns it, and nowhere else: nothing anywhere holds a mapping, so
31    /// the invariant that the engine keeps no state between calls is untouched.
32    ///
33    /// Spelled here rather than in the plugin contract because the value is a *qualified*
34    /// id, which no plugin ever constructs or interprets — a source stores this key's
35    /// value as it stores any other caller-defined one.
36    pub const ORIGIN_KEY: &'static str = "onetaskgraph.origin";
37
38    /// Qualify `native` as belonging to `source`.
39    #[must_use]
40    pub fn new(source: SourceName, native: NativeId) -> Self {
41        Self { source, native }
42    }
43}
44
45impl fmt::Display for GlobalId {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{}:{}", self.source, self.native)
48    }
49}
50
51impl FromStr for GlobalId {
52    type Err = SourceError;
53
54    fn from_str(value: &str) -> Result<Self, Self::Err> {
55        let Some((source, native)) = value.split_once(':') else {
56            return Err(SourceError::Config {
57                message: format!(
58                    "{value:?} is not a qualified id; write it as <source>:<id>, for example \
59                     work:ENG-1"
60                ),
61            });
62        };
63        if native.is_empty() {
64            return Err(SourceError::Config {
65                message: format!("{value:?} names a source but no id; write it as <source>:<id>"),
66            });
67        }
68        Ok(Self {
69            source: SourceName::new(source)?,
70            native: NativeId::from(native),
71        })
72    }
73}
74
75impl TryFrom<String> for GlobalId {
76    type Error = SourceError;
77
78    fn try_from(value: String) -> Result<Self, Self::Error> {
79        value.parse()
80    }
81}
82
83impl From<GlobalId> for String {
84    fn from(value: GlobalId) -> Self {
85        value.to_string()
86    }
87}