meta_language/graphql_adapter/
registry.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::error::Error;
3use std::fmt;
4
5use serde_json::Value;
6
7use crate::query_plan::{QueryAggregateFunction, QueryOperation};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
11pub enum GraphQlOperationType {
12 Query,
14 Mutation,
16}
17
18impl GraphQlOperationType {
19 #[must_use]
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::Query => "query",
24 Self::Mutation => "mutation",
25 }
26 }
27
28 fn parse(value: &str) -> Option<Self> {
29 match value {
30 "query" => Some(Self::Query),
31 "mutation" => Some(Self::Mutation),
32 _ => None,
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
39pub enum GraphQlArgumentRole {
40 Filter,
42 Order,
44 Limit,
46 Offset,
48 Group,
50 MutationInput,
52}
53
54impl GraphQlArgumentRole {
55 #[must_use]
57 pub const fn as_str(self) -> &'static str {
58 match self {
59 Self::Filter => "filter",
60 Self::Order => "order",
61 Self::Limit => "limit",
62 Self::Offset => "offset",
63 Self::Group => "group",
64 Self::MutationInput => "mutation-input",
65 }
66 }
67
68 fn parse(value: &str) -> Option<Self> {
69 match value {
70 "filter" => Some(Self::Filter),
71 "order" => Some(Self::Order),
72 "limit" => Some(Self::Limit),
73 "offset" => Some(Self::Offset),
74 "group" => Some(Self::Group),
75 "mutation-input" => Some(Self::MutationInput),
76 _ => None,
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct GraphQlRootMapping {
84 pub(super) source_operation: GraphQlOperationType,
85 pub(super) source_field: String,
86 pub(super) operation: QueryOperation,
87 pub(super) resource: String,
88 pub(super) arguments: BTreeMap<String, GraphQlArgumentRole>,
89 pub(super) fields: BTreeMap<String, String>,
90 pub(super) aggregates: BTreeMap<String, QueryAggregateFunction>,
91}
92
93impl GraphQlRootMapping {
94 #[must_use]
97 pub fn new(
98 source_operation: GraphQlOperationType,
99 source_field: impl Into<String>,
100 operation: QueryOperation,
101 resource: impl Into<String>,
102 ) -> Self {
103 Self {
104 source_operation,
105 source_field: source_field.into(),
106 operation,
107 resource: resource.into(),
108 arguments: BTreeMap::new(),
109 fields: BTreeMap::new(),
110 aggregates: BTreeMap::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn with_argument(
117 mut self,
118 source_name: impl Into<String>,
119 role: GraphQlArgumentRole,
120 ) -> Self {
121 self.arguments.insert(source_name.into(), role);
122 self
123 }
124
125 #[must_use]
127 pub fn with_field(
128 mut self,
129 source_name: impl Into<String>,
130 canonical_field: impl Into<String>,
131 ) -> Self {
132 self.fields
133 .insert(source_name.into(), canonical_field.into());
134 self
135 }
136
137 #[must_use]
139 pub fn with_aggregate(
140 mut self,
141 source_name: impl Into<String>,
142 aggregate: QueryAggregateFunction,
143 ) -> Self {
144 self.aggregates.insert(source_name.into(), aggregate);
145 self
146 }
147
148 #[must_use]
150 pub const fn source_operation(&self) -> GraphQlOperationType {
151 self.source_operation
152 }
153
154 #[must_use]
156 pub fn source_field(&self) -> &str {
157 &self.source_field
158 }
159
160 #[must_use]
162 pub const fn operation(&self) -> QueryOperation {
163 self.operation
164 }
165
166 #[must_use]
168 pub fn resource(&self) -> &str {
169 &self.resource
170 }
171
172 pub(super) fn mapped_field(&self, source: &str) -> Result<String, GraphQlAdapterError> {
173 self.fields
174 .get(source)
175 .cloned()
176 .ok_or_else(|| GraphQlAdapterError::new(format!("unmapped GraphQL field {source:?}")))
177 }
178
179 pub(super) fn mapped_symbol(&self, source: &str) -> Result<String, GraphQlAdapterError> {
180 let matches = self
181 .fields
182 .iter()
183 .filter(|(name, _)| name.eq_ignore_ascii_case(source))
184 .map(|(_, canonical)| canonical)
185 .collect::<Vec<_>>();
186 match matches.as_slice() {
187 [canonical] => Ok((*canonical).clone()),
188 [] => Err(GraphQlAdapterError::new(format!(
189 "unmapped GraphQL field symbol {source:?}"
190 ))),
191 _ => Err(GraphQlAdapterError::new(format!(
192 "ambiguous GraphQL field symbol {source:?}"
193 ))),
194 }
195 }
196}
197
198#[derive(Clone, Debug, Default, PartialEq, Eq)]
201pub struct GraphQlSchemaRegistry {
202 roots: BTreeMap<(GraphQlOperationType, String), GraphQlRootMapping>,
203}
204
205impl GraphQlSchemaRegistry {
206 #[must_use]
208 pub const fn new() -> Self {
209 Self {
210 roots: BTreeMap::new(),
211 }
212 }
213
214 pub fn register_root(
216 &mut self,
217 mapping: GraphQlRootMapping,
218 ) -> Result<(), GraphQlAdapterError> {
219 validate_mapping(&mapping)?;
220 let key = (mapping.source_operation, mapping.source_field.clone());
221 if self.roots.contains_key(&key) {
222 return Err(GraphQlAdapterError::new(format!(
223 "duplicate GraphQL {} root mapping {:?}",
224 mapping.source_operation.as_str(),
225 mapping.source_field
226 )));
227 }
228 self.roots.insert(key, mapping);
229 Ok(())
230 }
231
232 pub fn from_json(value: &Value) -> Result<Self, GraphQlAdapterError> {
234 let roots = value
235 .get("roots")
236 .and_then(Value::as_array)
237 .ok_or_else(|| GraphQlAdapterError::new("registry.roots must be an array"))?;
238 let mut registry = Self::new();
239 for root in roots {
240 let source_operation = required_string(root, "sourceOperation")?;
241 let source_operation =
242 GraphQlOperationType::parse(source_operation).ok_or_else(|| {
243 GraphQlAdapterError::new(format!(
244 "unsupported GraphQL operation mapping {source_operation:?}"
245 ))
246 })?;
247 let operation_label = required_string(root, "operation")?;
248 let operation = QueryOperation::parse(operation_label).ok_or_else(|| {
249 GraphQlAdapterError::new(format!(
250 "unsupported canonical operation {operation_label:?}"
251 ))
252 })?;
253 let mut mapping = GraphQlRootMapping::new(
254 source_operation,
255 required_string(root, "sourceField")?,
256 operation,
257 required_string(root, "resource")?,
258 );
259 for (name, role) in optional_object(root, "arguments")? {
260 let role = role.as_str().ok_or_else(|| {
261 GraphQlAdapterError::new("GraphQL argument roles must be strings")
262 })?;
263 let role = GraphQlArgumentRole::parse(role).ok_or_else(|| {
264 GraphQlAdapterError::new(format!("unsupported argument role {role:?}"))
265 })?;
266 mapping = mapping.with_argument(name, role);
267 }
268 for (name, canonical) in optional_object(root, "fields")? {
269 let canonical = canonical.as_str().ok_or_else(|| {
270 GraphQlAdapterError::new("canonical GraphQL fields must be strings")
271 })?;
272 mapping = mapping.with_field(name, canonical);
273 }
274 for (name, aggregate) in optional_object(root, "aggregates")? {
275 let aggregate = aggregate.as_str().ok_or_else(|| {
276 GraphQlAdapterError::new("GraphQL aggregate mappings must be strings")
277 })?;
278 let aggregate = QueryAggregateFunction::parse(aggregate).ok_or_else(|| {
279 GraphQlAdapterError::new(format!("unsupported aggregate {aggregate:?}"))
280 })?;
281 mapping = mapping.with_aggregate(name, aggregate);
282 }
283 registry.register_root(mapping)?;
284 }
285 Ok(registry)
286 }
287
288 pub(super) fn root(
289 &self,
290 operation: GraphQlOperationType,
291 source_field: &str,
292 ) -> Result<&GraphQlRootMapping, GraphQlAdapterError> {
293 self.roots
294 .get(&(operation, source_field.to_string()))
295 .ok_or_else(|| {
296 GraphQlAdapterError::new(format!(
297 "unmapped GraphQL {} root field {source_field:?}",
298 operation.as_str()
299 ))
300 })
301 }
302}
303
304#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct GraphQlAdapterError {
307 message: String,
308}
309
310impl GraphQlAdapterError {
311 pub(super) fn new(message: impl Into<String>) -> Self {
312 Self {
313 message: message.into(),
314 }
315 }
316}
317
318impl fmt::Display for GraphQlAdapterError {
319 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
320 formatter.write_str(&self.message)
321 }
322}
323
324impl Error for GraphQlAdapterError {}
325
326fn validate_mapping(mapping: &GraphQlRootMapping) -> Result<(), GraphQlAdapterError> {
327 if mapping.source_field.is_empty() || mapping.resource.is_empty() {
328 return Err(GraphQlAdapterError::new(
329 "GraphQL root and canonical resource names must not be empty",
330 ));
331 }
332 if mapping.arguments.keys().any(String::is_empty)
333 || mapping.fields.keys().any(String::is_empty)
334 || mapping.fields.values().any(String::is_empty)
335 || mapping.aggregates.keys().any(String::is_empty)
336 {
337 return Err(GraphQlAdapterError::new(
338 "GraphQL mapping names and canonical fields must not be empty",
339 ));
340 }
341 if mapping.source_operation == GraphQlOperationType::Query
342 && mapping.operation != QueryOperation::Select
343 {
344 return Err(GraphQlAdapterError::new(
345 "GraphQL query roots must map to the select operation",
346 ));
347 }
348 if mapping.source_operation == GraphQlOperationType::Mutation
349 && mapping.operation == QueryOperation::Select
350 {
351 return Err(GraphQlAdapterError::new(
352 "GraphQL mutation roots must map to insert, update, or delete",
353 ));
354 }
355 let unique_roles = mapping.arguments.values().copied().collect::<BTreeSet<_>>();
356 if unique_roles.len() != mapping.arguments.len() {
357 return Err(GraphQlAdapterError::new(
358 "a GraphQL root mapping cannot assign the same semantic role twice",
359 ));
360 }
361 let mut case_insensitive_fields = BTreeSet::new();
362 if mapping
363 .fields
364 .keys()
365 .any(|name| !case_insensitive_fields.insert(name.to_ascii_lowercase()))
366 {
367 return Err(GraphQlAdapterError::new(
368 "GraphQL field symbol mappings must not be case-ambiguous",
369 ));
370 }
371 if mapping
372 .aggregates
373 .keys()
374 .any(|name| mapping.fields.contains_key(name))
375 {
376 return Err(GraphQlAdapterError::new(
377 "a GraphQL selection cannot map to both a field and an aggregate",
378 ));
379 }
380 Ok(())
381}
382
383fn required_string<'a>(value: &'a Value, key: &str) -> Result<&'a str, GraphQlAdapterError> {
384 value
385 .get(key)
386 .and_then(Value::as_str)
387 .ok_or_else(|| GraphQlAdapterError::new(format!("registry root {key:?} must be a string")))
388}
389
390fn optional_object<'a>(
391 value: &'a Value,
392 key: &str,
393) -> Result<Vec<(&'a str, &'a Value)>, GraphQlAdapterError> {
394 let Some(value) = value.get(key) else {
395 return Ok(Vec::new());
396 };
397 let object = value.as_object().ok_or_else(|| {
398 GraphQlAdapterError::new(format!("registry root {key:?} must be an object"))
399 })?;
400 Ok(object
401 .iter()
402 .map(|(name, value)| (name.as_str(), value))
403 .collect())
404}