apollo-federation-types 0.17.2

apollo-federation-types contains types used by plugins for the Rover CLI
Documentation
//! Types used with the `apollo-composition` crate

use crate::build_plugin::{
    BuildMessage, BuildMessageLevel, BuildMessageLocation, BuildMessagePoint,
};
use crate::javascript::{CompositionHint, GraphQLError, SubgraphASTNode};
use crate::rover::{BuildError, BuildHint};
use apollo_compiler::parser::LineColumn;
use apollo_federation::error::{CompositionError, FederationError};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::ops::Range;

/// Group the types from the apollo-federation that can be ambiguous.
mod native {
    pub(super) use apollo_federation::error::SubgraphLocation;
    pub(super) use apollo_federation::supergraph::CompositionHint;
}

/// Some issue the user should address. Errors block composition, warnings do not.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Issue {
    pub code: String,
    pub message: String,
    pub locations: Vec<SubgraphLocation>,
    pub severity: Severity,
}

impl Display for Issue {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        // TODO include subgraph error location information once available
        write!(f, "{}: {}", self.code, self.message)
    }
}

impl From<GraphQLError> for Issue {
    fn from(error: GraphQLError) -> Issue {
        Issue {
            code: error
                .extensions
                .map(|extension| extension.code)
                .unwrap_or_default(),
            message: error.message,
            severity: Severity::Error,
            locations: error
                .nodes
                .unwrap_or_default()
                .into_iter()
                .filter_map(SubgraphLocation::from_ast)
                .collect(),
        }
    }
}

impl From<CompositionHint> for Issue {
    fn from(hint: CompositionHint) -> Issue {
        Issue {
            code: hint.definition.code,
            message: hint.message,
            severity: Severity::Warning,
            locations: hint
                .nodes
                .unwrap_or_default()
                .into_iter()
                .filter_map(SubgraphLocation::from_ast)
                .collect(),
        }
    }
}

impl From<BuildError> for Issue {
    fn from(error: BuildError) -> Issue {
        Issue {
            code: error
                .code
                .unwrap_or_else(|| "UNKNOWN_ERROR_CODE".to_string()),
            message: error.message.unwrap_or_else(|| "Unknown error".to_string()),
            locations: error
                .nodes
                .unwrap_or_default()
                .into_iter()
                .map(Into::into)
                .collect(),
            severity: Severity::Error,
        }
    }
}

impl From<BuildHint> for Issue {
    fn from(hint: BuildHint) -> Issue {
        Issue {
            code: hint.code.unwrap_or_else(|| "UNKNOWN_HINT_CODE".to_string()),
            message: hint.message,
            locations: hint
                .nodes
                .unwrap_or_default()
                .into_iter()
                .map(Into::into)
                .collect(),
            severity: Severity::Warning,
        }
    }
}

// thrown from expand_connectors and Supergraph::parse
impl From<FederationError> for Issue {
    fn from(error: FederationError) -> Self {
        let code = match &error {
            FederationError::SingleFederationError(err) => {
                err.code().definition().code().to_string()
            }
            _ => "UNKNOWN_ERROR_CODE".to_string(),
        };
        Issue {
            code,
            // Composition failed due to an internal error, please report this: {}
            message: error.to_string(),
            locations: vec![],
            severity: Severity::Error,
        }
    }
}

impl From<CompositionError> for Issue {
    fn from(error: CompositionError) -> Self {
        Issue {
            code: error.code().definition().code().to_string(),
            message: error.to_string(),
            locations: convert_subgraph_locations(error.locations().to_vec()),
            severity: Severity::Error,
        }
    }
}

impl From<native::CompositionHint> for Issue {
    fn from(hint: native::CompositionHint) -> Self {
        Issue {
            code: hint.code,
            message: hint.message,
            locations: convert_subgraph_locations(hint.locations),
            severity: Severity::Warning,
        }
    }
}

impl From<native::SubgraphLocation> for SubgraphLocation {
    fn from(location: native::SubgraphLocation) -> Self {
        SubgraphLocation {
            subgraph: Some(location.subgraph),
            range: Some(location.range),
        }
    }
}

fn convert_subgraph_locations(
    locations: impl IntoIterator<Item = native::SubgraphLocation>,
) -> Vec<SubgraphLocation> {
    locations.into_iter().map(|loc| loc.into()).collect()
}

/// Rover and GraphOS expect messages to start with `[subgraph name]`. (They
/// don't actually look at the `locations` field, sadly). This will prepend
/// the subgraph name if there's exactly one. If there's more than one, it's
/// probably a composition issue that's not attributable to a single subgraph,
/// and GraphOS will show "[subgraph unknown]", which is also not correct.
fn maybe_prepend_subgraph(message: &str, locations: &[SubgraphLocation]) -> String {
    if message.starts_with('[') {
        return message.to_string();
    }
    let unique_subgraphs = locations
        .iter()
        .filter_map(|l| l.subgraph.as_ref())
        .collect::<HashSet<_>>();
    if unique_subgraphs.len() == 1 {
        format!(
            "[{}] {}",
            unique_subgraphs.iter().next().expect("qed"),
            message
        )
    } else {
        message.to_string()
    }
}

impl From<Issue> for BuildMessage {
    fn from(issue: Issue) -> Self {
        BuildMessage {
            level: issue.severity.into(),
            message: maybe_prepend_subgraph(&issue.message, &issue.locations),
            code: Some(issue.code.to_string()),
            locations: issue
                .locations
                .into_iter()
                .map(|location| location.into())
                .collect(),
            schema_coordinate: None,
            step: None,
            other: Default::default(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Severity {
    Error,
    Warning,
}

impl From<Severity> for BuildMessageLevel {
    fn from(severity: Severity) -> Self {
        match severity {
            Severity::Error => BuildMessageLevel::Error,
            Severity::Warning => BuildMessageLevel::Warn,
        }
    }
}

/// A location in a subgraph's SDL
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct SubgraphLocation {
    /// This field is an Option to support the lack of subgraph names in
    /// existing composition errors. New composition errors should always
    /// include a subgraph name.
    pub subgraph: Option<String>,
    pub range: Option<Range<LineColumn>>,
}

impl SubgraphLocation {
    fn from_ast(node: SubgraphASTNode) -> Option<Self> {
        Some(Self {
            subgraph: node.subgraph,
            range: node.loc.and_then(|node_loc| {
                Some(Range {
                    start: LineColumn {
                        line: node_loc.start_token.line?,
                        column: node_loc.start_token.column?,
                    },
                    end: LineColumn {
                        line: node_loc.end_token.line?,
                        column: node_loc.end_token.column?,
                    },
                })
            }),
        })
    }
}

impl From<SubgraphLocation> for BuildMessageLocation {
    fn from(location: SubgraphLocation) -> Self {
        BuildMessageLocation {
            subgraph: location.subgraph,
            start: location.range.as_ref().map(|range| BuildMessagePoint {
                line: Some(range.start.line),
                column: Some(range.start.column),
                start: None,
                end: None,
            }),
            end: location.range.as_ref().map(|range| BuildMessagePoint {
                line: Some(range.end.line),
                column: Some(range.end.column),
                start: None,
                end: None,
            }),
            source: None,
            other: Default::default(),
        }
    }
}

impl From<BuildMessageLocation> for SubgraphLocation {
    fn from(location: BuildMessageLocation) -> Self {
        Self {
            subgraph: location.subgraph,
            range: location.start.and_then(|start| {
                let end = location.end?;
                Some(Range {
                    start: LineColumn {
                        line: start.line?,
                        column: start.column?,
                    },
                    end: LineColumn {
                        line: end.line?,
                        column: end.column?,
                    },
                })
            }),
        }
    }
}

#[derive(Debug, Clone)]
pub struct MergeResult {
    pub supergraph: String,
    pub hints: Vec<Issue>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[rstest::rstest]
    #[case("hello", &[], "hello")]
    #[case("hello", &[SubgraphLocation { subgraph: Some("subgraph".to_string()), range: None }], "[subgraph] hello")]
    #[case("[other] hello", &[SubgraphLocation { subgraph: Some("subgraph".to_string()), range: None }], "[other] hello")]
    #[case("hello", &[SubgraphLocation { subgraph: Some("subgraph".to_string()), range: None }, SubgraphLocation { subgraph: Some("other".to_string()), range: None }], "hello")]
    fn test_maybe_prepend_subgraph(
        #[case] message: &str,
        #[case] locations: &[SubgraphLocation],
        #[case] expected: &str,
    ) {
        assert_eq!(maybe_prepend_subgraph(message, locations), expected);
    }
}