homestar_invocation/
pointer.rs

1#![allow(missing_docs)]
2
3//! Pointers and references to [Invocations], [Tasks], [Instructions], and/or
4//! [Receipts], as well as handling for the [Await]'ed promises of pointers.
5//!
6//! [Invocations]: super::Invocation
7//! [Tasks]: super::Task
8//! [Instructions]: crate::task::Instruction
9//! [Receipts]: super::Receipt
10
11use crate::{ensure, Error, Unit};
12use const_format::formatcp;
13#[cfg(feature = "diesel")]
14use diesel::{
15    backend::Backend,
16    deserialize::{self, FromSql, FromSqlRow},
17    expression::AsExpression,
18    serialize::{self, IsNull, Output, ToSql},
19    sql_types::Text,
20    sqlite::Sqlite,
21};
22use enum_assoc::Assoc;
23use libipld::{cid::Cid, serde::from_ipld, Ipld, Link};
24use schemars::{
25    gen::SchemaGenerator,
26    schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
27    JsonSchema,
28};
29use serde::{Deserialize, Serialize};
30#[cfg(feature = "diesel")]
31use std::str::FromStr;
32use std::{borrow::Cow, collections::btree_map::BTreeMap, fmt, module_path};
33
34/// `await/ok` branch for instruction result.
35pub const OK_BRANCH: &str = "await/ok";
36/// `await/error` branch for instruction result.
37pub const ERR_BRANCH: &str = "await/error";
38/// `await/*` branch for instruction result.
39pub const PTR_BRANCH: &str = "await/*";
40
41/// Enumerated wrapper around resulting branches of a promise
42/// that's being awaited on.
43///
44/// Variants and branch strings are interchangable:
45///
46/// # Example
47///
48/// ```
49/// use homestar_invocation::pointer::AwaitResult;
50///
51/// let await_result = AwaitResult::Error;
52/// assert_eq!(await_result.branch(), "await/error");
53/// assert_eq!(AwaitResult::result("await/*").unwrap(), AwaitResult::Ptr);
54/// ```
55#[derive(Clone, Debug, PartialEq, Eq, Assoc, Deserialize, Serialize)]
56#[func(pub const fn branch(&self) -> &'static str)]
57#[func(pub fn result(s: &str) -> Option<Self>)]
58pub enum AwaitResult {
59    /// `Ok` branch.
60    #[assoc(branch = OK_BRANCH)]
61    #[assoc(result = OK_BRANCH)]
62    Ok,
63    /// `Error` branch.
64    #[assoc(branch = ERR_BRANCH)]
65    #[assoc(result = ERR_BRANCH)]
66    Error,
67    /// Direct resulting branch, without unwrapping of success or failure.
68    #[assoc(branch = PTR_BRANCH)]
69    #[assoc(result = PTR_BRANCH)]
70    Ptr,
71}
72
73impl fmt::Display for AwaitResult {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            AwaitResult::Error => write!(f, "await/error"),
77            AwaitResult::Ok => write!(f, "await/ok"),
78            AwaitResult::Ptr => write!(f, "await/*"),
79        }
80    }
81}
82
83impl JsonSchema for AwaitResult {
84    fn schema_name() -> String {
85        "await_result".to_owned()
86    }
87
88    fn schema_id() -> Cow<'static, str> {
89        Cow::Borrowed(formatcp!("{}::AwaitResult", module_path!()))
90    }
91
92    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
93        let mut schema = SchemaObject {
94            instance_type: None,
95            metadata: Some(Box::new(Metadata {
96                title: Some("Await result".to_string()),
97                description: Some("Branches of a promise that is awaited".to_string()),
98                ..Default::default()
99            })),
100            ..Default::default()
101        };
102
103        let await_ok = SchemaObject {
104            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
105            object: Some(Box::new(ObjectValidation {
106                properties: BTreeMap::from([(
107                    OK_BRANCH.to_string(),
108                    gen.subschema_for::<Pointer>(),
109                )]),
110                ..Default::default()
111            })),
112            ..Default::default()
113        };
114        let await_err = SchemaObject {
115            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
116            object: Some(Box::new(ObjectValidation {
117                properties: BTreeMap::from([(
118                    ERR_BRANCH.to_string(),
119                    gen.subschema_for::<Pointer>(),
120                )]),
121                ..Default::default()
122            })),
123            ..Default::default()
124        };
125        let await_ptr = SchemaObject {
126            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
127            object: Some(Box::new(ObjectValidation {
128                properties: BTreeMap::from([(
129                    PTR_BRANCH.to_string(),
130                    gen.subschema_for::<Pointer>(),
131                )]),
132                ..Default::default()
133            })),
134            ..Default::default()
135        };
136
137        schema.subschemas().one_of = Some(vec![
138            Schema::Object(await_ok),
139            Schema::Object(await_err),
140            Schema::Object(await_ptr),
141        ]);
142        schema.into()
143    }
144}
145
146/// Describes the eventual output of the referenced [Instruction] as a
147/// [Pointer], either resolving to a tagged [OK_BRANCH], [ERR_BRANCH], or direct
148/// result of a [PTR_BRANCH].
149///
150/// [Instruction]: crate::task::Instruction
151#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
152pub struct Await {
153    instruction: Pointer,
154    result: AwaitResult,
155}
156
157impl Await {
158    /// A new `Promise` [Await]'ed on, resulting in a [Pointer]
159    /// and [AwaitResult].
160    pub fn new(instruction: Pointer, result: AwaitResult) -> Self {
161        Self {
162            instruction,
163            result,
164        }
165    }
166
167    /// Return Cid to [Instruction] being [Await]'ed on.
168    ///
169    /// [Instruction]: crate::task::Instruction
170    pub fn instruction_cid(&self) -> Cid {
171        self.instruction.cid()
172    }
173
174    /// Return [AwaitResult] branch.
175    pub fn result(&self) -> &AwaitResult {
176        &self.result
177    }
178}
179
180impl From<Await> for Ipld {
181    fn from(await_promise: Await) -> Self {
182        Ipld::Map(BTreeMap::from([(
183            await_promise.result.branch().to_string(),
184            await_promise.instruction.into(),
185        )]))
186    }
187}
188
189impl From<&Await> for Ipld {
190    fn from(await_promise: &Await) -> Self {
191        From::from(await_promise.to_owned())
192    }
193}
194
195impl TryFrom<Ipld> for Await {
196    type Error = Error<Unit>;
197
198    fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
199        let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
200        ensure!(
201            map.len() == 1,
202            Error::ConditionNotMet(
203                "await promise must have only a single key in a map".to_string()
204            )
205        );
206
207        let (key, value) = map.into_iter().next().unwrap();
208        let instruction = Pointer::try_from(value)?;
209
210        let result = match key.as_str() {
211            OK_BRANCH => AwaitResult::Ok,
212            ERR_BRANCH => AwaitResult::Error,
213            _ => AwaitResult::Ptr,
214        };
215
216        Ok(Await {
217            instruction,
218            result,
219        })
220    }
221}
222
223impl TryFrom<&Ipld> for Await {
224    type Error = Error<Unit>;
225
226    fn try_from(ipld: &Ipld) -> Result<Self, Self::Error> {
227        TryFrom::try_from(ipld.to_owned())
228    }
229}
230
231/// References a specific [Invocation], [Task], [Instruction], and/or
232/// [Receipt], always wrapping a Cid.
233///
234/// [Invocation]: super::Invocation
235/// [Task]: super::Task
236/// [Instruction]: crate::task::Instruction
237/// [Receipt]: super::Receipt
238#[cfg(feature = "diesel")]
239#[cfg_attr(docsrs, doc(cfg(feature = "diesel")))]
240#[derive(
241    Clone,
242    Debug,
243    AsExpression,
244    FromSqlRow,
245    PartialEq,
246    Eq,
247    Serialize,
248    Deserialize,
249    Hash,
250    PartialOrd,
251    Ord,
252)]
253#[diesel(sql_type = Text)]
254#[repr(transparent)]
255pub struct Pointer(Cid);
256
257/// References a specific [Invocation], [Task], [Instruction], or
258/// [Receipt], always wrapping a Cid.
259///
260/// [Invocation]: super::Invocation
261/// [Task]: super::Task
262/// [Instruction]: super::Instruction
263/// [Receipt]: super::Receipt
264#[cfg(not(feature = "diesel"))]
265#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
266#[repr(transparent)]
267pub struct Pointer(Cid);
268
269impl fmt::Display for Pointer {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        let cid_as_string = self.0.to_string();
272        write!(f, "{cid_as_string}")
273    }
274}
275
276impl Pointer {
277    /// Return the `inner` Cid for the [Pointer].
278    pub fn cid(&self) -> Cid {
279        self.0
280    }
281
282    /// Wrap an [Pointer] for a given Cid.
283    pub fn new(cid: Cid) -> Self {
284        Pointer(cid)
285    }
286
287    /// Convert an `Ipld::Link` to an [Pointer].
288    pub fn new_from_link<T>(link: Link<T>) -> Self {
289        Pointer(*link)
290    }
291}
292
293impl From<Pointer> for Ipld {
294    fn from(ptr: Pointer) -> Self {
295        Ipld::Link(ptr.cid())
296    }
297}
298
299impl TryFrom<Ipld> for Pointer {
300    type Error = Error<Unit>;
301
302    fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
303        let s: Cid = from_ipld(ipld)?;
304        Ok(Pointer(s))
305    }
306}
307
308impl TryFrom<&Ipld> for Pointer {
309    type Error = Error<Unit>;
310
311    fn try_from(ipld: &Ipld) -> Result<Self, Self::Error> {
312        TryFrom::try_from(ipld.to_owned())
313    }
314}
315
316impl<'a> From<Pointer> for Cow<'a, Pointer> {
317    fn from(ptr: Pointer) -> Self {
318        Cow::Owned(ptr)
319    }
320}
321
322impl<'a> From<&'a Pointer> for Cow<'a, Pointer> {
323    fn from(ptr: &'a Pointer) -> Self {
324        Cow::Borrowed(ptr)
325    }
326}
327
328#[cfg(feature = "diesel")]
329#[cfg_attr(docsrs, doc(cfg(feature = "diesel")))]
330impl ToSql<Text, Sqlite> for Pointer {
331    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
332        out.set_value(self.cid().to_string());
333        Ok(IsNull::No)
334    }
335}
336
337#[cfg(feature = "diesel")]
338#[cfg_attr(docsrs, doc(cfg(feature = "diesel")))]
339impl<DB> FromSql<Text, DB> for Pointer
340where
341    DB: Backend,
342    String: FromSql<Text, DB>,
343{
344    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
345        let s = String::from_sql(bytes)?;
346        Ok(Pointer::new(Cid::from_str(&s)?))
347    }
348}
349
350impl JsonSchema for Pointer {
351    fn schema_name() -> String {
352        "pointer".to_owned()
353    }
354
355    fn schema_id() -> Cow<'static, str> {
356        Cow::Borrowed(formatcp!("{}::Pointer", module_path!()))
357    }
358
359    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
360        let schema = SchemaObject {
361            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
362            object: Some(Box::new(ObjectValidation {
363                properties: BTreeMap::from([('/'.to_string(), <String>::json_schema(gen))]),
364                ..Default::default()
365            })),
366            metadata: Some(Box::new(Metadata {
367                description: Some(
368                    "CID reference to an invocation, task, instruction, or receipt".to_string(),
369                ),
370                ..Default::default()
371            })),
372            ..Default::default()
373        };
374        schema.into()
375    }
376}
377
378#[cfg(test)]
379mod test {
380    use super::*;
381    use crate::test_utils::cid::generate_cid;
382    use rand::thread_rng;
383
384    #[test]
385    fn ser_de_pointer() {
386        let pointer = Pointer::new(generate_cid(&mut thread_rng()));
387        let ser = serde_json::to_string(&pointer).unwrap();
388        let de = serde_json::from_str(&ser).unwrap();
389
390        assert_eq!(pointer, de);
391    }
392
393    #[test]
394    fn ser_de_await() {
395        let awaited = Await::new(
396            Pointer::new(generate_cid(&mut thread_rng())),
397            AwaitResult::Ok,
398        );
399        let ser = serde_json::to_string(&awaited).unwrap();
400        let de = serde_json::from_str(&ser).unwrap();
401
402        assert_eq!(awaited, de);
403    }
404}