homestar_invocation/
task.rs

1//! A [Task] is the smallest unit of work that can be requested from a UCAN.
2
3use crate::{
4    authority::UcanPrf,
5    ipld::{DagCbor, DagJson},
6    Error, Pointer, Unit,
7};
8use libipld::{cid::Cid, serde::from_ipld, Ipld};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13pub mod config;
14pub mod instruction;
15mod result;
16
17pub use config::Resources;
18pub use instruction::Instruction;
19use instruction::RunInstruction;
20pub use result::Result;
21
22const RUN_KEY: &str = "run";
23const CAUSE_KEY: &str = "cause";
24const METADATA_KEY: &str = "meta";
25const PROOF_KEY: &str = "prf";
26
27/// Contains the [Instruction], configuration, and a possible
28/// [Receipt] of the invocation that caused this task to run.
29///
30/// [Instruction]: Instruction
31/// [Receipt]: super::Receipt
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
33#[schemars(
34    rename = "task",
35    description = "Contains a run instruction, configuration, optional reference to receipt that caused task to run, and authorization"
36)]
37pub struct Task<'a, T> {
38    #[schemars(with = "Instruction<'a, T>", rename = "run", title = "Run instruction")]
39    run: RunInstruction<'a, T>,
40    #[schemars(title = "Receipt reference")]
41    cause: Option<Pointer>,
42    #[schemars(with = "Resources", title = "Task Configuration")]
43    meta: Ipld,
44    #[schemars(title = "UCAN Authorization")]
45    prf: UcanPrf,
46}
47
48impl<'a, T> Task<'a, T>
49where
50    Ipld: From<T>,
51    T: Clone,
52{
53    /// Generate a new [Task] to run, with metadata, and `prf`.
54    pub fn new(run: RunInstruction<'a, T>, meta: Ipld, prf: UcanPrf) -> Self {
55        Self {
56            run,
57            cause: None,
58            meta,
59            prf,
60        }
61    }
62
63    /// Generate a new [Task] to execute, with metadata, given a `cause`, and
64    /// `prf`.
65    pub fn new_with_cause(
66        run: RunInstruction<'a, T>,
67        meta: Ipld,
68        prf: UcanPrf,
69        cause: Option<Pointer>,
70    ) -> Self {
71        Self {
72            run,
73            cause,
74            meta,
75            prf,
76        }
77    }
78
79    /// Return a reference pointer to given [Instruction] to run.
80    ///
81    /// [Instruction]: Instruction
82    pub fn run(&self) -> &RunInstruction<'_, T> {
83        &self.run
84    }
85
86    /// Get [Task] metadata in Ipld form.
87    pub fn meta(&self) -> &Ipld {
88        &self.meta
89    }
90
91    /// Turn [Task] into owned [RunInstruction].
92    pub fn into_instruction(self) -> RunInstruction<'a, T> {
93        self.run
94    }
95
96    /// Return the Cid of the contained [Instruction].
97    ///
98    /// [Instruction]: Instruction
99    pub fn instruction_cid(&self) -> std::result::Result<Cid, Error<Unit>> {
100        match &self.run {
101            RunInstruction::Expanded(instruction) => Ok(instruction.to_owned().to_cid()?),
102            RunInstruction::Ptr(instruction_ptr) => Ok(instruction_ptr.cid()),
103        }
104    }
105}
106
107impl<T> From<Task<'_, T>> for Ipld
108where
109    Ipld: From<T>,
110{
111    fn from(task: Task<'_, T>) -> Self {
112        Ipld::Map(BTreeMap::from([
113            (RUN_KEY.into(), task.run.into()),
114            (
115                CAUSE_KEY.into(),
116                task.cause.map_or(Ipld::Null, |cause| cause.into()),
117            ),
118            (METADATA_KEY.into(), task.meta),
119            (PROOF_KEY.into(), task.prf.into()),
120        ]))
121    }
122}
123
124impl<T> TryFrom<Ipld> for Task<'_, T>
125where
126    T: From<Ipld>,
127{
128    type Error = Error<Unit>;
129
130    fn try_from(ipld: Ipld) -> std::result::Result<Self, Self::Error> {
131        let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
132
133        Ok(Self {
134            run: RunInstruction::try_from(
135                map.get(RUN_KEY)
136                    .ok_or_else(|| Error::<Unit>::MissingField(RUN_KEY.to_string()))?
137                    .to_owned(),
138            )?,
139            cause: map
140                .get(CAUSE_KEY)
141                .and_then(|ipld| match ipld {
142                    Ipld::Null => None,
143                    ipld => Some(ipld),
144                })
145                .and_then(|ipld| ipld.to_owned().try_into().ok()),
146            meta: map
147                .get(METADATA_KEY)
148                .ok_or_else(|| Error::<Unit>::MissingField(METADATA_KEY.to_string()))?
149                .to_owned(),
150            prf: UcanPrf::try_from(
151                map.get(PROOF_KEY)
152                    .ok_or_else(|| Error::<Unit>::MissingField(PROOF_KEY.to_string()))?
153                    .to_owned(),
154            )?,
155        })
156    }
157}
158
159impl<T> TryFrom<&Ipld> for Task<'_, T>
160where
161    T: From<Ipld>,
162{
163    type Error = Error<Unit>;
164
165    fn try_from<'a>(ipld: &Ipld) -> std::result::Result<Self, Self::Error> {
166        TryFrom::try_from(ipld.to_owned())
167    }
168}
169
170impl<T> TryFrom<Task<'_, T>> for Pointer
171where
172    Ipld: From<T>,
173{
174    type Error = Error<Unit>;
175
176    fn try_from(task: Task<'_, T>) -> std::result::Result<Self, Self::Error> {
177        Ok(Pointer::new(task.to_cid()?))
178    }
179}
180
181impl<'a, T> DagCbor for Task<'a, T> where Ipld: From<T> {}
182
183impl<T> DagJson for Task<'_, T>
184where
185    T: From<Ipld> + Clone,
186    Ipld: From<T>,
187{
188}
189
190#[cfg(test)]
191mod test {
192    use super::*;
193    use crate::{consts::WASM_MAX_MEMORY, test_utils};
194
195    #[test]
196    fn ipld_roundtrip() {
197        let config = Resources::default();
198        let instruction = test_utils::instruction::<Unit>();
199        let task1 = Task::new(
200            RunInstruction::Expanded(instruction.clone()),
201            config.clone().into(),
202            UcanPrf::default(),
203        );
204
205        let ipld1 = Ipld::from(task1.clone());
206
207        let ipld_task = Ipld::Map(BTreeMap::from([
208            (
209                "rsc".into(),
210                Ipld::String(
211                    "ipfs://bafybeia32q3oy6u47x624rmsmgrrlpn7ulruissmz5z2ap6alv7goe7h3q".into(),
212                ),
213            ),
214            ("op".into(), Ipld::String("ipld/fun".to_string())),
215            ("input".into(), Ipld::List(vec![Ipld::Bool(true)])),
216            ("nnc".into(), Ipld::String("".to_string())),
217        ]));
218
219        assert_eq!(
220            ipld1,
221            Ipld::Map(BTreeMap::from([
222                (RUN_KEY.into(), ipld_task),
223                (CAUSE_KEY.into(), Ipld::Null),
224                (
225                    METADATA_KEY.into(),
226                    Ipld::Map(BTreeMap::from([
227                        ("fuel".into(), Ipld::Integer(u64::MAX.into())),
228                        ("memory".into(), Ipld::Integer(WASM_MAX_MEMORY.into())),
229                        ("time".into(), Ipld::Integer(100_000))
230                    ]))
231                ),
232                (PROOF_KEY.into(), Ipld::List(vec![]))
233            ]))
234        );
235
236        assert_eq!(task1, ipld1.try_into().unwrap());
237
238        let receipt = test_utils::receipt();
239
240        let task2 = Task::new_with_cause(
241            RunInstruction::Ptr::<Ipld>(instruction.try_into().unwrap()),
242            config.into(),
243            UcanPrf::default(),
244            Some(receipt.clone().try_into().unwrap()),
245        );
246
247        let ipld2 = Ipld::from(task2.clone());
248
249        assert_eq!(
250            ipld2,
251            Ipld::Map(BTreeMap::from([
252                (RUN_KEY.into(), Ipld::Link(task2.instruction_cid().unwrap())),
253                (CAUSE_KEY.into(), Ipld::Link(receipt.to_cid().unwrap())),
254                (
255                    METADATA_KEY.into(),
256                    Ipld::Map(BTreeMap::from([
257                        ("fuel".into(), Ipld::Integer(u64::MAX.into())),
258                        ("memory".into(), Ipld::Integer(WASM_MAX_MEMORY.into())),
259                        ("time".into(), Ipld::Integer(100_000))
260                    ]))
261                ),
262                (PROOF_KEY.into(), Ipld::List(vec![]))
263            ]))
264        );
265
266        assert_eq!(task2, ipld2.try_into().unwrap());
267    }
268
269    #[test]
270    fn ser_de() {
271        let config = Resources::default();
272        let instruction = test_utils::instruction::<Unit>();
273        let task = Task::new(
274            RunInstruction::Expanded(instruction.clone()),
275            config.into(),
276            UcanPrf::default(),
277        );
278
279        let ser = serde_json::to_string(&task).unwrap();
280        let de = serde_json::from_str(&ser).unwrap();
281
282        assert_eq!(task, de);
283    }
284}