homestar_invocation/
receipt.rs

1//! Output of an invocation, referenced by its invocation pointer.
2
3use crate::{
4    authority::{Issuer, UcanPrf},
5    ipld::{DagCbor, DagCborRef, DagJson},
6    task, Error, Pointer, Unit,
7};
8use libipld::{cbor::DagCborCodec, prelude::Codec, serde::from_ipld, Ipld};
9use schemars::{
10    gen::SchemaGenerator,
11    schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
12    JsonSchema,
13};
14use std::{
15    borrow::Cow,
16    collections::{BTreeMap, BTreeSet},
17};
18
19pub mod metadata;
20
21const RAN_KEY: &str = "ran";
22const OUT_KEY: &str = "out";
23const ISSUER_KEY: &str = "iss";
24const METADATA_KEY: &str = "meta";
25const PROOF_KEY: &str = "prf";
26
27/// A Receipt is a cryptographically signed description of the [Invocation]
28/// and its [resulting output] and requested effects.
29///
30/// TODO: Effects et al.
31///
32/// [resulting output]: task::Result
33/// [Invocation]: super::Invocation
34#[derive(Debug, Clone, PartialEq)]
35pub struct Receipt<T> {
36    ran: Pointer,
37    out: task::Result<T>,
38    meta: Ipld,
39    issuer: Option<Issuer>,
40    prf: UcanPrf,
41}
42
43impl<T> Receipt<T> {
44    /// Create a new [Receipt].
45    pub fn new(
46        ran: Pointer,
47        result: task::Result<T>,
48        metadata: Ipld,
49        issuer: Option<Issuer>,
50        proof: UcanPrf,
51    ) -> Self {
52        Self {
53            ran,
54            out: result,
55            meta: metadata,
56            issuer,
57            prf: proof,
58        }
59    }
60}
61
62impl<T> Receipt<T> {
63    /// [Pointer] for [Invocation] ran.
64    ///
65    /// [Invocation]: super::Invocation
66    pub fn ran(&self) -> &Pointer {
67        &self.ran
68    }
69
70    /// [task::Result] output from invocation/execution.
71    pub fn out(&self) -> &task::Result<T> {
72        &self.out
73    }
74
75    /// Ipld metadata.
76    pub fn meta(&self) -> &Ipld {
77        &self.meta
78    }
79
80    /// Optional [Issuer] for [Receipt].
81    pub fn issuer(&self) -> &Option<Issuer> {
82        &self.issuer
83    }
84
85    /// [UcanPrf] delegation chain.
86    pub fn prf(&self) -> &UcanPrf {
87        &self.prf
88    }
89}
90
91impl DagJson for Receipt<Ipld> {}
92
93impl TryFrom<Receipt<Ipld>> for Vec<u8> {
94    type Error = Error<Unit>;
95
96    fn try_from(receipt: Receipt<Ipld>) -> Result<Self, Self::Error> {
97        let receipt_ipld = Ipld::from(&receipt);
98        let encoded = DagCborCodec.encode(&receipt_ipld)?;
99        Ok(encoded)
100    }
101}
102
103impl TryFrom<Vec<u8>> for Receipt<Ipld> {
104    type Error = Error<Unit>;
105
106    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
107        let ipld: Ipld = DagCborCodec.decode(&bytes)?;
108        ipld.try_into()
109    }
110}
111
112impl DagCbor for Receipt<Ipld> {}
113impl DagCborRef for Receipt<Ipld> {}
114
115impl From<&Receipt<Ipld>> for Ipld {
116    fn from(receipt: &Receipt<Ipld>) -> Self {
117        Ipld::Map(BTreeMap::from([
118            (RAN_KEY.into(), receipt.ran.to_owned().into()),
119            (OUT_KEY.into(), receipt.out.to_owned().into()),
120            (METADATA_KEY.into(), receipt.meta.to_owned()),
121            (
122                ISSUER_KEY.into(),
123                receipt
124                    .issuer
125                    .as_ref()
126                    .map(|issuer| issuer.to_string().into())
127                    .unwrap_or(Ipld::Null),
128            ),
129            (PROOF_KEY.into(), receipt.prf.to_owned().into()),
130        ]))
131    }
132}
133
134impl From<Receipt<Ipld>> for Ipld {
135    fn from(receipt: Receipt<Ipld>) -> Self {
136        From::from(&receipt)
137    }
138}
139
140impl TryFrom<Ipld> for Receipt<Ipld> {
141    type Error = Error<Unit>;
142
143    fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
144        let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
145
146        let ran = map
147            .get(RAN_KEY)
148            .ok_or_else(|| Error::<Unit>::MissingField(RAN_KEY.to_string()))?
149            .try_into()?;
150
151        let out = map
152            .get(OUT_KEY)
153            .ok_or_else(|| Error::<Unit>::MissingField(OUT_KEY.to_string()))?;
154
155        let meta = map
156            .get(METADATA_KEY)
157            .ok_or_else(|| Error::<Unit>::MissingField(METADATA_KEY.to_string()))?;
158
159        let issuer = map
160            .get(ISSUER_KEY)
161            .and_then(|ipld| match ipld {
162                Ipld::Null => None,
163                ipld => Some(ipld),
164            })
165            .and_then(|ipld| from_ipld(ipld.to_owned()).ok())
166            .map(Issuer::new);
167
168        let prf = map
169            .get(PROOF_KEY)
170            .ok_or_else(|| Error::<Unit>::MissingField(PROOF_KEY.to_string()))?;
171
172        Ok(Receipt {
173            ran,
174            out: task::Result::try_from(out)?,
175            meta: meta.to_owned(),
176            issuer,
177            prf: UcanPrf::try_from(prf)?,
178        })
179    }
180}
181
182impl TryFrom<Receipt<Ipld>> for Pointer {
183    type Error = Error<Unit>;
184
185    fn try_from(receipt: Receipt<Ipld>) -> Result<Self, Self::Error> {
186        Ok(Pointer::new(receipt.to_cid()?))
187    }
188}
189
190impl<T> JsonSchema for Receipt<T> {
191    fn schema_name() -> String {
192        "receipt".to_owned()
193    }
194
195    fn schema_id() -> Cow<'static, str> {
196        Cow::Borrowed("homestar-invocation::receipt::Receipt")
197    }
198
199    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
200        let meta_schema = SchemaObject {
201            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
202            metadata: Some(Box::new(Metadata {
203                title: Some("Receipt metadata".to_string()),
204                description: Some(
205                    "Receipt metadata including the operation that produced the receipt"
206                        .to_string(),
207                ),
208                ..Default::default()
209            })),
210            object: Some(Box::new(ObjectValidation {
211                properties: BTreeMap::from([("op".to_owned(), <String>::json_schema(gen))]),
212                required: BTreeSet::from(["op".to_string()]),
213                ..Default::default()
214            })),
215            ..Default::default()
216        };
217
218        let schema = SchemaObject {
219            instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
220            metadata: Some(Box::new(Metadata {
221                title: Some("Receipt".to_string()),
222                description: Some("A computed receipt".to_string()),
223                ..Default::default()
224            })),
225            object: Some(Box::new(ObjectValidation {
226                properties: BTreeMap::from([
227                    ("ran".to_owned(), gen.subschema_for::<Pointer>()),
228                    ("out".to_owned(), gen.subschema_for::<task::Result<()>>()),
229                    ("meta".to_owned(), Schema::Object(meta_schema)),
230                    ("iss".to_owned(), gen.subschema_for::<Option<Issuer>>()),
231                    ("prf".to_owned(), gen.subschema_for::<UcanPrf>()),
232                ]),
233                required: BTreeSet::from([
234                    "ran".to_string(),
235                    "out".to_string(),
236                    "meta".to_string(),
237                    "prf".to_string(),
238                ]),
239                ..Default::default()
240            })),
241            ..Default::default()
242        };
243
244        schema.into()
245    }
246}