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