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