Skip to main content

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