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