graphql-composition 0.12.2

An implementation of GraphQL federated schema composition
Documentation
use super::*;
use crate::federated_graph::{Import, LinkDirectiveDeserialize, QualifiedImport};
use cynic_parser_deser::ConstDeserializer;

fn is_grafbase_extension_registry_url(url: &url::Url) -> bool {
    if url.scheme() != "https" {
        return false;
    }

    if let Some(host) = url.host_str() {
        if host == "extensions.grafbase.com" {
            return true;
        }
        if host == "grafbase.com" && url.path().starts_with("/extensions") {
            return true;
        }
    }

    false
}

pub(super) fn ingest_link_directive(directive: ast::Directive<'_>, subgraph_id: SubgraphId, subgraphs: &mut Subgraphs) {
    let LinkDirectiveDeserialize {
        url,
        r#as,
        import,
        r#for: _,
    }: LinkDirectiveDeserialize<'_> = match directive.deserialize() {
        Ok(directive) => directive,
        Err(err) => {
            subgraphs.push_ingestion_diagnostic(subgraph_id, format!("Invalid `@link` directive: {err}"));
            return;
        }
    };

    let link_url = subgraphs::parse_link_url(url);

    let name = link_url
        .as_ref()
        .and_then(|link_url| link_url.name.as_deref())
        .map(|name| subgraphs.strings.intern(name));

    let linked_schema_type = if let Some(federation_spec) = subgraphs::FederationSpec::from_url(url) {
        subgraphs::LinkedSchemaType::FederationSpec(federation_spec)
    } else {
        subgraphs::LinkedSchemaType::Other
    };

    let url = subgraphs.strings.intern(url);
    let r#as = r#as.map(|r#as| subgraphs.strings.intern(r#as));

    if let Some(link_url) = link_url.as_ref() {
        ingest_grafbase_extension_from_link(subgraphs, url, r#as, link_url);
    }

    let linked_schema_id = subgraphs.push_linked_schema(subgraphs::LinkedSchemaRecord {
        subgraph_id,
        linked_schema_type,
        url,
        r#as,
        name_from_url: name,
    });

    for import in import.into_iter().flatten() {
        match import {
            Import::String(name) => {
                let name = name.trim_start_matches("@");
                let original_name = subgraphs.strings.intern(name);

                subgraphs.push_linked_definition(
                    subgraph_id,
                    subgraphs::LinkedDefinitionRecord {
                        linked_schema_id,
                        original_name,
                        imported_as: None,
                    },
                );
            }
            Import::Qualified(QualifiedImport { name, r#as }) => {
                let is_directive = name.starts_with('@');

                let trimmed_name = name.trim_start_matches("@");
                let original_name = subgraphs.strings.intern(trimmed_name);

                let imported_as = if let Some(r#as) = r#as {
                    if r#as.starts_with('@') != is_directive {
                        if is_directive {
                            subgraphs.push_ingestion_diagnostic(
                                subgraph_id,
                                format!("Error in @link import: `{name}` is a directive, but it is imported as `{as}`. Missing @ prefix."),
                            );
                        } else if !is_directive {
                            subgraphs.push_ingestion_diagnostic(
                                subgraph_id,
                                format!("Error in @link import: `{name}` is not a directive, but it is imported as `{as}`. Consider removing the @ prefix."),
                            );
                        }
                    }

                    Some(subgraphs.strings.intern(r#as.trim_start_matches("@")))
                } else {
                    None
                };

                subgraphs.push_linked_definition(
                    subgraph_id,
                    subgraphs::LinkedDefinitionRecord {
                        linked_schema_id,
                        original_name,
                        imported_as,
                    },
                );
            }
        }
    }
}

/// Treat `@link`ed schemas with a file url or Grafbase extension registry URLs as extensions.
fn ingest_grafbase_extension_from_link(
    subgraphs: &mut Subgraphs,
    url: subgraphs::StringId,
    r#as: Option<subgraphs::StringId>,
    link_url: &subgraphs::LinkUrl,
) {
    if link_url.url.scheme() == "file" {
        let Some(name) = r#as.or_else(|| {
            let mut segments = link_url.url.path_segments()?;

            let mut name = segments.next_back()?;
            if name == "build"
                && let Some(next) = segments.next_back()
            {
                name = next
            }

            let mut name_chars = name.chars();

            let name = match name_chars.next() {
                Some('v') => match name_chars.next() {
                    Some(next_char) if next_char.is_ascii_digit() => segments.next_back(),
                    _ => Some(name),
                },
                Some(c) if c.is_ascii_digit() => segments.next_back(),
                Some(_) => Some(name),
                None => segments.next_back(),
            };

            name.map(|s| subgraphs.strings.intern(s))
        }) else {
            return;
        };

        subgraphs.push_extension(subgraphs::ExtensionRecord {
            url,
            link_url: url,
            name,
        });
    }

    if is_grafbase_extension_registry_url(&link_url.url) {
        let Some(name) = r#as.or_else(|| {
            let mut segments = link_url.url.path_segments()?;

            let name = segments.next_back()?;
            let mut name_chars = name.chars();

            let name = match name_chars.next() {
                Some('v') => match name_chars.next() {
                    Some(next_char) if next_char.is_ascii_digit() => segments.next_back(),
                    _ => Some(name),
                },
                Some(c) if c.is_ascii_digit() => segments.next_back(),
                Some(_) => Some(name),
                None => segments.next_back(),
            };

            name.map(|s| subgraphs.strings.intern(s))
        }) else {
            return;
        };

        subgraphs.push_extension(subgraphs::ExtensionRecord {
            url,
            link_url: url,
            name,
        });
    }
}