Skip to main content

gadget_blueprint_proc_macro_core/
lib.rs

1use gadget_std::borrow::Cow;
2use gadget_std::fmt::Write;
3
4pub type BlueprintString<'a> = std::borrow::Cow<'a, str>;
5/// A type that represents an EVM Address.
6pub type Address = ethereum_types::H160;
7
8#[derive(Default, Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
9pub enum FieldType {
10    /// A Field of `void` type.
11    #[default]
12    Void,
13    /// A Field of `bool` type.
14    Bool,
15    /// A Field of `u8` type.
16    Uint8,
17    /// A Field of `i8` type.
18    Int8,
19    /// A Field of `u16` type.
20    Uint16,
21    /// A Field of `i16` type.
22    Int16,
23    /// A Field of `u32` type.
24    Uint32,
25    /// A Field of `i32` type.
26    Int32,
27    /// A Field of `u64` type.
28    Uint64,
29    /// A Field of `i64` type.
30    Int64,
31    /// A field of `u128` type.
32    Uint128,
33    /// A field of `u256` type
34    U256,
35    /// A field of `i128` type.
36    Int128,
37    /// A field of `f64` type.
38    Float64,
39    /// A Field of `String` type.
40    String,
41    /// A Field of `Vec<u8>` type.
42    Bytes,
43    /// A Field of `Option<T>` type.
44    Optional(Box<FieldType>),
45    /// An array of N items of type [`FieldType`].
46    Array(u64, Box<FieldType>),
47    /// A List of items of type [`FieldType`].
48    List(Box<FieldType>),
49    /// A Struct of items of type [`FieldType`].
50    Struct(String, Vec<(String, Box<FieldType>)>),
51    /// Tuple
52    Tuple(Vec<FieldType>),
53    // NOTE: Special types starts from 100
54    /// A special type for `AccountId`
55    AccountId,
56}
57
58impl FieldType {
59    /// Returns the Rust type representation of this field type as a string.
60    ///
61    /// This method converts the `FieldType` enum variant into its corresponding Rust type string.
62    ///
63    /// # Returns
64    /// A `Cow<'_, str>` containing the Rust type as a string.
65    ///
66    /// # Panics
67    /// Panics if called on `FieldType::Void` since it has no representable type.
68    /// Also panics if called on `FieldType::Struct` which is currently unimplemented.
69    ///
70    /// # Examples
71    /// ```
72    /// use gadget_blueprint_proc_macro_core::FieldType;
73    ///
74    /// let uint8_type = FieldType::Uint8;
75    /// assert_eq!(uint8_type.as_rust_type(), "u8");
76    ///
77    /// let optional_type = FieldType::Optional(Box::new(FieldType::Bool));
78    /// assert_eq!(optional_type.as_rust_type(), "Option<bool>");
79    /// ```
80    #[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/// The main definition of a service.
118///
119/// This contains the metadata of the service, the job definitions, and other hooks, along with the
120/// gadget that will be executed when one of the jobs is calling this service.
121#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122pub struct ServiceBlueprint<'a> {
123    /// The metadata of the service.
124    pub metadata: ServiceMetadata<'a>,
125    /// The blueprint manager that will be used to manage the blueprints lifecycle.
126    pub manager: BlueprintServiceManager,
127    /// The Revision number of the Master Blueprint Service Manager.
128    ///
129    /// If not sure what to use, use `MasterBlueprintServiceManagerRevision::default()` which will use
130    /// the latest revision available.
131    pub master_manager_revision: MasterBlueprintServiceManagerRevision,
132    /// The job definitions that are available in this service.
133    pub jobs: Vec<JobDefinition<'a>>,
134    /// The parameters that are required for the service registration.
135    pub registration_params: Vec<FieldType>,
136    /// The parameters that are required for the service request.
137    pub request_params: Vec<FieldType>,
138    /// The gadget that will be executed for the service.
139    pub gadget: Gadget<'a>,
140}
141
142#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143pub struct ServiceMetadata<'a> {
144    /// The Service name.
145    pub name: BlueprintString<'a>,
146    /// The Service description.
147    pub description: Option<BlueprintString<'a>>,
148    /// The Service author.
149    /// Could be a company or a person.
150    pub author: Option<BlueprintString<'a>>,
151    /// The Job category.
152    pub category: Option<BlueprintString<'a>>,
153    /// Code Repository URL.
154    /// Could be a github, gitlab, or any other code repository.
155    pub code_repository: Option<BlueprintString<'a>>,
156    /// Service Logo URL.
157    pub logo: Option<BlueprintString<'a>>,
158    /// Service Website URL.
159    pub website: Option<BlueprintString<'a>>,
160    /// Service License.
161    pub license: Option<BlueprintString<'a>>,
162}
163
164/// A Job Definition is a definition of a job that can be called.
165/// It contains the input and output fields of the job with the permitted caller.
166#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
167pub struct JobDefinition<'a> {
168    pub job_id: u64,
169    /// The metadata of the job.
170    pub metadata: JobMetadata<'a>,
171    /// These are parameters that are required for this job.
172    /// i.e. the input.
173    pub params: Vec<FieldType>,
174    /// These are the result, the return values of this job.
175    /// i.e. the output.
176    pub result: Vec<FieldType>,
177}
178
179/// Master Blueprint Service Manager Revision.
180#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
181#[non_exhaustive]
182pub enum MasterBlueprintServiceManagerRevision {
183    /// Use Whatever the latest revision available on-chain.
184    ///
185    /// This is the default value.
186    #[default]
187    Latest,
188
189    /// Use a specific revision number.
190    ///
191    /// Note: Must be already deployed on-chain.
192    Specific(u32),
193}
194
195#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub struct JobMetadata<'a> {
197    /// The Job name.
198    pub name: BlueprintString<'a>,
199    /// The Job description.
200    pub description: Option<BlueprintString<'a>>,
201}
202
203/// Represents the definition of a report, including its metadata, parameters, and result type.
204#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
205pub struct ReportDefinition<'a> {
206    /// Metadata about the report, including its name and description.
207    pub metadata: ReportMetadata<'a>,
208
209    /// List of parameter types for the report function.
210    pub params: Vec<FieldType>,
211
212    /// List of result types for the report function.
213    pub result: Vec<FieldType>,
214
215    /// The type of report (Job or `QoS`).
216    pub report_type: ReportType,
217
218    /// The ID of the job this report is associated with (for job reports only).
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub job_id: Option<u8>,
221
222    /// The interval at which this report should be run (for `QoS` reports only).
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub interval: Option<u64>,
225
226    /// Optional metric thresholds for `QoS` reports.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub metric_thresholds: Option<Vec<(String, u64)>>,
229
230    /// The verifier to use for this report's results.
231    pub verifier: ReportResultVerifier,
232}
233
234/// Enum representing the type of report (Job or `QoS`).
235#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[serde(rename_all = "lowercase")]
237pub enum ReportType {
238    /// A report associated with a specific job.
239    #[default]
240    Job,
241    /// A report for Quality of Service metrics.
242    QoS,
243}
244
245/// Enum representing the type of verifier for the report result.
246#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
247#[serde(rename_all = "lowercase")]
248pub enum ReportResultVerifier {
249    /// No verifier specified.
250    #[default]
251    None,
252    /// An EVM-based verifier contract.
253    Evm(String),
254}
255
256#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
257pub struct ReportMetadata<'a> {
258    /// The Job name.
259    pub name: BlueprintString<'a>,
260    /// The Job description.
261    pub description: Option<BlueprintString<'a>>,
262}
263
264/// Service Blueprint Manager is a smart contract that will manage the service lifecycle.
265#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
266#[non_exhaustive]
267pub enum BlueprintServiceManager {
268    /// A Smart contract that will manage the service lifecycle.
269    Evm(String),
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
273pub enum Gadget<'a> {
274    /// A Gadget that is a WASM binary that will be executed.
275    /// inside the shell using the wasm runtime.
276    Wasm(WasmGadget<'a>),
277    /// A Gadget that is a native binary that will be executed.
278    /// inside the shell using the OS.
279    Native(NativeGadget<'a>),
280    /// A Gadget that is a container that will be executed.
281    /// inside the shell using the container runtime (e.g. Docker, Podman, etc.)
282    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/// A binary that is stored in the GitHub release.
295///
296/// This will construct the URL to the release and download the binary.
297/// The URL will be in the following format:
298///
299/// `https://github.com/<owner>/<repo>/releases/download/v<tag>/<path>`
300#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
301pub struct GithubFetcher<'a> {
302    /// The owner of the repository.
303    pub owner: BlueprintString<'a>,
304    /// The repository name.
305    pub repo: BlueprintString<'a>,
306    /// The release tag of the repository.
307    /// NOTE: The tag should be a valid semver tag.
308    pub tag: BlueprintString<'a>,
309    /// The names of the binary in the release by the arch and the os.
310    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/// The CPU or System architecture.
321#[derive(
322    PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Copy, serde::Serialize, serde::Deserialize,
323)]
324pub enum Architecture {
325    /// WebAssembly architecture (32-bit).
326    Wasm,
327    /// WebAssembly architecture (64-bit).
328    Wasm64,
329    /// WASI architecture (32-bit).
330    Wasi,
331    /// WASI architecture (64-bit).
332    Wasi64,
333    /// Amd architecture (32-bit).
334    Amd,
335    /// Amd64 architecture (`x86_64`).
336    Amd64,
337    /// Arm architecture (32-bit).
338    Arm,
339    /// Arm64 architecture (64-bit).
340    Arm64,
341    /// Risc-V architecture (32-bit).
342    RiscV,
343    /// Risc-V architecture (64-bit).
344    RiscV64,
345}
346
347/// Operating System that the binary is compiled for.
348#[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    /// Unknown operating system.
362    /// This is used when the operating system is not known
363    /// for example, for WASM, where the OS is not relevant.
364    #[default]
365    Unknown,
366    /// Linux operating system.
367    Linux,
368    /// Windows operating system.
369    Windows,
370    /// `MacOS` operating system.
371    MacOS,
372    /// BSD operating system.
373    BSD,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
377pub struct GadgetBinary<'a> {
378    /// CPU or System architecture.
379    pub arch: Architecture,
380    /// Operating System that the binary is compiled for.
381    pub os: OperatingSystem,
382    /// The name of the binary.
383    pub name: BlueprintString<'a>,
384    /// The sha256 hash of the binary.
385    /// used to verify the downloaded binary.
386    #[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    /// The fetcher that will fetch the gadget from a remote source.
394    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/// A Gadget Source Fetcher is a fetcher that will fetch the gadget
415/// from a remote source.
416#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
417#[serde(from = "DynamicGadgetSourceFetcher<'_>")]
418pub enum GadgetSourceFetcher<'a> {
419    /// A Gadget that will be fetched from the IPFS.
420    #[allow(clippy::upper_case_acronyms)]
421    IPFS(Vec<u8>),
422    /// A Gadget that will be fetched from the Github release.
423    Github(GithubFetcher<'a>),
424    /// A Gadgets that will be fetched from the container registry.
425    ContainerImage(ImageRegistryFetcher<'a>),
426    /// For testing
427    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/// A Gadget Source Fetcher is a fetcher that will fetch the gadget
452/// from a remote source.
453#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
454enum TaggedGadgetSourceFetcher<'a> {
455    /// A Gadget that will be fetched from the IPFS.
456    #[allow(clippy::upper_case_acronyms)]
457    IPFS(CidWrapper),
458    /// A Gadget that will be fetched from the Github release.
459    Github(GithubFetcher<'a>),
460    /// A Gadgets that will be fetched from the container registry.
461    ContainerImage(ImageRegistryFetcher<'a>),
462    /// For testing
463    Testing(TestFetcher<'a>),
464}
465
466/// A Gadget Source Fetcher is a fetcher that will fetch the gadget
467/// from a remote source.
468#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
469#[serde(untagged)]
470enum UntaggedGadgetSourceFetcher<'a> {
471    /// A Gadget that will be fetched from the IPFS.
472    #[allow(clippy::upper_case_acronyms)]
473    IPFS(CidWrapper),
474    /// A Gadget that will be fetched from the Github release.
475    Github(GithubFetcher<'a>),
476    /// A Gadgets that will be fetched from the container registry.
477    ContainerImage(ImageRegistryFetcher<'a>),
478    /// Testing
479    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    /// The URL of the container registry.
520    registry: BlueprintString<'a>,
521    /// The name of the image.
522    image: BlueprintString<'a>,
523    /// The tag of the image.
524    tag: BlueprintString<'a>,
525}
526
527/// A WASM binary that contains all the compiled gadget code.
528#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
529pub struct WasmGadget<'a> {
530    /// Which runtime to use to execute the WASM binary.
531    pub runtime: WasmRuntime,
532    /// Where the WASM binary is stored.
533    pub sources: Vec<GadgetSource<'a>>,
534}
535
536#[derive(Copy, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
537pub enum WasmRuntime {
538    /// The WASM binary will be executed using the `WASMtime` runtime.
539    Wasmtime,
540    /// The WASM binary will be executed using the Wasmer runtime.
541    Wasmer,
542}
543
544/// A Native binary that contains all the gadget code.
545#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
546pub struct NativeGadget<'a> {
547    /// Where the WASM binary is stored.
548    pub sources: Vec<GadgetSource<'a>>,
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
552pub struct ContainerGadget<'a> {
553    /// Where the Image of the gadget binary is stored.
554    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        // Deserialize the entire Blueprint
592        let gadget: Gadget<'static> =
593            serde_json::from_str(&blueprint_content["gadget"].to_string())
594                .expect("Failed to deserialize blueprint.json");
595
596        // Assertions
597
598        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}