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