1use crate::{
5 ipld::{self, DagCbor},
6 pointer::AwaitResult,
7 Error, Pointer, Unit,
8};
9use libipld::{cid::multibase::Base, serde::from_ipld, Ipld};
10use schemars::{
11 gen::SchemaGenerator,
12 schema::{
13 ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject,
14 SingleOrVec,
15 },
16 JsonSchema,
17};
18use serde::{Deserialize, Serialize};
19use serde_json::json;
20use std::{
21 borrow::Cow,
22 collections::{BTreeMap, BTreeSet},
23 fmt,
24};
25use url::Url;
26
27const RESOURCE_KEY: &str = "rsc";
28const OP_KEY: &str = "op";
29const INPUT_KEY: &str = "input";
30const NNC_KEY: &str = "nnc";
31
32mod ability;
33pub mod input;
34mod nonce;
35pub use ability::*;
36pub use input::{Args, Input, Parse, Parsed};
37pub use nonce::*;
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub enum RunInstruction<'a, T> {
43 Expanded(Instruction<'a, T>),
45 Ptr(Pointer),
47}
48
49impl<'a, T> From<Instruction<'a, T>> for RunInstruction<'a, T> {
50 fn from(instruction: Instruction<'a, T>) -> Self {
51 RunInstruction::Expanded(instruction)
52 }
53}
54
55impl<'a, T> TryFrom<RunInstruction<'a, T>> for Instruction<'a, T>
56where
57 T: fmt::Debug,
58{
59 type Error = Error<RunInstruction<'a, T>>;
60
61 fn try_from(run: RunInstruction<'a, T>) -> Result<Self, Self::Error> {
62 match run {
63 RunInstruction::Expanded(instruction) => Ok(instruction),
64 e => Err(Error::InvalidDiscriminant(e)),
65 }
66 }
67}
68
69impl<T> From<Pointer> for RunInstruction<'_, T> {
70 fn from(ptr: Pointer) -> Self {
71 RunInstruction::Ptr(ptr)
72 }
73}
74
75impl<'a, T> TryFrom<RunInstruction<'a, T>> for Pointer
76where
77 T: fmt::Debug,
78{
79 type Error = Error<RunInstruction<'a, T>>;
80
81 fn try_from(run: RunInstruction<'a, T>) -> Result<Self, Self::Error> {
82 match run {
83 RunInstruction::Ptr(ptr) => Ok(ptr),
84 e => Err(Error::InvalidDiscriminant(e)),
85 }
86 }
87}
88
89impl<'a, 'b, T> TryFrom<&'b RunInstruction<'a, T>> for &'b Pointer
90where
91 T: fmt::Debug,
92{
93 type Error = Error<&'b RunInstruction<'a, T>>;
94
95 fn try_from(run: &'b RunInstruction<'a, T>) -> Result<Self, Self::Error> {
96 match run {
97 RunInstruction::Ptr(ptr) => Ok(ptr),
98 e => Err(Error::InvalidDiscriminant(e)),
99 }
100 }
101}
102
103impl<'a, 'b, T> TryFrom<&'b RunInstruction<'a, T>> for Pointer
104where
105 T: fmt::Debug,
106{
107 type Error = Error<&'b RunInstruction<'a, T>>;
108
109 fn try_from(run: &'b RunInstruction<'a, T>) -> Result<Self, Self::Error> {
110 match run {
111 RunInstruction::Ptr(ptr) => Ok(ptr.to_owned()),
112 e => Err(Error::InvalidDiscriminant(e)),
113 }
114 }
115}
116
117impl<T> From<RunInstruction<'_, T>> for Ipld
118where
119 Ipld: From<T>,
120{
121 fn from(run: RunInstruction<'_, T>) -> Self {
122 match run {
123 RunInstruction::Expanded(instruction) => instruction.into(),
124 RunInstruction::Ptr(instruction_ptr) => instruction_ptr.into(),
125 }
126 }
127}
128
129impl<T> TryFrom<Ipld> for RunInstruction<'_, T>
130where
131 T: From<Ipld>,
132{
133 type Error = Error<Unit>;
134
135 fn try_from<'a>(ipld: Ipld) -> Result<Self, Self::Error> {
136 match ipld {
137 Ipld::Map(_) => Ok(RunInstruction::Expanded(Instruction::try_from(ipld)?)),
138 Ipld::Link(_) => Ok(RunInstruction::Ptr(Pointer::try_from(ipld)?)),
139 other_ipld => Err(Error::unexpected_ipld(other_ipld)),
140 }
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
201pub struct Instruction<'a, T> {
202 rsc: Url,
203 op: Cow<'a, Ability>,
204 input: Input<T>,
205 nnc: Nonce,
206}
207
208impl<T> Instruction<'_, T> {
209 pub fn new(rsc: Url, ability: Ability, input: Input<T>) -> Self {
211 Self {
212 rsc,
213 op: Cow::from(ability),
214 input,
215 nnc: Nonce::Empty,
216 }
217 }
218
219 pub fn new_with_nonce(rsc: Url, ability: Ability, input: Input<T>, nnc: Nonce) -> Self {
221 Self {
222 rsc,
223 op: Cow::from(ability),
224 input,
225 nnc,
226 }
227 }
228
229 pub fn unique(rsc: Url, ability: Ability, input: Input<T>) -> Self {
231 Self {
232 rsc,
233 op: Cow::from(ability),
234 input,
235 nnc: Nonce::generate(),
236 }
237 }
238
239 pub fn resource(&self) -> &Url {
241 &self.rsc
242 }
243
244 pub fn op(&self) -> &Ability {
246 &self.op
247 }
248
249 pub fn input(&self) -> &Input<T> {
251 &self.input
252 }
253
254 pub fn nonce(&self) -> &Nonce {
256 &self.nnc
257 }
258}
259
260impl<T> TryFrom<Instruction<'_, T>> for Pointer
261where
262 Ipld: From<T>,
263{
264 type Error = Error<Unit>;
265
266 fn try_from(instruction: Instruction<'_, T>) -> Result<Self, Self::Error> {
267 Ok(Pointer::new(instruction.to_cid()?))
268 }
269}
270
271impl<T> From<Instruction<'_, T>> for Ipld
272where
273 Ipld: From<T>,
274{
275 fn from(instruction: Instruction<'_, T>) -> Self {
276 Ipld::Map(BTreeMap::from([
277 (RESOURCE_KEY.into(), instruction.rsc.to_string().into()),
278 (OP_KEY.into(), instruction.op.to_string().into()),
279 (INPUT_KEY.into(), instruction.input.into()),
280 (NNC_KEY.into(), instruction.nnc.into()),
281 ]))
282 }
283}
284
285impl<T> TryFrom<&Ipld> for Instruction<'_, T>
286where
287 T: From<Ipld>,
288{
289 type Error = Error<Unit>;
290
291 fn try_from(ipld: &Ipld) -> Result<Self, Self::Error> {
292 TryFrom::try_from(ipld.to_owned())
293 }
294}
295
296impl<T> TryFrom<Ipld> for Instruction<'_, T>
297where
298 T: From<Ipld>,
299{
300 type Error = Error<Unit>;
301
302 fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
303 let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
304
305 let rsc = match map.get(RESOURCE_KEY) {
306 Some(Ipld::Link(cid)) => cid
307 .to_string_of_base(Base::Base32Lower) .map_err(Error::<Unit>::CidEncode)
309 .and_then(|txt| {
310 Url::parse(format!("{}{}", "ipfs://", txt).as_str())
311 .map_err(Error::ParseResource)
312 }),
313 Some(Ipld::String(txt)) => Url::parse(txt.as_str()).map_err(Error::ParseResource),
314 _ => Err(Error::MissingField(RESOURCE_KEY.to_string())),
315 }?;
316
317 Ok(Self {
318 rsc,
319 op: from_ipld(
320 map.get(OP_KEY)
321 .ok_or_else(|| Error::<Unit>::MissingField(OP_KEY.to_string()))?
322 .to_owned(),
323 )?,
324 input: Input::try_from(
325 map.get(INPUT_KEY)
326 .ok_or_else(|| Error::<String>::MissingField(INPUT_KEY.to_string()))?
327 .to_owned(),
328 )?,
329 nnc: Nonce::try_from(
330 map.get(NNC_KEY)
331 .unwrap_or(&Ipld::String("".to_string()))
332 .to_owned(),
333 )?,
334 })
335 }
336}
337
338impl<'a, T> DagCbor for Instruction<'a, T> where Ipld: From<T> {}
339
340impl<'a, T> JsonSchema for Instruction<'a, T> {
341 fn schema_name() -> String {
342 "run".to_owned()
343 }
344
345 fn schema_id() -> Cow<'static, str> {
346 Cow::Borrowed("homestar-invocation::task::Instruction")
347 }
348
349 fn json_schema(gen: &mut SchemaGenerator) -> Schema {
350 struct InputConditional {
351 if_schema: Schema,
352 then_schema: Schema,
353 else_schema: Schema,
354 }
355
356 fn input_conditional(gen: &mut SchemaGenerator) -> InputConditional {
357 let if_schema = SchemaObject {
358 instance_type: None,
359 object: Some(Box::new(ObjectValidation {
360 properties: BTreeMap::from([(
361 "op".to_owned(),
362 Schema::Object(SchemaObject {
363 instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
364 const_value: Some(json!("wasm/run")),
365 ..Default::default()
366 }),
367 )]),
368 ..Default::default()
369 })),
370 ..Default::default()
371 };
372
373 let func_schema = SchemaObject {
374 instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
375 metadata: Some(Box::new(Metadata {
376 description: Some("The function to call on the Wasm resource".to_string()),
377 ..Default::default()
378 })),
379 ..Default::default()
380 };
381
382 let args_schema = SchemaObject {
383 instance_type: Some(SingleOrVec::Single(InstanceType::Array.into())),
384 metadata: Some(Box::new(Metadata {
385 description: Some(
386 "Arguments to the function. May await a result from another task."
387 .to_string(),
388 ),
389 ..Default::default()
390 })),
391 array: Some(Box::new(ArrayValidation {
392 items: Some(SingleOrVec::Vec(vec![
393 gen.subschema_for::<ipld::schema::IpldStub>(),
394 gen.subschema_for::<AwaitResult>(),
395 ])),
396 ..Default::default()
397 })),
398 ..Default::default()
399 };
400
401 let input_schema = SchemaObject {
402 instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
403 object: Some(Box::new(ObjectValidation {
404 properties: BTreeMap::from([
405 ("func".to_string(), Schema::Object(func_schema)),
406 ("args".to_string(), Schema::Object(args_schema)),
407 ]),
408 required: BTreeSet::from(["func".to_string(), "args".to_string()]),
409 ..Default::default()
410 })),
411 ..Default::default()
412 };
413
414 let then_schema = SchemaObject {
415 instance_type: None,
416 object: Some(Box::new(ObjectValidation {
417 properties: BTreeMap::from([(
418 "input".to_string(),
419 Schema::Object(input_schema),
420 )]),
421 ..Default::default()
422 })),
423 ..Default::default()
424 };
425
426 InputConditional {
427 if_schema: Schema::Object(if_schema),
428 then_schema: Schema::Object(then_schema),
429 else_schema: Schema::Bool(false),
430 }
431 }
432
433 let op_schema = SchemaObject {
434 instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
435 metadata: Some(Box::new(Metadata {
436 description: Some("Function executor".to_string()),
437 ..Default::default()
438 })),
439 enum_values: Some(vec![json!("wasm/run")]),
440 ..Default::default()
441 };
442
443 let mut schema = SchemaObject {
444 instance_type: Some(SingleOrVec::Single(InstanceType::Object.into())),
445 metadata: Some(Box::new(Metadata {
446 title: Some("Run instruction".to_string()),
447 description: Some("An instruction that runs a function from a resource, executor that will run the function, inputs to the executor, and optional nonce".to_string()),
448 ..Default::default()
449 })),
450 object: Some(Box::new(ObjectValidation {
451 properties: BTreeMap::from([
452 ("rsc".to_owned(), <Url>::json_schema(gen)),
453 ("op".to_owned(), Schema::Object(op_schema)),
454 ("nnc".to_owned(), <Nonce>::json_schema(gen))
455 ]),
456 required: BTreeSet::from(["rsc".to_string(), "op".to_string(), "input".to_string(), "nnc".to_string()]),
457 ..Default::default()
458 })),
459 ..Default::default()
460 };
461
462 let input = input_conditional(gen);
463 schema.subschemas().if_schema = Some(Box::new(input.if_schema));
464 schema.subschemas().then_schema = Some(Box::new(input.then_schema));
465 schema.subschemas().else_schema = Some(Box::new(input.else_schema));
466
467 schema.into()
468 }
469}
470
471#[cfg(test)]
472mod test {
473 use super::*;
474 use crate::{test_utils, DAG_CBOR};
475 use libipld::{
476 cbor::DagCborCodec,
477 multihash::{Code, MultihashDigest},
478 prelude::Codec,
479 Cid,
480 };
481
482 #[test]
483 fn ipld_roundtrip() {
484 let (instruction, bytes) = test_utils::instruction_with_nonce::<Unit>();
485 let ipld = Ipld::from(instruction.clone());
486
487 assert_eq!(
488 ipld,
489 Ipld::Map(BTreeMap::from([
490 (
491 RESOURCE_KEY.into(),
492 Ipld::String(
493 "ipfs://bafybeia32q3oy6u47x624rmsmgrrlpn7ulruissmz5z2ap6alv7goe7h3q".into()
494 )
495 ),
496 (OP_KEY.into(), Ipld::String("ipld/fun".to_string())),
497 (INPUT_KEY.into(), Ipld::List(vec![Ipld::Bool(true)])),
498 (NNC_KEY.into(), Ipld::Bytes(bytes))
499 ]))
500 );
501 assert_eq!(instruction, ipld.try_into().unwrap())
502 }
503
504 #[test]
505 fn ipld_cid_trials() {
506 let a_cid =
507 Cid::try_from("bafyrmiev5j2jzjrqncbfqo6pbraiw7r2p527m4z3bbm6ir3o5kdz2zwcjy").unwrap();
508 let ipld = libipld::ipld!({"input":
509 {
510 "args": [{"await/ok": a_cid}, "111111"],
511 "func": "join-strings"
512 },
513 "nnc": "", "op": "wasm/run",
514 "rsc": "ipfs://bafybeia32q3oy6u47x624rmsmgrrlpn7ulruissmz5z2ap6alv7goe7h3q"});
515
516 let instruction = Instruction::<Unit>::try_from(ipld.clone()).unwrap();
517 let instr_cid = instruction.to_cid().unwrap();
518
519 let bytes = DagCborCodec.encode(&ipld).unwrap();
520 let hash = Code::Sha3_256.digest(&bytes);
521 let ipld_to_cid = Cid::new_v1(DAG_CBOR, hash);
522
523 assert_eq!(ipld_to_cid, instr_cid);
524 }
525
526 #[test]
527 fn ser_de() {
528 let (instruction, _bytes) = test_utils::instruction_with_nonce::<Unit>();
529 let ser = serde_json::to_string(&instruction).unwrap();
530 let de = serde_json::from_str(&ser).unwrap();
531
532 assert_eq!(instruction, de);
533 }
534}