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