iota_graphql_rpc/
functional_group.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use async_graphql::*;
8use once_cell::sync::Lazy;
9use serde::{Deserialize, Serialize};
10use serde_json as json;
11
12/// Groups of features served by the RPC service.  The GraphQL Service can be
13/// configured to enable or disable these features.
14#[derive(Enum, Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
15#[serde(rename_all = "kebab-case")]
16#[graphql(name = "Feature")]
17pub(crate) enum FunctionalGroup {
18    /// Statistics about how the network was running (TPS, top packages, APY,
19    /// etc)
20    Analytics,
21
22    /// Coin metadata, per-address coin and balance information.
23    Coins,
24
25    /// Querying an object's dynamic fields.
26    DynamicFields,
27
28    /// Transaction and Event subscriptions.
29    Subscriptions,
30
31    /// Aspects that affect the running of the system that are managed by the
32    /// validators either directly, or through system transactions.
33    SystemState,
34}
35
36impl FunctionalGroup {
37    /// Name that the group is referred to by in configuration and responses on
38    /// the GraphQL API. Not a suitable `Display` implementation because it
39    /// enquotes the representation.
40    pub(crate) fn name(&self) -> String {
41        json::ser::to_string(self).expect("Serializing `FunctionalGroup` cannot fail.")
42    }
43
44    /// List of all functional groups
45    pub(crate) fn all() -> &'static [FunctionalGroup] {
46        use FunctionalGroup as G;
47        static ALL: &[FunctionalGroup] = &[
48            G::Analytics,
49            G::Coins,
50            G::DynamicFields,
51            G::Subscriptions,
52            G::SystemState,
53        ];
54        ALL
55    }
56}
57
58/// Mapping from type and field name in the schema to the functional group it
59/// belongs to.
60fn functional_groups() -> &'static BTreeMap<(&'static str, &'static str), FunctionalGroup> {
61    // TODO: Introduce a macro to declare the functional group for a field and/or
62    // type on the appropriate type, field, or function, instead of here.  This
63    // may also be able to set the graphql `visible` attribute to control schema
64    // visibility by functional groups.
65
66    use FunctionalGroup as G;
67    static GROUPS: Lazy<BTreeMap<(&str, &str), FunctionalGroup>> = Lazy::new(|| {
68        BTreeMap::from_iter([
69            (("Address", "balance"), G::Coins),
70            (("Address", "balances"), G::Coins),
71            (("Address", "coins"), G::Coins),
72            (("Checkpoint", "addressMetrics"), G::Analytics),
73            (("Checkpoint", "networkTotalTransactions"), G::Analytics),
74            (("Epoch", "protocolConfigs"), G::SystemState),
75            (("Epoch", "referenceGasPrice"), G::SystemState),
76            (("Epoch", "validatorSet"), G::SystemState),
77            (("Object", "balance"), G::Coins),
78            (("Object", "balances"), G::Coins),
79            (("Object", "coins"), G::Coins),
80            (("Object", "dynamicField"), G::DynamicFields),
81            (("Object", "dynamicObjectField"), G::DynamicFields),
82            (("Object", "dynamicFields"), G::DynamicFields),
83            (("Owner", "balance"), G::Coins),
84            (("Owner", "balances"), G::Coins),
85            (("Owner", "coins"), G::Coins),
86            (("Owner", "dynamicField"), G::DynamicFields),
87            (("Owner", "dynamicObjectField"), G::DynamicFields),
88            (("Owner", "dynamicFields"), G::DynamicFields),
89            (("Query", "coinMetadata"), G::Coins),
90            (("Query", "moveCallMetrics"), G::Analytics),
91            (("Query", "networkMetrics"), G::Analytics),
92            (("Query", "protocolConfig"), G::SystemState),
93            (("Subscription", "events"), G::Subscriptions),
94            (("Subscription", "transactions"), G::Subscriptions),
95            (("SystemStateSummary", "safeMode"), G::SystemState),
96            (("SystemStateSummary", "storageFund"), G::SystemState),
97            (("SystemStateSummary", "systemParameters"), G::SystemState),
98            (("SystemStateSummary", "systemStateVersion"), G::SystemState),
99        ])
100    });
101
102    Lazy::force(&GROUPS)
103}
104
105/// Map a type and field name to a functional group.  If an explicit group does
106/// not exist for the field, then it is assumed to be a "core" feature.
107pub(crate) fn functional_group(type_: &str, field: &str) -> Option<FunctionalGroup> {
108    functional_groups().get(&(type_, field)).copied()
109}
110
111#[cfg(test)]
112mod tests {
113    use std::collections::BTreeSet;
114
115    use async_graphql::{OutputType, registry::Registry};
116
117    use super::*;
118    use crate::types::query::Query;
119
120    #[test]
121    /// Makes sure all the functional groups correspond to real elements of the
122    /// schema unless they are explicitly recorded as unimplemented.
123    /// Complementarily, makes sure that fields marked as unimplemented
124    /// don't appear in the set of unimplemented fields.
125    fn test_groups_match_schema() {
126        let mut registry = Registry::default();
127        Query::create_type_info(&mut registry);
128
129        let unimplemented = BTreeSet::from_iter([
130            ("Checkpoint", "addressMetrics"),
131            ("Epoch", "protocolConfig"),
132            ("Query", "moveCallMetrics"),
133            ("Query", "networkMetrics"),
134            ("Subscription", "events"),
135            ("Subscription", "transactions"),
136        ]);
137
138        for (type_, field) in &unimplemented {
139            let Some(meta_type) = registry.concrete_type_by_name(type_) else {
140                continue;
141            };
142
143            let Some(_) = meta_type.field_by_name(field) else {
144                continue;
145            };
146
147            panic!(
148                "Field '{type_}.{field}' is marked as unimplemented in this test, but it's in the \
149                 schema.  Fix this by removing it from the `unimplemented` set."
150            );
151        }
152
153        for (type_, field) in functional_groups().keys() {
154            if unimplemented.contains(&(type_, field)) {
155                continue;
156            }
157
158            let Some(meta_type) = registry.concrete_type_by_name(type_) else {
159                panic!("Type '{type_}' from functional group configs does not appear in schema.");
160            };
161
162            let Some(_) = meta_type.field_by_name(field) else {
163                panic!(
164                    "Field '{type_}.{field}' from functional group configs does not appear in \
165                     schema."
166                );
167            };
168        }
169    }
170}