1use 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#[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 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 pub fn ran(&self) -> &Pointer {
67 &self.ran
68 }
69
70 pub fn out(&self) -> &task::Result<T> {
72 &self.out
73 }
74
75 pub fn meta(&self) -> &Ipld {
77 &self.meta
78 }
79
80 pub fn issuer(&self) -> &Option<Issuer> {
82 &self.issuer
83 }
84
85 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}