use super::subgraph::Subgraph;
use crate::{
diagnostics::{lsp_range_from_line_column_range, reformat_satisfiability_error},
goto_definition::goto_definition_at_position,
hover::get_hover_at_position,
};
use apollo_federation_types::{
composition, composition::Issue, config::SchemaSource, javascript::SubgraphDefinition,
};
use std::{collections::HashMap, sync::Arc};
#[derive(Default, Debug, Clone)]
pub(crate) struct KnownSubgraphs {
pub(crate) root_uri: String,
pub(crate) by_name: HashMap<String, SchemaSource>,
pub(crate) by_uri: HashMap<lsp::Url, String>,
}
impl KnownSubgraphs {
#[cfg(any(not(feature = "wasm"), test))]
pub(crate) fn uri_for_name(&self, name: &str) -> Option<lsp::Url> {
let source = self.by_name.get(name)?;
match source {
SchemaSource::File { file } => {
if file.is_relative() {
let uri =
lsp::Url::from_directory_path(&self.root_uri).expect("Failed to parse URL");
Some(
uri.join(file.to_str().expect("Failed to convert path to string"))
.expect("Failed to join URL"),
)
} else {
Some(lsp::Url::from_file_path(file).expect("Failed to convert path to URL"))
}
}
_ => None,
}
}
#[cfg(all(feature = "wasm", not(test)))]
pub(crate) fn uri_for_name(&self, _: &str) -> Option<lsp::Url> {
None
}
}
#[derive(Debug, Default)]
pub struct Supergraph {
subgraphs_by_uri: HashMap<lsp::Url, Arc<Subgraph>>,
subgraphs_by_name: HashMap<String, Arc<Subgraph>>,
pub(crate) known_subgraphs: KnownSubgraphs,
}
type DiagnosticsBySubgraph = HashMap<lsp::Url, (Vec<lsp::Diagnostic>, i32)>;
impl Supergraph {
pub fn new(known_subgraphs: KnownSubgraphs) -> Supergraph {
Supergraph {
subgraphs_by_uri: HashMap::new(),
subgraphs_by_name: HashMap::new(),
known_subgraphs,
}
}
pub(crate) fn insert(&mut self, subgraph: Subgraph) {
let subgraph = Arc::new(subgraph);
if self.subgraphs_by_name.contains_key(&subgraph.name) {
panic!("Subgraph `{}` already exists", subgraph.name);
}
self.subgraphs_by_name
.insert(subgraph.name.clone(), subgraph.clone());
self.subgraphs_by_uri.insert(subgraph.uri.clone(), subgraph);
}
pub(crate) fn update(&mut self, subgraph: Subgraph) {
let subgraph = Arc::new(subgraph);
self.subgraphs_by_name
.insert(subgraph.name.clone(), subgraph.clone());
self.subgraphs_by_uri.insert(subgraph.uri.clone(), subgraph);
}
pub(crate) fn remove(&mut self, uri: &lsp::Url) -> Option<Subgraph> {
if let Some(subgraph) = self.subgraphs_by_uri.remove(uri) {
self.subgraphs_by_name.remove(&subgraph.name);
Some(Arc::try_unwrap(subgraph).unwrap())
} else {
None
}
}
#[cfg(any(not(feature = "wasm"), test))]
pub(crate) async fn add_known_subgraph(&mut self, name: String, source: SchemaSource) {
match source {
SchemaSource::File { file } => {
let uri = if file.is_relative() {
let uri = lsp::Url::from_directory_path(&self.known_subgraphs.root_uri)
.expect("Failed to parse URL");
uri.join(file.to_str().expect("Failed to convert path to string"))
.expect("Failed to join URL")
} else {
lsp::Url::from_file_path(&file).expect("Failed to convert path to URL")
};
self.known_subgraphs
.by_name
.insert(name.clone(), SchemaSource::File { file: file.clone() });
self.known_subgraphs.by_uri.insert(uri, name);
}
_ => {
self.known_subgraphs
.by_name
.insert(name.clone(), source.clone());
}
};
}
#[cfg(any(not(feature = "wasm"), test))]
pub(crate) async fn remove_known_subgraph(&mut self, name: &str) {
let removed = self.known_subgraphs.by_name.remove(name);
if let Some(SchemaSource::File { file }) = removed {
let uri = if file.is_relative() {
let uri = lsp::Url::from_directory_path(&self.known_subgraphs.root_uri)
.expect("Failed to parse URL");
uri.join(file.to_str().expect("Failed to convert path to string"))
.expect("Failed to join URL")
} else {
lsp::Url::from_file_path(&file).expect("Failed to convert path to URL")
};
self.known_subgraphs.by_uri.remove(&uri);
}
}
pub(crate) fn subgraph_by_uri(&self, uri: &lsp::Url) -> Option<&Arc<Subgraph>> {
self.subgraphs_by_uri.get(uri)
}
pub(crate) fn uri_for_name(&self, name: &str) -> Option<lsp::Url> {
(self
.subgraphs_by_name
.get(name)
.map(|subgraph| subgraph.uri.clone()))
.or_else(|| self.known_subgraphs.uri_for_name(name))
}
pub fn subgraph_definitions(&self) -> Vec<SubgraphDefinition> {
self.subgraphs_by_name
.iter()
.map(|(uri, subgraph)| SubgraphDefinition {
name: subgraph.name.clone(),
url: uri.to_string(),
sdl: subgraph.source_text.to_string(),
})
.collect()
}
pub fn get_invalid_subgraph_uris(&self) -> Vec<String> {
self.subgraphs_by_name
.values()
.filter(|subgraph| subgraph.has_diagnostics())
.map(|subgraph| subgraph.uri.to_string())
.collect()
}
pub fn subgraphs_are_invalid(&self) -> bool {
self.subgraphs_by_name
.values()
.any(|subgraph| subgraph.has_diagnostics())
}
pub fn diagnostics_for_subgraph(&self, uri: &lsp::Url) -> (Vec<lsp::Diagnostic>, i32) {
let subgraph = self
.subgraph_by_uri(uri)
.unwrap_or_else(|| panic!("No subgraph found at uri: {}", uri));
(subgraph.diagnostics(), subgraph.version)
}
pub fn version_for_subgraph(&self, uri: &lsp::Url) -> Option<i32> {
self.subgraph_by_uri(uri).map(|s| s.version)
}
pub fn diagnostics_for_composition(
&self,
issues: Vec<Issue>,
) -> (DiagnosticsBySubgraph, Vec<lsp::Diagnostic>) {
let mut diagnostics_by_subgraph = self
.subgraphs_by_uri
.iter()
.map(|(uri, subgraph)| (uri.clone(), (vec![], subgraph.version)))
.collect::<DiagnosticsBySubgraph>();
for uri in self.known_subgraphs.by_uri.keys() {
if diagnostics_by_subgraph.contains_key(uri) {
continue;
}
diagnostics_by_subgraph.insert(uri.clone(), (vec![], 0));
}
let mut unattributed_diagnostics = vec![];
let first_character_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 0,
},
end: lsp::Position {
line: 0,
character: 1,
},
};
for issue in issues {
if issue.locations.is_empty() {
let message = if issue.code == "SATISFIABILITY_ERROR" {
reformat_satisfiability_error(&issue)
} else {
issue.message.clone()
};
unattributed_diagnostics.push(lsp::Diagnostic {
range: first_character_range,
severity: Some(match issue.severity {
composition::Severity::Error => lsp::DiagnosticSeverity::ERROR,
composition::Severity::Warning => lsp::DiagnosticSeverity::WARNING,
}),
message,
code: Some(lsp::NumberOrString::String(issue.code.to_string())),
..Default::default()
});
continue;
}
for location in issue.locations.iter() {
if let Some(uri) = location
.subgraph
.as_ref()
.and_then(|subgraph| self.uri_for_name(subgraph))
{
diagnostics_by_subgraph
.get_mut(&uri)
.unwrap_or_else(|| {
panic!("Unexpectedly missing subgraph with uri `{}`", uri)
})
.0
.push(lsp::Diagnostic {
range: location
.range
.as_ref()
.map(|range| lsp_range_from_line_column_range(range.clone()))
.unwrap_or(first_character_range),
severity: Some(match issue.severity {
composition::Severity::Error => lsp::DiagnosticSeverity::ERROR,
composition::Severity::Warning => lsp::DiagnosticSeverity::WARNING,
}),
message: issue.message.clone(),
code: Some(lsp::NumberOrString::String(issue.code.to_string())),
..Default::default()
});
} else {
unattributed_diagnostics.push(lsp::Diagnostic {
range: first_character_range,
severity: Some(match issue.severity {
composition::Severity::Error => lsp::DiagnosticSeverity::ERROR,
composition::Severity::Warning => lsp::DiagnosticSeverity::WARNING,
}),
message: issue.message.clone(),
code: Some(lsp::NumberOrString::String(issue.code.to_string())),
..Default::default()
});
}
}
}
(diagnostics_by_subgraph, unattributed_diagnostics)
}
pub fn on_hover(&self, uri: &lsp::Url, position: &lsp::Position) -> Option<lsp::Hover> {
let subgraph = self
.subgraph_by_uri(uri)
.unwrap_or_else(|| panic!("No subgraph found at uri: {}", uri));
let doc = subgraph.cst.document();
get_hover_at_position(&doc, subgraph.schema(), &subgraph.source_text, position)
}
pub fn goto_definition(
&self,
uri: &lsp::Url,
position: &lsp::Position,
) -> Option<Vec<lsp::LocationLink>> {
let subgraph = self
.subgraph_by_uri(uri)
.unwrap_or_else(|| panic!("No subgraph found at uri: {}", uri));
let doc = subgraph.cst.document();
goto_definition_at_position(
uri,
&doc,
subgraph.schema(),
&subgraph.source_text,
position,
Some(&self.subgraphs_by_uri),
)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::graph::{supergraph::KnownSubgraphs, Graph};
use itertools::Itertools;
#[test]
fn captures_subgraph_names_in_uri_from_query_params_or_defaults() {
let mut graph = Graph::new(
lsp::Url::parse("file:///path/to/subgraph1.graphql?subgraph_name=MyGraph").unwrap(),
r#"
extend schema @link(url: "https://specs.apollo.dev/federation/v2.10")
type Query { foo: String }
"#
.to_string(),
0,
KnownSubgraphs::default(),
Default::default(),
);
graph.update(
lsp::Url::parse("file:///path/to/subgraph2.graphql").unwrap(),
r#"
extend schema @link(url: "https://specs.apollo.dev/federation/v2.10")
type Query { bar: String }
"#
.to_string(),
1,
Default::default(),
);
let supergraph = graph.supergraph().unwrap();
assert_eq!(
supergraph
.subgraph_definitions()
.iter()
.map(|d| d.name.as_str())
.sorted()
.collect::<Vec<_>>(),
vec!["MyGraph", "file:///path/to/subgraph2.graphql"]
);
}
#[test]
fn test_unimported_spec_directives() {
let subgraph_uri = lsp::Url::parse("file:///path/to/subgraph.graphql").unwrap();
let graph = Graph::new(
subgraph_uri.clone(),
r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@key"])
@link(url: "https://specs.apollo.dev/connect/v0.1")
type Query { foo: String }
"#
.to_string(),
0,
KnownSubgraphs::default(),
Default::default(),
);
let unimported = graph
.supergraph()
.unwrap()
.subgraph_by_uri(&subgraph_uri)
.unwrap()
.links()
.values()
.flat_map(|parsed_link| parsed_link.unimported_directives())
.collect::<HashMap<_, _>>();
assert!(!unimported.contains_key(&"key".to_string()));
assert!(unimported.contains_key(&"source".to_string()));
assert!(unimported.contains_key(&"connect".to_string()));
}
}