graphql_composition/
subgraphs.rs

1mod definitions;
2mod directives;
3mod enums;
4mod extensions;
5mod field_types;
6mod fields;
7mod ids;
8mod keys;
9mod linked_schemas;
10mod strings;
11mod top;
12mod unions;
13mod view;
14mod walker;
15
16pub(crate) use self::{
17    definitions::{Definition, DefinitionKind, DefinitionWalker},
18    directives::*,
19    extensions::*,
20    field_types::*,
21    fields::*,
22    ids::*,
23    keys::*,
24    linked_schemas::*,
25    strings::{StringId, StringWalker},
26    top::*,
27    view::View,
28    walker::Walker,
29};
30
31use crate::VecExt;
32use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
33
34/// A set of subgraphs to be composed.
35pub struct Subgraphs {
36    pub(super) strings: strings::Strings,
37    subgraphs: Vec<Subgraph>,
38    definitions: definitions::Definitions,
39    directives: directives::Directives,
40    enums: enums::Enums,
41    fields: fields::Fields,
42    field_types: field_types::FieldTypes,
43    keys: keys::Keys,
44    unions: unions::Unions,
45    linked_schemas: linked_schemas::LinkedSchemas,
46
47    ingestion_diagnostics: crate::Diagnostics,
48
49    extensions: Vec<ExtensionRecord>,
50
51    // Secondary indexes.
52
53    // We want a BTreeMap because we need range queries. The name comes first, then the subgraph,
54    // because we want to know which definitions have the same name but live in different
55    // subgraphs.
56    //
57    // (definition name, subgraph_id) -> definition id
58    definition_names: BTreeMap<(StringId, SubgraphId), DefinitionId>,
59}
60
61impl Default for Subgraphs {
62    fn default() -> Self {
63        let mut strings = strings::Strings::default();
64        BUILTIN_SCALARS.into_iter().for_each(|scalar| {
65            strings.intern(scalar);
66        });
67
68        Self {
69            strings,
70            subgraphs: Default::default(),
71            definitions: Default::default(),
72            directives: Default::default(),
73            enums: Default::default(),
74            fields: Default::default(),
75            field_types: Default::default(),
76            keys: Default::default(),
77            unions: Default::default(),
78            ingestion_diagnostics: Default::default(),
79            definition_names: Default::default(),
80            linked_schemas: Default::default(),
81            extensions: Vec::new(),
82        }
83    }
84}
85
86const BUILTIN_SCALARS: [&str; 5] = ["ID", "String", "Boolean", "Int", "Float"];
87
88/// returned when a subgraph cannot be ingested
89#[derive(Debug)]
90pub struct IngestError {
91    error: cynic_parser::Error,
92    report: String,
93}
94
95impl std::error::Error for IngestError {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        self.error.source()
98    }
99}
100
101impl std::fmt::Display for IngestError {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        if f.alternate() {
104            return self.report.fmt(f);
105        }
106        std::fmt::Display::fmt(&self.error, f)
107    }
108}
109
110impl Subgraphs {
111    /// Add a subgraph to compose.
112    pub fn ingest(&mut self, subgraph_schema: &cynic_parser::TypeSystemDocument, name: &str, url: Option<&str>) {
113        crate::ingest_subgraph::ingest_subgraph(subgraph_schema, name, url, self);
114    }
115
116    /// Add a subgraph to compose.
117    pub fn ingest_str(&mut self, subgraph_schema: &str, name: &str, url: Option<&str>) -> Result<(), IngestError> {
118        let subgraph_schema =
119            cynic_parser::parse_type_system_document(subgraph_schema).map_err(|error| IngestError {
120                report: error.to_report(subgraph_schema).to_string(),
121                error,
122            })?;
123        crate::ingest_subgraph::ingest_subgraph(&subgraph_schema, name, url, self);
124        Ok(())
125    }
126
127    /// Add Grafbase extension schemas to compose. The extensions are referenced in subgraphs through their `url` in an `@link` directive.
128    ///
129    /// It is safe to add the same extension (same name) multiple times. It will only be an error if the urls are not compatible. Different remote versions are compatible between each other, but different paths are not compatible, and local paths are not compatible with remote urls.
130    #[cfg(feature = "grafbase-extensions")]
131    pub fn ingest_loaded_extensions(&mut self, extensions: impl IntoIterator<Item = crate::LoadedExtension>) {
132        self.extensions
133            .extend(extensions.into_iter().map(|ext| ExtensionRecord {
134                url: self.strings.intern(ext.url),
135                name: self.strings.intern(ext.name),
136            }));
137    }
138
139    /// Checks whether any subgraphs have been ingested
140    pub fn is_empty(&self) -> bool {
141        self.subgraphs.is_empty()
142    }
143
144    /// Iterate over groups of definitions to compose. The definitions are grouped by name. The
145    /// argument is a closure that receives each group as argument. The order of iteration is
146    /// deterministic but unspecified.
147    pub(crate) fn iter_definition_groups<'a>(&'a self, mut compose_fn: impl FnMut(&[DefinitionWalker<'a>])) {
148        let mut key = None;
149        let mut buf = Vec::new();
150
151        for ((name, subgraph), definition) in &self.definition_names {
152            if Some(name) != key {
153                // New key. Compose previous key and start new group.
154                compose_fn(&buf);
155                buf.clear();
156                key = Some(name);
157            }
158
159            // Fill buf, except if we are dealing with a root object type.
160
161            if self.is_root_type(*subgraph, *definition) {
162                continue; // handled separately
163            }
164
165            buf.push(self.walk(*definition));
166        }
167
168        compose_fn(&buf)
169    }
170
171    pub(crate) fn push_ingestion_diagnostic(&mut self, subgraph: SubgraphId, message: String) {
172        self.ingestion_diagnostics
173            .push_fatal(format!("[{}]: {message}", self.walk_subgraph(subgraph).name().as_str()));
174    }
175
176    pub(crate) fn push_ingestion_warning(&mut self, subgraph: SubgraphId, message: String) {
177        self.ingestion_diagnostics
178            .push_warning(format!("[{}]: {message}", self.walk_subgraph(subgraph).name().as_str()));
179    }
180
181    pub(crate) fn walk<Id>(&self, id: Id) -> Walker<'_, Id> {
182        Walker { id, subgraphs: self }
183    }
184
185    /// Iterates all builtin scalars _that are in use in at least one subgraph_.
186    pub(crate) fn iter_builtin_scalars(&self) -> impl Iterator<Item = StringWalker<'_>> + '_ {
187        BUILTIN_SCALARS
188            .into_iter()
189            .map(|name| self.strings.lookup(name).expect("all built in scalars to be interned"))
190            .map(|string| self.walk(string))
191    }
192
193    pub(crate) fn emit_ingestion_diagnostics(&self, diagnostics: &mut crate::Diagnostics) {
194        diagnostics.clone_all_from(&self.ingestion_diagnostics);
195    }
196}