sui_gql_client/queries/
object_args.rs

1//! For requesting [`ObjectArg`]s from the server. Defines [`object_args!`](crate::object_args!).
2use af_sui_types::{ObjectArg, ObjectId, Version};
3use bimap::BiMap;
4use futures::TryStreamExt as _;
5use itertools::Itertools as _;
6use sui_gql_schema::scalars;
7
8use super::fragments::ObjectFilterV2;
9use super::objects_flat::Variables;
10use crate::{GraphQlClient, GraphQlErrors, GraphQlResponseExt as _, schema};
11
12type Query = super::objects_flat::Query<Object>;
13
14// WARN: can't call this `Result`, otherwise it will mess with the code generated by the derive
15// macros in this module.
16type Res<T, C> = std::result::Result<T, Error<<C as GraphQlClient>::Error>>;
17
18#[derive(thiserror::Error, Debug)]
19pub enum Error<T> {
20    #[error(transparent)]
21    Client(T),
22    #[error(transparent)]
23    Server(#[from] GraphQlErrors),
24    #[error("No data in object args query response")]
25    NoData,
26    #[error("Response missing object args for pairs: {0:?}")]
27    MissingNamedArgs(Vec<(String, ObjectId)>),
28}
29
30/// Turn a bijective map of names and object ids into one of names and object args.
31///
32/// Fails if the query response does not have the necessary data for the input map.
33pub(super) async fn query<C: GraphQlClient>(
34    client: &C,
35    mut names: BiMap<String, ObjectId>,
36    page_size: Option<u32>,
37) -> Res<BiMap<String, ObjectArg>, C> {
38    let object_ids = names.right_values().cloned().collect_vec();
39    let filter = ObjectFilterV2 {
40        object_ids: Some(&object_ids),
41        type_: None,
42        owner: None,
43    };
44    let vars = Variables {
45        filter: Some(filter),
46        after: None,
47        first: page_size.map(|n| n as i32),
48    };
49
50    let mut stream = std::pin::pin!(super::stream::forward(client, vars, request));
51
52    let mut result = BiMap::new();
53
54    while let Some(arg) = stream.try_next().await? {
55        if let Some((key, _)) = names.remove_by_right(arg.id_borrowed()) {
56            result.insert(key, arg);
57        }
58    }
59
60    if !names.is_empty() {
61        return Err(Error::MissingNamedArgs(names.into_iter().collect()));
62    }
63
64    Ok(result)
65}
66
67async fn request<C: GraphQlClient>(
68    client: &C,
69    vars: Variables<'_>,
70) -> Res<super::stream::Page<impl Iterator<Item = Res<ObjectArg, C>> + 'static + use<C>>, C> {
71    let objects = client
72        .query::<Query, _>(vars)
73        .await
74        .map_err(Error::Client)?
75        .try_into_data()?
76        .ok_or(Error::NoData)?
77        .objects;
78
79    Ok(super::stream::Page {
80        info: objects.page_info.into(),
81        data: objects
82            .nodes
83            .into_iter()
84            .filter_map(Object::object_arg)
85            .map(Ok),
86    })
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used)]
91#[test]
92fn gql_output() {
93    use cynic::QueryBuilder as _;
94    let vars = Variables {
95        filter: None,
96        first: None,
97        after: None,
98    };
99    let operation = Query::build(vars);
100    insta::assert_snapshot!(operation.query, @r###"
101    query Query($filter: ObjectFilter, $after: String, $first: Int) {
102      objects(filter: $filter, first: $first, after: $after) {
103        nodes {
104          address
105          version
106          digest
107          owner {
108            __typename
109            ... on Immutable {
110              _
111            }
112            ... on Shared {
113              __typename
114              initialSharedVersion
115            }
116            ... on Parent {
117              __typename
118            }
119            ... on AddressOwner {
120              __typename
121            }
122          }
123        }
124        pageInfo {
125          hasNextPage
126          endCursor
127        }
128      }
129    }
130    "###);
131}
132
133// =============================================================================
134//  Macro helper
135// =============================================================================
136
137/// Query [ObjectArg]s and assign them to variables. Optionally, set the page size.
138///
139/// This will panic if the user specifies two different identifiers mapping to the same [ObjectId].
140///
141/// The `mut` keyword here means we're requesting a mutable [ObjectArg::SharedObject].
142///
143/// # Example
144/// ```no_run
145/// # use color_eyre::Result;
146/// # use sui_gql_client::object_args;
147/// # use sui_gql_client::reqwest::ReqwestClient;
148/// # const SUI_GRAPHQL_SERVER_URL: &str = "https://sui-testnet.mystenlabs.com/graphql";
149/// # tokio_test::block_on(async {
150/// let client = ReqwestClient::new(
151///     reqwest::Client::default(),
152///     SUI_GRAPHQL_SERVER_URL.to_owned(),
153/// );
154/// object_args!({
155///     mut clearing_house: "0xe4a1c0bfc53a7c2941a433a9a681c942327278b402878e0c45280eecd098c3d1".parse()?,
156///     registry: "0x400e84251a6ce2192f69c1aa775d68bab7690e059578317bf9e844d40e07e04d".parse()?,
157/// } with { &client } paged by 10);
158/// # println!("{clearing_house:?}");
159/// # println!("{registry:?}");
160/// # Ok::<_, color_eyre::eyre::Error>(())
161/// # });
162/// ```
163#[macro_export]
164macro_rules! object_args {
165    (
166        {$($tt:tt)*}
167        with { $client:expr_2021 } $(paged by $page_size:expr_2021)?
168    ) => {
169        $crate::object_args!(@Names $($tt)*);
170        {
171            use $crate::queries::GraphQlClientExt as _;
172            let mut names = $crate::queries::BiMap::new();
173            $crate::object_args! { @Map names $($tt)* }
174            let mut oargs = $crate::queries::GraphQlClientExt::object_args(
175                $client,
176                names,
177                $crate::object_args!(@PageSize $($page_size)?)
178            ).await?;
179            $crate::object_args! { @Result oargs $($tt)* }
180        }
181    };
182
183    (@Names mut $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
184        $crate::object_args!(@Names $name: $_ $(, $($rest)*)?)
185    };
186
187    (@Names $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
188        let $name;
189        $crate::object_args!{ @Names $($($rest)*)? }
190    };
191
192    (@Names ) => {};
193
194    (@Map $map:ident mut $name:ident: $object_id:expr_2021 $(, $($rest:tt)*)?) => {
195        $crate::object_args! { @Map $map $name: $object_id $(, $($rest)*)? }
196    };
197
198    (@Map $map:ident $name:ident: $object_id:expr_2021 $(, $($rest:tt)*)?) => {
199        $map.insert(stringify!($name).to_owned(), $object_id);
200        $crate::object_args!{ @Map $map $($($rest)*)? }
201    };
202
203    (@Map $map:ident) => {};
204
205    (@Result $oargs:ident mut $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
206        let mut arg = $oargs
207            .remove_by_left(stringify!($name))
208            .expect("request_named_object_args should fail if any names are missing")
209            .1;
210        arg.set_mutable(true)?;
211        $name = arg;
212        $crate::object_args! {@Result $oargs $($($rest)*)?}
213    };
214
215    (@Result $oargs:ident $name:ident: $_:expr_2021 $(, $($rest:tt)*)?) => {
216        $name = $oargs
217            .remove_by_left(stringify!($name))
218            .expect("request_named_object_args should fail if any names are missing")
219            .1;
220        $crate::object_args! { @Result $oargs $($($rest)*)? }
221    };
222
223    (@Result $oargs:ident ) => {
224    };
225
226    (@PageSize $page_size:expr_2021) => { Some($page_size) };
227    (@PageSize) => { None };
228}
229
230// =============================================================================
231//  Inner query fragments
232// =============================================================================
233
234#[derive(cynic::QueryFragment, Debug)]
235struct Object {
236    #[cynic(rename = "address")]
237    object_id: ObjectId,
238    version: Version,
239    digest: Option<scalars::Digest>,
240    owner: Option<ObjectOwner>,
241}
242
243impl Object {
244    /// Return the [ObjectArg] or none if missing data.
245    ///
246    /// For shared objects, `mutable` is set as `false`. Use [ObjectArg::set_mutable] if needed.
247    fn object_arg(self) -> Option<ObjectArg> {
248        let Self {
249            object_id,
250            version,
251            digest,
252            owner: Some(owner),
253        } = self
254        else {
255            return None;
256        };
257
258        build_object_arg_default(object_id, version, owner, digest)
259    }
260}
261
262fn build_object_arg_default(
263    id: ObjectId,
264    version: Version,
265    owner: ObjectOwner,
266    digest: Option<scalars::Digest>,
267) -> Option<ObjectArg> {
268    Some(match owner {
269        ObjectOwner::Immutable(_) | ObjectOwner::Parent(_) | ObjectOwner::AddressOwner(_) => {
270            ObjectArg::ImmOrOwnedObject((id, version, digest?.0.into()))
271        }
272        ObjectOwner::Shared(Shared {
273            initial_shared_version,
274            ..
275        }) => ObjectArg::SharedObject {
276            id,
277            initial_shared_version,
278            mutable: false,
279        },
280        ObjectOwner::Unknown => return None,
281    })
282}
283
284pub(super) fn build_oarg_set_mut(
285    object_id: ObjectId,
286    version: Version,
287    owner: Option<ObjectOwner>,
288    digest: Option<scalars::Digest>,
289    mutable_: bool,
290) -> Option<ObjectArg> {
291    let mut oarg = build_object_arg_default(object_id, version, owner?, digest)?;
292    if let ObjectArg::SharedObject {
293        ref mut mutable, ..
294    } = oarg
295    {
296        *mutable = mutable_;
297    }
298    Some(oarg)
299}
300
301#[derive(cynic::InlineFragments, Debug)]
302pub(super) enum ObjectOwner {
303    #[allow(dead_code)]
304    Immutable(Immutable),
305
306    Shared(Shared),
307
308    #[allow(dead_code)]
309    Parent(Parent),
310
311    #[allow(dead_code)]
312    AddressOwner(AddressOwner),
313
314    #[cynic(fallback)]
315    Unknown,
316}
317
318#[derive(cynic::QueryFragment, Debug)]
319pub(super) struct Immutable {
320    #[cynic(rename = "_")]
321    __underscore: Option<bool>,
322}
323
324#[derive(cynic::QueryFragment, Debug)]
325pub(super) struct Shared {
326    __typename: String,
327    initial_shared_version: Version,
328}
329
330#[derive(cynic::QueryFragment, Debug)]
331pub(super) struct Parent {
332    __typename: String,
333}
334
335#[derive(cynic::QueryFragment, Debug)]
336pub(super) struct AddressOwner {
337    __typename: String,
338}