mod connect;
mod coordinates;
mod errors;
mod expression;
mod graphql;
mod http;
mod schema;
mod source;
use std::ops::Range;
use apollo_compiler::Name;
use apollo_compiler::Schema;
use apollo_compiler::parser::LineColumn;
use apollo_compiler::schema::SchemaBuilder;
use itertools::Itertools;
pub(crate) use schema::field_set_is_subset;
use strum_macros::Display;
use strum_macros::IntoStaticStr;
use crate::connectors::ConnectSpec;
use crate::connectors::spec::ConnectLink;
use crate::connectors::spec::source::SOURCE_DIRECTIVE_NAME_IN_SPEC;
use crate::connectors::validation::connect::fields_seen_by_all_connects;
use crate::connectors::validation::graphql::SchemaInfo;
use crate::connectors::validation::source::SourceDirective;
#[derive(Debug)]
pub struct ValidationResult {
pub errors: Vec<Message>,
pub has_connectors: bool,
pub schema: Schema,
pub transformed: String,
}
pub fn validate(mut source_text: String, file_name: &str) -> ValidationResult {
let schema = SchemaBuilder::new()
.adopt_orphan_extensions()
.parse(&source_text, file_name)
.build()
.unwrap_or_else(|schema_with_errors| schema_with_errors.partial);
let link = match ConnectLink::new(&schema) {
None => {
return ValidationResult {
errors: Vec::new(),
has_connectors: false,
schema,
transformed: source_text,
};
}
Some(Err(err)) => {
return ValidationResult {
errors: vec![err],
has_connectors: true,
schema,
transformed: source_text,
};
}
Some(Ok(link)) => link,
};
let schema_info = SchemaInfo::new(&schema, &source_text, link);
let (source_directives, mut messages) = SourceDirective::find(&schema_info);
let all_source_names = source_directives
.iter()
.map(|directive| directive.name.clone())
.collect_vec();
for source in source_directives {
messages.extend(source.type_check());
}
match fields_seen_by_all_connects(&schema_info, &all_source_names) {
Ok(fields_seen_by_connectors) => {
messages.extend(schema::validate(
&schema_info,
file_name,
fields_seen_by_connectors,
))
}
Err(errs) => {
messages.extend(errs);
}
}
if schema_info.source_directive_name() == DEFAULT_SOURCE_DIRECTIVE_NAME
&& messages
.iter()
.any(|error| error.code == Code::NoSourcesDefined)
{
messages.push(Message {
code: Code::NoSourceImport,
message: format!("The `@{SOURCE_DIRECTIVE_NAME_IN_SPEC}` directive is not imported. Try adding `@{SOURCE_DIRECTIVE_NAME_IN_SPEC}` to `import` for `{link}`", link=schema_info.connect_link),
locations: schema_info.connect_link.directive.line_column_range(&schema.sources)
.into_iter()
.collect(),
});
}
if schema_info.connect_link.spec == ConnectSpec::V0_1 {
if let Some(version_range) =
schema_info
.connect_link
.directive
.location()
.and_then(|link_range| {
let version_offset = source_text
.get(link_range.offset()..link_range.end_offset())?
.find(ConnectSpec::V0_1.as_str())?;
let start = link_range.offset() + version_offset;
let end = start + ConnectSpec::V0_1.as_str().len();
Some(start..end)
})
{
source_text.replace_range(version_range, ConnectSpec::V0_2.as_str());
} else {
messages.push(Message {
code: Code::UnknownConnectorsVersion,
message: "Failed to auto-upgrade 0.1 to 0.2, you must manually update the version in `@link`".to_string(),
locations: schema_info.connect_link.directive.line_column_range(&schema.sources)
.into_iter()
.collect(),
});
return ValidationResult {
errors: messages,
has_connectors: true,
schema,
transformed: source_text,
};
};
}
ValidationResult {
errors: messages,
has_connectors: true,
schema,
transformed: source_text,
}
}
const DEFAULT_SOURCE_DIRECTIVE_NAME: &str = "connect__source";
type DirectiveName = Name;
#[derive(Debug, Clone)]
pub struct Message {
pub code: Code,
pub message: String,
pub locations: Vec<Range<LineColumn>>,
}
#[derive(Clone, Copy, Debug, Display, Eq, IntoStaticStr, PartialEq)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum Code {
GraphQLError,
DuplicateSourceName,
DuplicateIdName,
InvalidSourceName,
EmptySourceName,
InvalidConnectorIdName,
InvalidUrl,
InvalidUrlScheme,
SourceNameMismatch,
SubscriptionInConnectors,
AbsoluteConnectUrlWithSource,
RelativeConnectUrlWithoutSource,
NoSourcesDefined,
NoSourceImport,
MultipleHttpMethods,
MissingHttpMethod,
EntityNotOnRootQuery,
EntityResolverArgumentMismatch,
EntityTypeInvalid,
MissingEntityConnector,
InvalidSelection,
InvalidBody,
InvalidErrorsMessage,
InvalidIsSuccess,
CircularReference,
SelectedFieldNotFound,
GroupSelectionIsNotObject,
HttpHeaderNameCollision,
InvalidHeader,
ConnectorsUnsupportedFederationDirective,
ConnectorsUnsupportedAbstractType,
GroupSelectionRequiredForObject,
ConnectorsUnresolvedField,
ConnectorsFieldWithArguments,
ConnectorsBatchKeyNotInSelection,
ConnectorsNonRootBatchKey,
ConnectorsCannotResolveKey,
UndefinedArgument,
UndefinedField,
UnsupportedVariableType,
UnknownConnectorsVersion,
ConnectOnTypeMustBeEntity,
ConnectOnRoot,
ConnectBatchAndThis,
InvalidUrlProperty,
MissingSchemaType,
}
impl Code {
pub fn severity(&self) -> Severity {
match self {
Self::NoSourceImport => Severity::Warning,
_ => Severity::Error,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
}
#[cfg(test)]
mod test_validate_source {
use std::fs::read_to_string;
use insta::assert_debug_snapshot;
use insta::assert_snapshot;
use insta::glob;
use pretty_assertions::assert_str_eq;
use super::*;
#[test]
fn validation_tests() {
insta::with_settings!({prepend_module_to_snapshot => false}, {
glob!("test_data", "**/*.graphql", |path| {
let schema = read_to_string(path).unwrap();
let result = validate(schema.clone(), path.to_str().unwrap());
assert_debug_snapshot!(result.errors);
if path.parent().is_some_and(|parent| parent.ends_with("transformed")) {
assert_snapshot!(&diff::lines(&schema, &result.transformed).into_iter().filter_map(|res| match res {
diff::Result::Left(line) => Some(format!("- {line}")),
diff::Result::Right(line) => Some(format!("+ {line}")),
diff::Result::Both(_, _) => None,
}).join("\n"));
} else {
assert_str_eq!(schema, result.transformed, "Schema should not have been transformed by validations")
}
});
});
}
}