blueprint_tangle_extra/metadata/
job_definition.rs

1//! Job definition traits and implementations for Tangle jobs.
2//!
3//! This module provides traits and implementations for converting Rust functions into Tangle job
4//! definitions that can be registered with a Tangle Services pallet. The main traits are:
5//!
6//! - [`IntoJobMetadata`]: Converts a type into [`JobMetadata`]
7//! - [`IntoJobDefinition`]: Converts a [`Job`] function into a complete [`JobDefinition`]
8//! - [`IntoTangleFieldTypes`]: Defines how a type maps to a Tangle [`FieldType`]
9//!
10//! # Examples
11//!
12//! ```
13//! use blueprint_core::extract::Context;
14//! use blueprint_tangle_extra::extract::TangleArg;
15//! use blueprint_tangle_extra::extract::TangleResult;
16//! use blueprint_tangle_extra::metadata::IntoJobDefinition;
17//!
18//! // Define a simple squaring job
19//! async fn square_job(
20//!     Context(_): Context<u64>,
21//!     TangleArg(x): TangleArg<u64>,
22//! ) -> TangleResult<u64> {
23//!     TangleResult(x * x)
24//! }
25//!
26//! // Convert the function into a job definition
27//! let job_definition = square_job.into_job_definition();
28//! ```
29//!
30//! [`Job`]: blueprint_core::Job
31
32use crate::serde::{BoundedVec, new_bounded_string};
33use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::field::FieldType;
34use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::jobs::{
35    JobDefinition, JobMetadata,
36};
37
38/// Trait for types that can be converted into job metadata.
39///
40/// This trait is implemented for all types and uses the type name as the job name.
41pub trait IntoJobMetadata {
42    /// Converts the type into job metadata.
43    ///
44    /// # Returns
45    ///
46    /// A [`JobMetadata`] instance with the name set to the type name.
47    fn into_job_metadata(self) -> JobMetadata;
48}
49
50impl<T> IntoJobMetadata for T {
51    fn into_job_metadata(self) -> JobMetadata {
52        JobMetadata {
53            name: new_bounded_string(core::any::type_name::<T>().to_string()),
54            description: None,
55        }
56    }
57}
58
59/// Trait for types that can be converted into a job definition.
60///
61/// This trait is implemented for functions that can be executed as Tangle jobs.
62pub trait IntoJobDefinition<T> {
63    /// Converts the implementor into a job definition.
64    ///
65    /// # Returns
66    ///
67    /// A [`JobDefinition`] instance with metadata, parameters, and result types.
68    fn into_job_definition(self) -> JobDefinition;
69}
70
71/// Trait for types that can be converted into Tangle field types.
72///
73/// This trait defines how a Rust type maps to Tangle field types.
74pub trait IntoTangleFieldTypes {
75    /// Returns a vector of [`FieldType`] values that represent the type.
76    ///
77    /// # Returns
78    ///
79    /// A vector of [`FieldType`] values.
80    fn into_tangle_fields() -> Vec<FieldType>;
81}
82
83/// Implementation for [`Void`] type.
84///
85/// [Void]: blueprint_core::job_result::Void
86impl IntoTangleFieldTypes for blueprint_core::job_result::Void {
87    fn into_tangle_fields() -> Vec<FieldType> {
88        Vec::from([FieldType::Void])
89    }
90}
91
92/// Implementation for functions with no arguments.
93impl<F, Fut, Res> IntoJobDefinition<((),)> for F
94where
95    F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
96    Fut: Future<Output = Res> + Send,
97    Res: IntoTangleFieldTypes,
98{
99    fn into_job_definition(self) -> JobDefinition {
100        JobDefinition {
101            metadata: self.into_job_metadata(),
102            params: BoundedVec(Vec::new()),
103            result: BoundedVec(Res::into_tangle_fields()),
104        }
105    }
106}
107
108/// Macro to implement [`IntoJobDefinition`] for functions with arguments.
109///
110/// This macro generates implementations for functions with different numbers of arguments.
111macro_rules! impl_into_job_definition {
112    (
113        [$($ty:ident),*], $last:ident
114    ) => {
115        impl<F, Fut, Res, $($ty,)* $last> IntoJobDefinition<((), $($ty,)* $last,)> for F
116        where
117            F: FnOnce($($ty,)* $last,) -> Fut + Clone + Send + Sync + 'static,
118            Fut: Future<Output = Res> + Send,
119            $last: IntoTangleFieldTypes,
120            Res: IntoTangleFieldTypes,
121        {
122            fn into_job_definition(self) -> JobDefinition {
123                JobDefinition {
124                    metadata: self.into_job_metadata(),
125                    params: BoundedVec($last::into_tangle_fields()),
126                    result: BoundedVec(Res::into_tangle_fields()),
127                }
128            }
129        }
130    };
131}
132
133// Use the all_the_tuples macro from blueprint_core to generate implementations
134// for functions with different numbers of arguments.
135blueprint_core::all_the_tuples!(impl_into_job_definition);
136
137#[cfg(test)]
138#[allow(clippy::type_complexity)]
139mod tests {
140    use blueprint_core::extract::Context;
141
142    use super::*;
143    use crate::extract::{
144        TangleArg, TangleArgs2, TangleArgs3, TangleArgs4, TangleArgs5, TangleArgs6, TangleArgs7,
145        TangleArgs8, TangleArgs9, TangleArgs10, TangleArgs11, TangleArgs12, TangleArgs13,
146        TangleArgs14, TangleArgs15, TangleArgs16, TangleResult,
147    };
148
149    macro_rules! count {
150        ($val:ident, $($rest:tt)*) => {
151            1 + count!($($rest)*)
152        };
153        ($val:ident) => {
154            1
155        };
156        () => {
157            0
158        }
159    }
160
161    async fn empty() -> TangleResult<u64> {
162        TangleResult(0)
163    }
164
165    #[test]
166    fn empty_test() {
167        let empty = empty.into_job_definition();
168        assert_eq!(
169            empty.metadata.name.0.0,
170            b"blueprint_tangle_extra::metadata::job_definition::tests::empty",
171            "expected empty, got {}",
172            std::str::from_utf8(&empty.metadata.name.0.0).unwrap()
173        );
174        assert!(empty.params.0.is_empty());
175        assert_eq!(empty.result.0[0], FieldType::Uint64);
176    }
177
178    #[derive(Debug, Default, Clone, Copy)]
179    struct MyContext {
180        foo: u64,
181    }
182
183    #[derive(Debug)]
184    struct MyError;
185
186    impl std::fmt::Display for MyError {
187        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188            write!(f, "MyError")
189        }
190    }
191
192    impl std::error::Error for MyError {}
193
194    macro_rules! definition_tests {
195        ($($func:ident($args_ty:ident($($args:ident),*): $ty:ty));* $(;)?) => {
196            $(
197                #[allow(unused_variables)]
198                async fn $func($args_ty($($args),*): $ty) -> TangleResult<u64> {
199                    todo!()
200                }
201
202                paste::paste! {
203                    #[allow(unused_variables)]
204                    #[allow(clippy::type_complexity)]
205                    async fn [<$func _with_context>](Context(ctx): Context<MyContext>, $args_ty($($args),*): $ty) -> TangleResult<u64> {
206                        eprintln!("ctx: {:?}", ctx.foo);
207                        todo!()
208                    }
209
210                    #[allow(unused_variables)]
211                    #[allow(clippy::type_complexity)]
212                    async fn [<$func _with_context_ret_res>](Context(ctx): Context<MyContext>, $args_ty($($args),*): $ty) -> Result<TangleResult<u64>, MyError> {
213                        eprintln!("ctx: {:?}", ctx.foo);
214                        todo!()
215                    }
216
217
218                    #[allow(unused_variables)]
219                    #[allow(clippy::type_complexity)]
220                    async fn [<$func _with_context_ret_opt>](Context(ctx): Context<MyContext>, $args_ty($($args),*): $ty) -> Option<TangleResult<u64>> {
221                        eprintln!("ctx: {:?}", ctx.foo);
222                        todo!()
223                    }
224
225                    #[test]
226                    fn [<$func _test>]() {
227                        let def = $func.into_job_definition();
228                        assert_eq!(
229                            def.metadata.name.0.0,
230                            format!("blueprint_tangle_extra::metadata::job_definition::tests::{}", stringify!($func)).as_bytes(),
231                        );
232                        assert_eq!(def.params.0.len(), count!($($args),*));
233                        assert_eq!(def.result.0.len(), 1);
234                        assert!(def.params.0.iter().all(|ty| ty.clone() == FieldType::Uint64));
235                        assert_eq!(def.result.0[0], FieldType::Uint64);
236
237                        let def2 = [<$func _with_context>].into_job_definition();
238                        assert_eq!(
239                            def2.metadata.name.0.0,
240                            format!("blueprint_tangle_extra::metadata::job_definition::tests::{}", stringify!([<$func _with_context>])).as_bytes(),
241                        );
242                        assert_eq!(def2.params.0.len(), count!($($args),*));
243                        assert_eq!(def2.result.0.len(), 1);
244                        assert!(def2.params.0.iter().all(|ty| ty.clone() == FieldType::Uint64));
245                        assert_eq!(def2.result.0[0], FieldType::Uint64);
246
247
248                        let def3 = [<$func _with_context_ret_res>].into_job_definition();
249                        assert_eq!(
250                            def3.metadata.name.0.0,
251                            format!("blueprint_tangle_extra::metadata::job_definition::tests::{}", stringify!([<$func _with_context_ret_res>])).as_bytes(),
252                        );
253                        assert_eq!(def3.params.0.len(), count!($($args),*));
254                        assert_eq!(def3.result.0.len(), 1);
255                        assert!(def3.params.0.iter().all(|ty| ty.clone() == FieldType::Uint64));
256                        assert_eq!(def3.result.0[0], FieldType::Uint64);
257
258                        let def4 = [<$func _with_context_ret_opt>].into_job_definition();
259                        assert_eq!(
260                            def4.metadata.name.0.0,
261                            format!("blueprint_tangle_extra::metadata::job_definition::tests::{}", stringify!([<$func _with_context_ret_opt>])).as_bytes(),
262                        );
263                        assert_eq!(def4.params.0.len(), count!($($args),*));
264                        assert_eq!(def4.result.0.len(), 1);
265                        assert!(def4.params.0.iter().all(|ty| ty.clone() == FieldType::Uint64));
266                        assert_eq!(def4.result.0[0], FieldType::Uint64);
267
268                    }
269                }
270            )*
271        }
272    }
273
274    definition_tests!(
275        xsquare(TangleArg(x): TangleArg<u64>);
276        xsquare2(TangleArgs2(x, y): TangleArgs2<u64, u64>);
277        xsquare3(TangleArgs3(x, y, z): TangleArgs3<u64, u64, u64>);
278        xsquare4(TangleArgs4(x, y, z, w): TangleArgs4<u64, u64, u64, u64>);
279        xsquare5(TangleArgs5(x, y, z, w, v): TangleArgs5<u64, u64, u64, u64, u64>);
280        xsquare6(TangleArgs6(x, y, z, w, v, u): TangleArgs6<u64, u64, u64, u64, u64, u64>);
281        xsquare7(TangleArgs7(x, y, z, w, v, u, t): TangleArgs7<u64, u64, u64, u64, u64, u64, u64>);
282        xsquare8(TangleArgs8(x, y, z, w, v, u, t, s): TangleArgs8<u64, u64, u64, u64, u64, u64, u64, u64>);
283        xsquare9(TangleArgs9(x, y, z, w, v, u, t, s, r): TangleArgs9<u64, u64, u64, u64, u64, u64, u64, u64, u64>);
284        xsquare10(TangleArgs10(x, y, z, w, v, u, t, s, r, q): TangleArgs10<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
285        xsquare11(TangleArgs11(x, y, z, w, v, u, t, s, r, q, p): TangleArgs11<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
286        xsquare12(TangleArgs12(x, y, z, w, v, u, t, s, r, q, p, o): TangleArgs12<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
287        xsquare13(TangleArgs13(x, y, z, w, v, u, t, s, r, q, p, o, n): TangleArgs13<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
288        xsquare14(TangleArgs14(x, y, z, w, v, u, t, s, r, q, p, o, n, m): TangleArgs14<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
289        xsquare15(TangleArgs15(x, y, z, w, v, u, t, s, r, q, p, o, n, m, l): TangleArgs15<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
290        xsquare16(TangleArgs16(x, y, z, w, v, u, t, s, r, q, p, o, n, m, l, k): TangleArgs16<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>);
291    );
292}