Skip to main content

fraiseql_core/schema/compiled/
schema_lookup.rs

1//! Lookup and indexing methods for [`CompiledSchema`].
2//!
3//! All `find_*` methods, `build_indexes`, `display_name`, and `operation_count`.
4
5use super::{
6    directive::DirectiveDefinition, mutation::MutationDefinition, query::QueryDefinition,
7    schema::CompiledSchema,
8};
9use crate::schema::{
10    config_types::NamingConvention,
11    graphql_type_defs::{
12        EnumDefinition, InputObjectDefinition, InterfaceDefinition, TypeDefinition, UnionDefinition,
13    },
14    subscription_types::SubscriptionDefinition,
15};
16
17impl CompiledSchema {
18    /// Build O(1) lookup indexes for queries, mutations, and subscriptions.
19    ///
20    /// Called automatically by `from_json()`. Must be called manually after any
21    /// direct mutation of `self.queries`, `self.mutations`, or `self.subscriptions`.
22    pub fn build_indexes(&mut self) {
23        let camel = matches!(self.naming_convention, NamingConvention::CamelCase);
24
25        self.query_index = self
26            .queries
27            .iter()
28            .enumerate()
29            .flat_map(|(i, q)| {
30                let mut entries = vec![(q.name.clone(), i)];
31                if camel {
32                    let converted = crate::utils::casing::to_camel_case(&q.name);
33                    if converted != q.name {
34                        entries.push((converted, i));
35                    }
36                }
37                entries
38            })
39            .collect();
40
41        self.mutation_index = self
42            .mutations
43            .iter()
44            .enumerate()
45            .flat_map(|(i, m)| {
46                let mut entries = vec![(m.name.clone(), i)];
47                if camel {
48                    let converted = crate::utils::casing::to_camel_case(&m.name);
49                    if converted != m.name {
50                        entries.push((converted, i));
51                    }
52                }
53                entries
54            })
55            .collect();
56
57        self.subscription_index = self
58            .subscriptions
59            .iter()
60            .enumerate()
61            .flat_map(|(i, s)| {
62                let mut entries = vec![(s.name.clone(), i)];
63                if camel {
64                    let converted = crate::utils::casing::to_camel_case(&s.name);
65                    if converted != s.name {
66                        entries.push((converted, i));
67                    }
68                }
69                entries
70            })
71            .collect();
72    }
73
74    /// Return the display name for an operation, applying the naming convention.
75    ///
76    /// When `naming_convention` is `CamelCase`, converts `snake_case` names to
77    /// `camelCase` (e.g., `create_dns_server` → `createDnsServer`).
78    /// When `Preserve`, returns the name unchanged.
79    #[must_use]
80    pub fn display_name(&self, name: &str) -> String {
81        match self.naming_convention {
82            NamingConvention::CamelCase => crate::utils::casing::to_camel_case(name),
83            NamingConvention::Preserve => name.to_string(),
84        }
85    }
86
87    /// Find a type definition by name.
88    #[must_use]
89    pub fn find_type(&self, name: &str) -> Option<&TypeDefinition> {
90        self.types.iter().find(|t| t.name == name)
91    }
92
93    /// Find an enum definition by name.
94    #[must_use]
95    pub fn find_enum(&self, name: &str) -> Option<&EnumDefinition> {
96        self.enums.iter().find(|e| e.name == name)
97    }
98
99    /// Find an input object definition by name.
100    #[must_use]
101    pub fn find_input_type(&self, name: &str) -> Option<&InputObjectDefinition> {
102        self.input_types.iter().find(|i| i.name == name)
103    }
104
105    /// Find an interface definition by name.
106    #[must_use]
107    pub fn find_interface(&self, name: &str) -> Option<&InterfaceDefinition> {
108        self.interfaces.iter().find(|i| i.name == name)
109    }
110
111    /// Find all types that implement a given interface.
112    #[must_use]
113    pub fn find_implementors(&self, interface_name: &str) -> Vec<&TypeDefinition> {
114        self.types
115            .iter()
116            .filter(|t| t.implements.contains(&interface_name.to_string()))
117            .collect()
118    }
119
120    /// Find a union definition by name.
121    #[must_use]
122    pub fn find_union(&self, name: &str) -> Option<&UnionDefinition> {
123        self.unions.iter().find(|u| u.name == name)
124    }
125
126    /// Find a query definition by name.
127    ///
128    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
129    /// scan for schemas built directly in tests without calling `build_indexes()`.
130    ///
131    /// If the exact name is not found, retries with `to_snake_case(name)` to
132    /// handle camelCase → `snake_case` normalization (e.g. `dnsServers` →
133    /// `dns_servers`). This supports schemas compiled before the SDK camelCase
134    /// migration.
135    #[must_use]
136    pub fn find_query(&self, name: &str) -> Option<&QueryDefinition> {
137        if self.query_index.is_empty() && !self.queries.is_empty() {
138            self.queries.iter().find(|q| q.name == name).or_else(|| {
139                let snake = crate::utils::casing::to_snake_case(name);
140                self.queries.iter().find(|q| q.name == snake)
141            })
142        } else {
143            self.query_index
144                .get(name)
145                .or_else(|| self.query_index.get(&crate::utils::casing::to_snake_case(name)))
146                .map(|&i| &self.queries[i])
147        }
148    }
149
150    /// Find a mutation definition by name.
151    ///
152    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
153    /// scan for schemas built directly in tests without calling `build_indexes()`.
154    ///
155    /// If the exact name is not found, retries with `to_snake_case(name)` to
156    /// handle camelCase → `snake_case` normalization. This supports schemas
157    /// compiled before the SDK camelCase migration.
158    #[must_use]
159    pub fn find_mutation(&self, name: &str) -> Option<&MutationDefinition> {
160        if self.mutation_index.is_empty() && !self.mutations.is_empty() {
161            self.mutations.iter().find(|m| m.name == name).or_else(|| {
162                let snake = crate::utils::casing::to_snake_case(name);
163                self.mutations.iter().find(|m| m.name == snake)
164            })
165        } else {
166            self.mutation_index
167                .get(name)
168                .or_else(|| self.mutation_index.get(&crate::utils::casing::to_snake_case(name)))
169                .map(|&i| &self.mutations[i])
170        }
171    }
172
173    /// Find a subscription definition by name.
174    ///
175    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
176    /// scan for schemas built directly in tests without calling `build_indexes()`.
177    ///
178    /// If the exact name is not found, retries with `to_snake_case(name)` to
179    /// handle camelCase → `snake_case` normalization. This supports schemas
180    /// compiled before the SDK camelCase migration.
181    #[must_use]
182    pub fn find_subscription(&self, name: &str) -> Option<&SubscriptionDefinition> {
183        if self.subscription_index.is_empty() && !self.subscriptions.is_empty() {
184            self.subscriptions.iter().find(|s| s.name == name).or_else(|| {
185                let snake = crate::utils::casing::to_snake_case(name);
186                self.subscriptions.iter().find(|s| s.name == snake)
187            })
188        } else {
189            self.subscription_index
190                .get(name)
191                .or_else(|| self.subscription_index.get(&crate::utils::casing::to_snake_case(name)))
192                .map(|&i| &self.subscriptions[i])
193        }
194    }
195
196    /// Find a custom directive definition by name.
197    #[must_use]
198    pub fn find_directive(&self, name: &str) -> Option<&DirectiveDefinition> {
199        self.directives.iter().find(|d| d.name == name)
200    }
201
202    /// Get total number of operations (queries + mutations + subscriptions).
203    #[must_use]
204    pub const fn operation_count(&self) -> usize {
205        self.queries.len() + self.mutations.len() + self.subscriptions.len()
206    }
207}