1use gadget_std::borrow::Cow;
2use gadget_std::fmt::Write;
3
4pub type BlueprintString<'a> = std::borrow::Cow<'a, str>;
5pub type Address = ethereum_types::H160;
7
8#[derive(Default, Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
9pub enum FieldType {
10 #[default]
12 Void,
13 Bool,
15 Uint8,
17 Int8,
19 Uint16,
21 Int16,
23 Uint32,
25 Int32,
27 Uint64,
29 Int64,
31 Uint128,
33 U256,
35 Int128,
37 Float64,
39 String,
41 Bytes,
43 Optional(Box<FieldType>),
45 Array(u64, Box<FieldType>),
47 List(Box<FieldType>),
49 Struct(String, Vec<(String, Box<FieldType>)>),
51 Tuple(Vec<FieldType>),
53 AccountId,
56}
57
58impl FieldType {
59 #[must_use]
81 pub fn as_rust_type(&self) -> Cow<'_, str> {
82 match self {
83 FieldType::Uint8 => Cow::Borrowed("u8"),
84 FieldType::Uint16 => Cow::Borrowed("u16"),
85 FieldType::Uint32 => Cow::Borrowed("u32"),
86 FieldType::Uint64 => Cow::Borrowed("u64"),
87 FieldType::Int8 => Cow::Borrowed("i8"),
88 FieldType::Int16 => Cow::Borrowed("i16"),
89 FieldType::Int32 => Cow::Borrowed("i32"),
90 FieldType::Int64 => Cow::Borrowed("i64"),
91 FieldType::Uint128 => Cow::Borrowed("u128"),
92 FieldType::U256 => Cow::Borrowed("U256"),
93 FieldType::Int128 => Cow::Borrowed("i128"),
94 FieldType::Float64 => Cow::Borrowed("f64"),
95 FieldType::Bool => Cow::Borrowed("bool"),
96 FieldType::String => Cow::Borrowed("String"),
97 FieldType::Bytes => Cow::Borrowed("Vec<u8>"),
98 FieldType::AccountId => Cow::Borrowed("AccountId"),
99
100 FieldType::Optional(ty) => Cow::Owned(format!("Option<{}>", ty.as_rust_type())),
101 FieldType::Array(size, ty) => Cow::Owned(format!("[{}; {size}]", ty.as_rust_type())),
102 FieldType::List(ty) => Cow::Owned(format!("Vec<{}>", ty.as_rust_type())),
103 FieldType::Struct(..) => unimplemented!("FieldType::Struct encoding"),
104 FieldType::Tuple(tys) => {
105 let mut s = String::from("(");
106 for ty in tys {
107 write!(s, "{},", ty.as_rust_type()).unwrap();
108 }
109 s.push(')');
110 Cow::Owned(s)
111 }
112 FieldType::Void => panic!("Void is not a representable type"),
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122pub struct ServiceBlueprint<'a> {
123 pub metadata: ServiceMetadata<'a>,
125 pub manager: BlueprintServiceManager,
127 pub master_manager_revision: MasterBlueprintServiceManagerRevision,
132 pub jobs: Vec<JobDefinition<'a>>,
134 pub registration_params: Vec<FieldType>,
136 pub request_params: Vec<FieldType>,
138 pub gadget: Gadget<'a>,
140}
141
142#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143pub struct ServiceMetadata<'a> {
144 pub name: BlueprintString<'a>,
146 pub description: Option<BlueprintString<'a>>,
148 pub author: Option<BlueprintString<'a>>,
151 pub category: Option<BlueprintString<'a>>,
153 pub code_repository: Option<BlueprintString<'a>>,
156 pub logo: Option<BlueprintString<'a>>,
158 pub website: Option<BlueprintString<'a>>,
160 pub license: Option<BlueprintString<'a>>,
162}
163
164#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
167pub struct JobDefinition<'a> {
168 pub job_id: u64,
169 pub metadata: JobMetadata<'a>,
171 pub params: Vec<FieldType>,
174 pub result: Vec<FieldType>,
177}
178
179#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
181#[non_exhaustive]
182pub enum MasterBlueprintServiceManagerRevision {
183 #[default]
187 Latest,
188
189 Specific(u32),
193}
194
195#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub struct JobMetadata<'a> {
197 pub name: BlueprintString<'a>,
199 pub description: Option<BlueprintString<'a>>,
201}
202
203#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
205pub struct ReportDefinition<'a> {
206 pub metadata: ReportMetadata<'a>,
208
209 pub params: Vec<FieldType>,
211
212 pub result: Vec<FieldType>,
214
215 pub report_type: ReportType,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub job_id: Option<u8>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub interval: Option<u64>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub metric_thresholds: Option<Vec<(String, u64)>>,
229
230 pub verifier: ReportResultVerifier,
232}
233
234#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[serde(rename_all = "lowercase")]
237pub enum ReportType {
238 #[default]
240 Job,
241 QoS,
243}
244
245#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
247#[serde(rename_all = "lowercase")]
248pub enum ReportResultVerifier {
249 #[default]
251 None,
252 Evm(String),
254}
255
256#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
257pub struct ReportMetadata<'a> {
258 pub name: BlueprintString<'a>,
260 pub description: Option<BlueprintString<'a>>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
266#[non_exhaustive]
267pub enum BlueprintServiceManager {
268 Evm(String),
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
273pub enum Gadget<'a> {
274 Wasm(WasmGadget<'a>),
277 Native(NativeGadget<'a>),
280 Container(ContainerGadget<'a>),
283}
284
285impl Default for Gadget<'_> {
286 fn default() -> Self {
287 Gadget::Wasm(WasmGadget {
288 runtime: WasmRuntime::Wasmtime,
289 sources: vec![],
290 })
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
301pub struct GithubFetcher<'a> {
302 pub owner: BlueprintString<'a>,
304 pub repo: BlueprintString<'a>,
306 pub tag: BlueprintString<'a>,
309 pub binaries: Vec<GadgetBinary<'a>>,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
314pub struct TestFetcher<'a> {
315 pub cargo_package: BlueprintString<'a>,
316 pub cargo_bin: BlueprintString<'a>,
317 pub base_path: BlueprintString<'a>,
318}
319
320#[derive(
322 PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Copy, serde::Serialize, serde::Deserialize,
323)]
324pub enum Architecture {
325 Wasm,
327 Wasm64,
329 Wasi,
331 Wasi64,
333 Amd,
335 Amd64,
337 Arm,
339 Arm64,
341 RiscV,
343 RiscV64,
345}
346
347#[derive(
349 Default,
350 PartialEq,
351 PartialOrd,
352 Ord,
353 Eq,
354 Debug,
355 Clone,
356 Copy,
357 serde::Serialize,
358 serde::Deserialize,
359)]
360pub enum OperatingSystem {
361 #[default]
365 Unknown,
366 Linux,
368 Windows,
370 MacOS,
372 BSD,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
377pub struct GadgetBinary<'a> {
378 pub arch: Architecture,
380 pub os: OperatingSystem,
382 pub name: BlueprintString<'a>,
384 #[serde(default)]
387 pub sha256: [u8; 32],
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
391#[serde(from = "GadgetSourceFlat<'_>")]
392pub struct GadgetSource<'a> {
393 pub fetcher: GadgetSourceFetcher<'a>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
398#[serde(untagged)]
399enum GadgetSourceFlat<'a> {
400 WithFetcher { fetcher: GadgetSourceFetcher<'a> },
401 Plain(GadgetSourceFetcher<'a>),
402}
403
404impl<'a> From<GadgetSourceFlat<'a>> for GadgetSource<'a> {
405 fn from(flat: GadgetSourceFlat<'a>) -> GadgetSource<'a> {
406 match flat {
407 GadgetSourceFlat::Plain(fetcher) | GadgetSourceFlat::WithFetcher { fetcher } => {
408 GadgetSource { fetcher }
409 }
410 }
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
417#[serde(from = "DynamicGadgetSourceFetcher<'_>")]
418pub enum GadgetSourceFetcher<'a> {
419 #[allow(clippy::upper_case_acronyms)]
421 IPFS(Vec<u8>),
422 Github(GithubFetcher<'a>),
424 ContainerImage(ImageRegistryFetcher<'a>),
426 Testing(TestFetcher<'a>),
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
431#[serde(untagged)]
432enum DynamicGadgetSourceFetcher<'a> {
433 Tagged(TaggedGadgetSourceFetcher<'a>),
434 Untagged(UntaggedGadgetSourceFetcher<'a>),
435}
436
437#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
438struct CidWrapper(cid::Cid);
439
440impl<'de> serde::Deserialize<'de> for CidWrapper {
441 fn deserialize<D>(deserializer: D) -> Result<CidWrapper, D::Error>
442 where
443 D: serde::Deserializer<'de>,
444 {
445 let str_value = String::deserialize(deserializer)?;
446 let cid = cid::Cid::try_from(str_value).map_err(serde::de::Error::custom)?;
447 Ok(CidWrapper(cid))
448 }
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
454enum TaggedGadgetSourceFetcher<'a> {
455 #[allow(clippy::upper_case_acronyms)]
457 IPFS(CidWrapper),
458 Github(GithubFetcher<'a>),
460 ContainerImage(ImageRegistryFetcher<'a>),
462 Testing(TestFetcher<'a>),
464}
465
466#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
469#[serde(untagged)]
470enum UntaggedGadgetSourceFetcher<'a> {
471 #[allow(clippy::upper_case_acronyms)]
473 IPFS(CidWrapper),
474 Github(GithubFetcher<'a>),
476 ContainerImage(ImageRegistryFetcher<'a>),
478 Testing(TestFetcher<'a>),
480}
481
482impl<'a> From<DynamicGadgetSourceFetcher<'a>> for GadgetSourceFetcher<'a> {
483 fn from(f: DynamicGadgetSourceFetcher<'a>) -> GadgetSourceFetcher<'a> {
484 match f {
485 DynamicGadgetSourceFetcher::Tagged(tagged) => tagged.into(),
486 DynamicGadgetSourceFetcher::Untagged(untagged) => untagged.into(),
487 }
488 }
489}
490
491impl<'a> From<UntaggedGadgetSourceFetcher<'a>> for GadgetSourceFetcher<'a> {
492 fn from(untagged: UntaggedGadgetSourceFetcher<'a>) -> GadgetSourceFetcher<'a> {
493 match untagged {
494 UntaggedGadgetSourceFetcher::IPFS(hash) => GadgetSourceFetcher::IPFS(hash.0.to_bytes()),
495 UntaggedGadgetSourceFetcher::Github(fetcher) => GadgetSourceFetcher::Github(fetcher),
496 UntaggedGadgetSourceFetcher::ContainerImage(fetcher) => {
497 GadgetSourceFetcher::ContainerImage(fetcher)
498 }
499 UntaggedGadgetSourceFetcher::Testing(fetcher) => GadgetSourceFetcher::Testing(fetcher),
500 }
501 }
502}
503
504impl<'a> From<TaggedGadgetSourceFetcher<'a>> for GadgetSourceFetcher<'a> {
505 fn from(tagged: TaggedGadgetSourceFetcher<'a>) -> GadgetSourceFetcher<'a> {
506 match tagged {
507 TaggedGadgetSourceFetcher::IPFS(hash) => GadgetSourceFetcher::IPFS(hash.0.to_bytes()),
508 TaggedGadgetSourceFetcher::Github(fetcher) => GadgetSourceFetcher::Github(fetcher),
509 TaggedGadgetSourceFetcher::ContainerImage(fetcher) => {
510 GadgetSourceFetcher::ContainerImage(fetcher)
511 }
512 TaggedGadgetSourceFetcher::Testing(fetcher) => GadgetSourceFetcher::Testing(fetcher),
513 }
514 }
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
518pub struct ImageRegistryFetcher<'a> {
519 registry: BlueprintString<'a>,
521 image: BlueprintString<'a>,
523 tag: BlueprintString<'a>,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
529pub struct WasmGadget<'a> {
530 pub runtime: WasmRuntime,
532 pub sources: Vec<GadgetSource<'a>>,
534}
535
536#[derive(Copy, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
537pub enum WasmRuntime {
538 Wasmtime,
540 Wasmer,
542}
543
544#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
546pub struct NativeGadget<'a> {
547 pub sources: Vec<GadgetSource<'a>>,
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
552pub struct ContainerGadget<'a> {
553 pub sources: Vec<GadgetSource<'a>>,
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use gadget_std::path::PathBuf;
561
562 #[test]
563 fn test_blueprint_deserialization() {
564 const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR");
565
566 let base_path = PathBuf::from(CRATE_ROOT)
567 .join("../../..")
568 .join("blueprints/incredible-squaring/")
569 .canonicalize()
570 .unwrap();
571
572 let output = std::process::Command::new("cargo")
573 .arg("build")
574 .current_dir(&base_path)
575 .output()
576 .expect("Failed to run cargo build");
577 assert!(
578 output.status.success(),
579 "Failed to build incredible-squaring: {}",
580 String::from_utf8_lossy(&output.stderr)
581 );
582
583 let blueprint_path = base_path.join("blueprint.json");
584
585 let blueprint_content =
586 std::fs::read_to_string(blueprint_path).expect("Failed to read blueprint.json");
587
588 let blueprint_content: serde_json::Value = serde_json::from_str(&blueprint_content)
589 .expect("Failed to deserialize blueprint.json file");
590
591 let gadget: Gadget<'static> =
593 serde_json::from_str(&blueprint_content["gadget"].to_string())
594 .expect("Failed to deserialize blueprint.json");
595
596 if let Gadget::Native(gadget) = gadget {
599 for src in gadget.sources {
600 if let GadgetSourceFetcher::Testing(testing) = src.fetcher {
601 assert_eq!(PathBuf::from(testing.base_path.to_string()), base_path);
602 assert_eq!(testing.cargo_bin, "main");
603 assert_eq!(testing.cargo_package, "incredible-squaring-blueprint");
604 return;
605 }
606 }
607 } else {
608 panic!("Unexpected Gadget variant");
609 }
610
611 panic!(
612 "The sources included with the `gadget` field does not have a valid entry for Testing"
613 )
614 }
615}