use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Write;
use std::sync::Arc;
use std::sync::OnceLock;
use itertools::Itertools;
use jsonschema::Validator;
use jsonschema::error::ValidationErrorKind;
use schemars::Schema;
use schemars::generate::SchemaSettings;
use yaml_rust::scanner::Marker;
use super::APOLLO_PLUGIN_PREFIX;
use super::Configuration;
use super::ConfigurationError;
use super::expansion::Expansion;
use super::expansion::coerce;
use super::plugins;
use super::yaml;
use crate::configuration::upgrade::UpgradeMode;
pub(crate) use crate::configuration::upgrade::generate_upgrade;
use crate::configuration::upgrade::upgrade_configuration;
const NUMBER_OF_PREVIOUS_LINES_TO_DISPLAY: usize = 5;
pub(crate) fn generate_config_schema() -> Schema {
let settings = SchemaSettings::draft07().with(|s| {
s.inline_subschemas = false;
});
let generator = settings.into_generator();
let mut schema = generator.into_root_schema_for::<Configuration>();
schema.insert("additionalProperties".to_string(), false.into());
schema
}
#[derive(Eq, PartialEq)]
pub(crate) enum Mode {
Upgrade,
NoUpgrade,
}
pub(crate) fn validate_yaml_configuration(
raw_yaml: &str,
expansion: Expansion,
migration: Mode,
) -> Result<Configuration, ConfigurationError> {
let defaulted_yaml = if raw_yaml.trim().is_empty() {
"{}".to_string()
} else {
raw_yaml.to_string()
};
let mut yaml: serde_json::Value = serde_yaml::from_str(&defaulted_yaml).map_err(|e| {
ConfigurationError::InvalidConfiguration {
message: "failed to parse yaml",
error: e.to_string(),
}
})?;
if let Some(object) = yaml.as_object_mut()
&& let Some(plugins_value) = object.get_mut("plugins").filter(|v| v.is_null())
{
*plugins_value = serde_json::json!({});
}
static VALIDATOR: OnceLock<Validator> = OnceLock::new();
let validator = VALIDATOR.get_or_init(|| {
let config_schema = serde_json::to_value(generate_config_schema())
.expect("failed to parse configuration schema");
let result = jsonschema::draft7::new(&config_schema);
match result {
Ok(validator) => validator,
Err(e) => {
panic!("failed to compile configuration schema: {e}")
}
}
});
if migration == Mode::Upgrade {
let upgraded = upgrade_configuration(&yaml, true, UpgradeMode::Minor)?;
let expanded_yaml = expansion.expand(&upgraded)?;
if validator.is_valid(&expanded_yaml) {
yaml = upgraded;
} else {
tracing::warn!(
"Configuration could not be upgraded automatically as it had errors. If you previously used this configuration with Router 1.x, please refer to the migration guide: https://www.apollographql.com/docs/graphos/reference/migration/from-router-v1"
)
}
}
let expanded_yaml = expansion.expand(&yaml)?;
let parsed_yaml = super::yaml::parse(raw_yaml)?;
{
let mut errors_it = validator.iter_errors(&expanded_yaml).peekable();
if errors_it.peek().is_some() {
let yaml_split_by_lines = raw_yaml.split('\n').collect::<Vec<_>>();
let mut errors = String::new();
for (idx, mut e) in errors_it.enumerate() {
if let Some(element) = parsed_yaml.get_element(&e.instance_path) {
match element {
yaml::Value::String(value, marker) => {
let start_marker = marker;
let end_marker = marker;
let offset = start_marker
.line()
.saturating_sub(NUMBER_OF_PREVIOUS_LINES_TO_DISPLAY);
let end = if end_marker.line() > yaml_split_by_lines.len() {
yaml_split_by_lines.len()
} else {
end_marker.line()
};
let lines = yaml_split_by_lines[offset..end]
.iter()
.map(|line| format!(" {line}"))
.join("\n");
e.instance = Cow::Owned(coerce(value));
let _ = write!(
&mut errors,
"{}. at line {}\n\n{}\n{}^----- {}\n\n",
idx + 1,
start_marker.line(),
lines,
" ".repeat(2 + marker.col()),
e
);
}
seq_element @ yaml::Value::Sequence(_, m) => {
let (start_marker, end_marker) = (m, seq_element.end_marker());
let lines =
context_lines(&yaml_split_by_lines, start_marker, end_marker);
let _ = write!(
&mut errors,
"{}. at line {}\n\n{}\nâ””-----> {}\n\n",
idx + 1,
start_marker.line(),
lines,
e
);
}
map_value @ yaml::Value::Mapping(current_label, map, marker) => {
let unexpected_opt = match &e.kind {
ValidationErrorKind::AdditionalProperties { unexpected } => {
Some(unexpected.clone())
}
_ => None,
};
if let Some(unexpected) = unexpected_opt {
for key in unexpected {
if let Some((label, value)) =
map.iter().find(|(label, _)| label.name == key)
{
let (start_marker, end_marker) = (
label.marker.as_ref().unwrap_or(marker),
value.end_marker(),
);
let lines = context_lines(
&yaml_split_by_lines,
start_marker,
end_marker,
);
e.kind = ValidationErrorKind::AdditionalProperties {
unexpected: vec![key.clone()],
};
let _ = write!(
&mut errors,
"{}. at line {}\n\n{}\nâ””-----> {}\n\n",
idx + 1,
start_marker.line(),
lines,
e
);
}
}
} else {
let (start_marker, end_marker) = (
current_label
.as_ref()
.and_then(|label| label.marker.as_ref())
.unwrap_or(marker),
map_value.end_marker(),
);
let lines =
context_lines(&yaml_split_by_lines, start_marker, end_marker);
let _ = write!(
&mut errors,
"{}. at line {}\n\n{}\nâ””-----> {}\n\n",
idx + 1,
start_marker.line(),
lines,
e
);
}
}
}
}
}
if !errors.is_empty() {
tracing::warn!(
"Configuration had errors. It may be possible to update your configuration automatically. Execute 'router config upgrade --help' for more details. If you previously used this configuration with Router 1.x, please refer to the upgrade guide: https://www.apollographql.com/docs/graphos/reference/upgrade/from-router-v1"
);
return Err(ConfigurationError::InvalidConfiguration {
message: "configuration had errors",
error: format!("\n{errors}"),
});
}
}
}
let mut config: Configuration = serde_json::from_value(expanded_yaml.clone())
.map_err(ConfigurationError::DeserializeConfigError)?;
config.raw_yaml = Some(Arc::from(raw_yaml));
let registered_plugins = plugins();
let apollo_plugin_names: Vec<&str> = registered_plugins
.filter_map(|factory| factory.name.strip_prefix(APOLLO_PLUGIN_PREFIX))
.collect();
let unknown_fields: Vec<&String> = config
.apollo_plugins
.plugins
.keys()
.filter(|ap_name| {
let ap_name = ap_name.as_str();
ap_name != "server" && ap_name != "plugins" && !apollo_plugin_names.contains(&ap_name)
})
.collect();
if !unknown_fields.is_empty() {
tracing::warn!(
"Configuration had errors. It may be possible to update your configuration automatically. Execute 'router config upgrade --help' for more details. If you previously used this configuration with Router 1.x, please refer to the upgrade guide: https://www.apollographql.com/docs/graphos/reference/upgrade/from-router-v1"
);
return Err(ConfigurationError::InvalidConfiguration {
message: "unknown fields",
error: format!(
"additional properties are not allowed ('{}' was/were unexpected)",
unknown_fields.iter().join(", ")
),
});
}
config.validated_yaml = Some(expanded_yaml);
Ok(config)
}
fn context_lines(
yaml_split_by_lines: &[&str],
start_marker: &Marker,
end_marker: &Marker,
) -> String {
let offset = start_marker
.line()
.saturating_sub(NUMBER_OF_PREVIOUS_LINES_TO_DISPLAY);
yaml_split_by_lines[offset..end_marker.line()]
.iter()
.enumerate()
.map(|(idx, line)| {
let real_line = idx + offset + 1;
match real_line.cmp(&start_marker.line()) {
Ordering::Equal => format!("┌ {line}"),
Ordering::Greater => format!("| {line}"),
Ordering::Less => format!(" {line}"),
}
})
.join("\n")
}