use std::{collections::HashMap, path::Path};
use pest::Parser;
use serde_json::json;
use crate::{
error::{
HenDiagnostic, HenDiagnosticLocation, HenDiagnosticPosition,
HenDiagnosticRange, HenDiagnosticRelatedInformation,
HenDiagnosticSymbol,
},
schema::{
DiscriminatorBranch, NumericValue, ObjectSchema, PrimitiveType, ScalarBase,
ScalarDefinition, ScalarExpression, ScalarLiteral, ScalarPredicate,
SchemaDefinition, SchemaField, SchemaRegistry, SchemaRegistryError,
SchemaShape, TypeReference,
},
};
use super::{CollectionParser, Rule, legacy_header, preprocessor};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeclarationSymbolKind {
Scalar,
Schema,
}
impl DeclarationSymbolKind {
fn label(self) -> &'static str {
match self {
DeclarationSymbolKind::Scalar => "scalar",
DeclarationSymbolKind::Schema => "schema",
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct SourceSpan {
start: usize,
end: usize,
}
impl SourceSpan {
fn from_pest_span(span: pest::Span<'_>) -> Self {
Self {
start: span.start(),
end: span.end(),
}
}
fn into_pest_span<'a>(self, source: &'a str) -> pest::Span<'a> {
pest::Span::new(source, self.start, self.end).unwrap()
}
}
pub(super) type DeclarationSpanIndex = HashMap<String, SourceSpan>;
pub(super) fn register_declaration(
schema_registry: &mut SchemaRegistry,
pair: pest::iterators::Pair<Rule>,
) -> Result<(), pest::error::Error<Rule>> {
let inner = pair.into_inner().next().unwrap();
match inner.as_rule() {
Rule::scalar_declaration => register_scalar_declaration(schema_registry, inner),
Rule::schema_object_declaration | Rule::schema_assignment_declaration => {
register_schema_declaration(schema_registry, inner)
}
_ => unreachable!("unexpected declaration rule: {:?}", inner.as_rule()),
}
}
fn register_scalar_declaration(
schema_registry: &mut SchemaRegistry,
pair: pest::iterators::Pair<Rule>,
) -> Result<(), pest::error::Error<Rule>> {
let span = pair.as_span();
let definition = parse_scalar_definition(pair)?;
schema_registry
.register_scalar(definition)
.map_err(|err| schema_registry_error_to_span(err, span))
}
fn register_schema_declaration(
schema_registry: &mut SchemaRegistry,
pair: pest::iterators::Pair<Rule>,
) -> Result<(), pest::error::Error<Rule>> {
let span = pair.as_span();
let definition = parse_schema_definition(pair)?;
schema_registry
.register_schema(definition)
.map_err(|err| schema_registry_error_to_span(err, span))
}
fn parse_scalar_definition(
pair: pest::iterators::Pair<Rule>,
) -> Result<ScalarDefinition, pest::error::Error<Rule>> {
let span = pair.as_span();
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let expression = parse_scalar_expression(inner.next().unwrap())?;
if expression.base.is_none() && expression.predicates.is_empty() {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "scalar declaration must define a base type or predicate".to_string(),
},
span,
));
}
Ok(ScalarDefinition::new(name, expression))
}
fn parse_scalar_expression(
pair: pest::iterators::Pair<Rule>,
) -> Result<ScalarExpression, pest::error::Error<Rule>> {
let span = pair.as_span();
let mut base = None;
let mut predicates = Vec::new();
for term in pair.into_inner() {
match term.as_rule() {
Rule::primitive_type => {
if base
.replace(ScalarBase::Primitive(parse_primitive_type(term.as_str())))
.is_some()
{
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "scalar expressions can only declare one base type".to_string(),
},
span,
));
}
}
Rule::identifier => {
if base
.replace(ScalarBase::Named(term.as_str().to_string()))
.is_some()
{
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "scalar expressions can only declare one base type".to_string(),
},
span,
));
}
}
Rule::const_predicate => {
predicates.push(ScalarPredicate::Enum(vec![parse_scalar_literal(
term.into_inner().next().unwrap(),
)]));
}
Rule::enum_predicate => predicates.push(parse_enum_predicate(term)),
Rule::format_predicate => predicates.push(parse_format_predicate(term)),
Rule::len_predicate => predicates.push(parse_len_predicate(term)?),
Rule::pattern_predicate => predicates.push(parse_pattern_predicate(term)),
Rule::range_predicate => predicates.push(parse_range_predicate(term)?),
_ => unreachable!("unexpected scalar term: {:?}", term.as_rule()),
}
}
Ok(ScalarExpression::new(base, predicates))
}
fn parse_enum_predicate(pair: pest::iterators::Pair<Rule>) -> ScalarPredicate {
let values = pair.into_inner().map(parse_scalar_literal).collect();
ScalarPredicate::Enum(values)
}
fn parse_format_predicate(pair: pest::iterators::Pair<Rule>) -> ScalarPredicate {
let name = pair.into_inner().next().unwrap().as_str().to_string();
ScalarPredicate::Format(name)
}
fn parse_len_predicate(
pair: pest::iterators::Pair<Rule>,
) -> Result<ScalarPredicate, pest::error::Error<Rule>> {
let span = pair.as_span();
let (min, max) = parse_bounds_body(pair.as_str(), span.clone(), "len")?;
let min = min
.map(|value| {
value.parse::<usize>().map_err(|_| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "len() bounds must be integers".to_string(),
},
span.clone(),
)
})
})
.transpose()?;
let max = max
.map(|value| {
value.parse::<usize>().map_err(|_| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "len() bounds must be integers".to_string(),
},
span.clone(),
)
})
})
.transpose()?;
Ok(ScalarPredicate::Length { min, max })
}
fn parse_pattern_predicate(pair: pest::iterators::Pair<Rule>) -> ScalarPredicate {
let regex_literal = pair.into_inner().next().unwrap().as_str();
ScalarPredicate::Pattern(regex_literal[1..regex_literal.len() - 1].to_string())
}
fn parse_range_predicate(
pair: pest::iterators::Pair<Rule>,
) -> Result<ScalarPredicate, pest::error::Error<Rule>> {
let span = pair.as_span();
let (min, max) = parse_bounds_body(pair.as_str(), span.clone(), "range")?;
let min = min
.map(|value| parse_numeric_value(value, span.clone()))
.transpose()?;
let max = max
.map(|value| parse_numeric_value(value, span.clone()))
.transpose()?;
Ok(ScalarPredicate::Range { min, max })
}
fn parse_scalar_literal(pair: pest::iterators::Pair<Rule>) -> ScalarLiteral {
match pair.as_rule() {
Rule::quoted_string => ScalarLiteral::String(normalize_quoted_string(pair.as_str())),
Rule::boolean_literal => ScalarLiteral::Boolean(pair.as_str() == "true"),
Rule::null_literal => ScalarLiteral::Null,
Rule::numeric_literal => {
if pair.as_str().contains('.') {
ScalarLiteral::Number(pair.as_str().parse().unwrap())
} else {
ScalarLiteral::Integer(pair.as_str().parse().unwrap())
}
}
_ => unreachable!("unexpected scalar literal: {:?}", pair.as_rule()),
}
}
fn parse_schema_definition(
pair: pest::iterators::Pair<Rule>,
) -> Result<SchemaDefinition, pest::error::Error<Rule>> {
let rule = pair.as_rule();
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let shape = match rule {
Rule::schema_object_declaration => {
let object = parse_object_schema(inner.next().unwrap())?;
SchemaShape::Object(object)
}
Rule::schema_assignment_declaration => {
parse_schema_assignment_shape(inner.next().unwrap())?
}
_ => unreachable!("unexpected schema declaration: {:?}", rule),
};
Ok(SchemaDefinition::new(name, shape))
}
fn parse_schema_assignment_shape(
pair: pest::iterators::Pair<Rule>,
) -> Result<SchemaShape, pest::error::Error<Rule>> {
let span = pair.as_span();
match pair.as_rule() {
Rule::type_reference => {
let value_type = parse_type_reference(pair)?;
if matches!(value_type.kind, crate::schema::TypeReferenceKind::Array(_)) {
Ok(SchemaShape::Array(value_type))
} else {
Ok(SchemaShape::Alias(value_type))
}
}
Rule::schema_allof_expression => parse_schema_target_list(pair).map(SchemaShape::AllOf),
Rule::schema_oneof_expression => parse_schema_target_list(pair).map(SchemaShape::OneOf),
Rule::schema_anyof_expression => parse_schema_target_list(pair).map(SchemaShape::AnyOf),
Rule::schema_not_expression => {
let target = pair.into_inner().next().unwrap().as_str().to_string();
Ok(SchemaShape::Not(target))
}
Rule::schema_discriminator_expression => parse_discriminator_shape(pair),
_ => Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "unsupported schema assignment expression".to_string(),
},
span,
)),
}
}
fn parse_schema_target_list(
pair: pest::iterators::Pair<Rule>,
) -> Result<Vec<String>, pest::error::Error<Rule>> {
let span = pair.as_span();
let targets = pair
.into_inner()
.flat_map(|entry| {
if entry.as_rule() == Rule::schema_target_list {
entry.into_inner().collect::<Vec<_>>()
} else {
vec![entry]
}
})
.filter(|entry| entry.as_rule() == Rule::schema_target)
.map(|entry| entry.as_str().to_string())
.collect::<Vec<_>>();
if targets.len() < 2 {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "schema combinators require at least two targets".to_string(),
},
span,
));
}
Ok(targets)
}
fn parse_discriminator_shape(
pair: pest::iterators::Pair<Rule>,
) -> Result<SchemaShape, pest::error::Error<Rule>> {
let span = pair.as_span();
let mut inner = pair.into_inner();
let field = inner.next().unwrap().as_str().to_string();
let mut branches = Vec::new();
for branch in inner {
let mut branch_inner = branch.into_inner();
let tag = parse_scalar_literal(branch_inner.next().unwrap());
let target = branch_inner.next().unwrap().as_str().to_string();
branches.push(DiscriminatorBranch { tag, target });
}
if branches.is_empty() {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "discriminator() requires at least one branch".to_string(),
},
span,
));
}
Ok(SchemaShape::Discriminator { field, branches })
}
fn parse_object_schema(
pair: pest::iterators::Pair<Rule>,
) -> Result<ObjectSchema, pest::error::Error<Rule>> {
let mut fields = Vec::new();
for field in pair.into_inner() {
fields.push(parse_schema_field(field)?);
}
Ok(ObjectSchema::open(fields))
}
fn parse_schema_field(
pair: pest::iterators::Pair<Rule>,
) -> Result<SchemaField, pest::error::Error<Rule>> {
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let next = inner.next().unwrap();
if next.as_rule() == Rule::field_optional {
let value_type = parse_type_reference(inner.next().unwrap())?;
Ok(SchemaField::optional(name, value_type))
} else {
let value_type = parse_type_reference(next)?;
Ok(SchemaField::required(name, value_type))
}
}
fn parse_type_reference(
pair: pest::iterators::Pair<Rule>,
) -> Result<TypeReference, pest::error::Error<Rule>> {
let mut inner = pair.into_inner();
let base = inner.next().unwrap();
let mut value_type = match base.as_rule() {
Rule::primitive_type => TypeReference::primitive(parse_primitive_type(base.as_str())),
Rule::identifier => TypeReference::named(base.as_str()),
_ => unreachable!("unexpected type reference base: {:?}", base.as_rule()),
};
for part in inner {
match part.as_rule() {
Rule::array_suffix => {
value_type = TypeReference::array(value_type);
}
Rule::nullable_suffix => {
value_type = value_type.nullable();
}
_ => unreachable!("unexpected type reference component: {:?}", part.as_rule()),
}
}
Ok(value_type)
}
fn parse_primitive_type(value: &str) -> PrimitiveType {
match value {
"string" => PrimitiveType::String,
"integer" => PrimitiveType::Integer,
"number" => PrimitiveType::Number,
"boolean" => PrimitiveType::Boolean,
"null" => PrimitiveType::Null,
_ => unreachable!("unexpected primitive type: {}", value),
}
}
fn parse_bounds_body<'a>(
raw: &'a str,
span: pest::Span<'_>,
prefix: &str,
) -> Result<(Option<&'a str>, Option<&'a str>), pest::error::Error<Rule>> {
let body = raw
.trim()
.strip_prefix(prefix)
.and_then(|value| value.trim_start().strip_prefix('('))
.and_then(|value| value.strip_suffix(')'))
.ok_or_else(|| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!("invalid {}() predicate", prefix),
},
span.clone(),
)
})?;
let Some((min, max)) = body.split_once("..") else {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!("{}() requires min..max syntax", prefix),
},
span,
));
};
let min = if min.trim().is_empty() {
None
} else {
Some(min.trim())
};
let max = if max.trim().is_empty() {
None
} else {
Some(max.trim())
};
if min.is_none() && max.is_none() {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!("{}() requires at least one bound", prefix),
},
span,
));
}
Ok((min, max))
}
fn parse_numeric_value(
raw: &str,
span: pest::Span<'_>,
) -> Result<NumericValue, pest::error::Error<Rule>> {
if raw.contains('.') {
raw.parse::<f64>()
.map(NumericValue::Number)
.map_err(|_| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "range() bounds must be numbers".to_string(),
},
span,
)
})
} else {
raw.parse::<i64>()
.map(NumericValue::Integer)
.map_err(|_| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "range() bounds must be numbers".to_string(),
},
span,
)
})
}
}
fn normalize_quoted_string(value: &str) -> String {
value[1..value.len() - 1].to_string()
}
fn schema_registry_error_to_span(
error: SchemaRegistryError,
span: pest::Span<'_>,
) -> pest::error::Error<Rule> {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: error.to_string(),
},
span,
)
}
pub(super) fn structured_diagnostic_for_parse_error(
input: &str,
working_dir: &Path,
error: &pest::error::Error<Rule>,
path: Option<&Path>,
) -> Option<HenDiagnostic> {
let preprocessed = preprocessor::preprocess(input, working_dir).ok()?;
let preprocessed = legacy_header::normalize_legacy_collection_header(preprocessed.as_str());
if let Some(diagnostic) = misplaced_declaration_diagnostic(preprocessed.as_str(), error, path) {
return Some(diagnostic);
}
if let Some(diagnostic) = scalar_declaration_authoring_diagnostic(preprocessed.as_str(), error, path) {
return Some(diagnostic);
}
let collection = parse_request_collection(preprocessed.as_str()).ok()?;
if let Some(duplicate_name) = duplicate_environment_name(error) {
let (original_span, duplicate_span) =
duplicate_environment_spans(collection.clone(), duplicate_name)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
duplicate_span,
duplicate_name,
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), duplicate_span))?;
diagnostic.related_information = vec![HenDiagnosticRelatedInformation {
message: format!("First environment '{}' is declared here.", duplicate_name),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: reference_range_in_source_span(
preprocessed.as_str(),
original_span,
duplicate_name,
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), original_span))?,
},
}];
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: "environment".to_string(),
name: duplicate_name.to_string(),
role: "declaration".to_string(),
});
diagnostic.data = Some(json!({
"duplicateName": duplicate_name,
}));
return Some(diagnostic);
}
if let Some(duplicate_name) = duplicate_oauth_profile_name(error) {
let (original_span, duplicate_span) =
duplicate_oauth_profile_spans(collection.clone(), duplicate_name)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
duplicate_span,
duplicate_name,
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), duplicate_span))?;
diagnostic.related_information = vec![HenDiagnosticRelatedInformation {
message: format!("First OAuth profile '{}' is declared here.", duplicate_name),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: reference_range_in_source_span(
preprocessed.as_str(),
original_span,
duplicate_name,
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), original_span))?,
},
}];
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: "oauthProfile".to_string(),
name: duplicate_name.to_string(),
role: "declaration".to_string(),
});
diagnostic.data = Some(json!({
"duplicateName": duplicate_name,
}));
return Some(diagnostic);
}
if let Some((environment_name, missing_variable)) = unknown_environment_variable_details(error) {
let available_names = global_variable_names(collection.clone());
let line_span = line_span(preprocessed.as_str(), error_primary_line(error)?)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
line_span,
missing_variable.as_str(),
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), line_span))?;
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: "variable".to_string(),
name: missing_variable.clone(),
role: "reference".to_string(),
});
if !available_names.is_empty() {
add_available_names_data(&mut diagnostic, &available_names);
diagnostic.suggestions = available_names
.iter()
.take(3)
.map(|name| crate::error::HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(diagnostic.location.range.clone()),
text: Some(name.clone()),
})
.collect();
}
if let Some(serde_json::Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("expectedKinds".to_string(), json!(["scalarVariable"]));
map.insert("environmentName".to_string(), json!(environment_name));
map.insert("symbolName".to_string(), json!(missing_variable));
} else {
diagnostic.data = Some(json!({
"expectedKinds": ["scalarVariable"],
"environmentName": environment_name,
"symbolName": missing_variable,
}));
}
return Some(diagnostic);
}
if let Some(duplicate_name) = duplicate_declaration_name(error) {
let (duplicate_kind, original_span, duplicate_span) =
duplicate_declaration_spans(collection.clone(), duplicate_name)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = diagnostic_range_from_source_span(preprocessed.as_str(), duplicate_span)?;
diagnostic.related_information = vec![HenDiagnosticRelatedInformation {
message: format!("First declaration of '{}' is here.", duplicate_name),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: diagnostic_range_from_source_span(preprocessed.as_str(), original_span)?,
},
}];
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: duplicate_kind.label().to_string(),
name: duplicate_name.to_string(),
role: "declaration".to_string(),
});
return Some(diagnostic);
}
if let Some(reserved_name) = reserved_declaration_name(error) {
let (reserved_kind, declaration_span) =
declaration_span_and_kind(collection.clone(), reserved_name)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
declaration_span,
reserved_name,
)
.or_else(|| diagnostic_range_from_source_span(preprocessed.as_str(), declaration_span))?;
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: reserved_kind.label().to_string(),
name: reserved_name.to_string(),
role: "declaration".to_string(),
});
diagnostic.data = Some(json!({
"reservedName": reserved_name,
"reservedNamespace": "builtin",
}));
return Some(diagnostic);
}
if let Some((owner_name, missing_reference)) = unknown_reference_names(error) {
let declaration_spans = declaration_spans_for_collection(collection.clone());
let owner_span = declaration_spans.get(owner_name.as_str()).copied()?;
let available_names = declaration_names(collection);
if available_names.is_empty() {
return None;
}
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
owner_span,
missing_reference.as_str(),
)
.unwrap_or_else(|| diagnostic.location.range.clone());
add_available_names_data(&mut diagnostic, &available_names);
diagnostic.suggestions = available_names
.iter()
.take(3)
.map(|name| crate::error::HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(diagnostic.location.range.clone()),
text: Some(name.clone()),
})
.collect();
return Some(diagnostic);
}
if let Some((scalar_name, schema_name)) = invalid_scalar_base_names(error) {
let available_scalar_names = scalar_declaration_names(collection.clone(), scalar_name.as_str());
let declaration_spans = declaration_spans_for_collection(collection);
let scalar_span = declaration_spans.get(scalar_name.as_str()).copied()?;
let schema_span = declaration_spans.get(schema_name.as_str()).copied()?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
preprocessed.as_str(),
scalar_span,
schema_name.as_str(),
)
.unwrap_or_else(|| diagnostic.location.range.clone());
diagnostic.related_information = vec![HenDiagnosticRelatedInformation {
message: format!("Schema '{}' is declared here.", schema_name),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: diagnostic_range_from_source_span(preprocessed.as_str(), schema_span)?,
},
}];
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: "scalar".to_string(),
name: scalar_name.to_string(),
role: "declaration".to_string(),
});
if !available_scalar_names.is_empty() {
add_available_names_data(&mut diagnostic, &available_scalar_names);
diagnostic.suggestions = available_scalar_names
.iter()
.take(3)
.map(|name| crate::error::HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(diagnostic.location.range.clone()),
text: Some(name.clone()),
})
.collect();
}
if let Some(serde_json::Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("expectedKinds".to_string(), json!(["scalar"]));
map.insert("referencedSchema".to_string(), json!(schema_name));
} else {
diagnostic.data = Some(json!({
"expectedKinds": ["scalar"],
"referencedSchema": schema_name,
}));
}
return Some(diagnostic);
}
let cycle_path = schema_cycle_names(error)?;
let declaration_spans = declaration_spans_for_collection(collection);
let primary_name = cycle_path.first()?;
let primary_span = declaration_spans.get(primary_name.as_str()).copied()?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = diagnostic_range_from_source_span(preprocessed.as_str(), primary_span)?;
diagnostic.related_information = schema_cycle_related_information(
cycle_path.as_slice(),
&declaration_spans,
preprocessed.as_str(),
path,
)?;
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: "schema".to_string(),
name: primary_name.clone(),
role: "declaration".to_string(),
});
diagnostic.data = Some(json!({
"cyclePath": cycle_path,
}));
Some(diagnostic)
}
pub(super) fn validate_schema_registry(
schema_registry: &SchemaRegistry,
source: &str,
declaration_spans: &DeclarationSpanIndex,
) -> Result<(), pest::error::Error<Rule>> {
schema_registry.validate_references().map_err(|error| {
let span = span_for_schema_registry_error(&error, source, declaration_spans);
schema_registry_error_to_span(error, span)
})
}
pub(super) fn parse_request_collection(
source: &str,
) -> Result<pest::iterators::Pair<'_, Rule>, pest::error::Error<Rule>> {
if let Ok(mut pairs) = CollectionParser::parse(Rule::request_collection_with_preamble, source)
{
return Ok(pairs.next().unwrap());
}
if let Ok(mut pairs) = CollectionParser::parse(Rule::request_collection_preamble_only, source)
{
return Ok(pairs.next().unwrap());
}
let mut pairs = CollectionParser::parse(Rule::request_collection_without_preamble, source)
.map_err(|error| rewrite_request_collection_error(source, error))?;
Ok(pairs.next().unwrap())
}
fn rewrite_request_collection_error(
source: &str,
error: pest::error::Error<Rule>,
) -> pest::error::Error<Rule> {
if let Some(span) = misplaced_declaration_span(source, &error) {
return pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "schema and scalar declarations must appear before the first ---"
.to_string(),
},
span.into_pest_span(source),
);
}
error
}
fn misplaced_declaration_span(
source: &str,
error: &pest::error::Error<Rule>,
) -> Option<SourceSpan> {
let line_number = error_primary_line(error)?;
let line_text = source.lines().nth(line_number.checked_sub(1)?)?;
if !looks_like_declaration(line_text) {
return None;
}
line_span(source, line_number)
}
fn line_span(source: &str, line_number: usize) -> Option<SourceSpan> {
if line_number == 0 {
return None;
}
let mut current_line = 1;
let mut start = 0;
for (index, ch) in source.char_indices() {
if current_line == line_number {
break;
}
if ch == '\n' {
current_line += 1;
start = index + 1;
}
}
if current_line != line_number {
return None;
}
let end = source[start..]
.find('\n')
.map(|offset| start + offset)
.unwrap_or(source.len());
Some(SourceSpan { start, end })
}
fn misplaced_declaration_diagnostic(
source: &str,
error: &pest::error::Error<Rule>,
path: Option<&Path>,
) -> Option<HenDiagnostic> {
if !is_misplaced_declaration_error(error) {
return None;
}
let declaration_span = misplaced_declaration_span(source, error)?;
let declaration_source = &source[declaration_span.start..declaration_span.end];
let (declaration_kind, declaration_name) = declaration_name_and_kind_from_line(declaration_source)?;
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.location.range = reference_range_in_source_span(
source,
declaration_span,
declaration_name.as_str(),
)
.or_else(|| diagnostic_range_from_source_span(source, declaration_span))?;
diagnostic.related_information = vec![HenDiagnosticRelatedInformation {
message: "Requests begin here.".to_string(),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: diagnostic_range_from_source_span(source, first_request_separator_span(source)?)?,
},
}];
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: declaration_kind.label().to_string(),
name: declaration_name.clone(),
role: "declaration".to_string(),
});
diagnostic.data = Some(json!({
"declarationName": declaration_name,
"afterRequestsBegin": true,
}));
Some(diagnostic)
}
fn scalar_declaration_authoring_diagnostic(
source: &str,
error: &pest::error::Error<Rule>,
path: Option<&Path>,
) -> Option<HenDiagnostic> {
let reason = scalar_declaration_authoring_reason(error)?;
let declaration_span = line_span(source, error_primary_line(error)?)?;
let declaration_source = &source[declaration_span.start..declaration_span.end];
let (declaration_kind, declaration_name) = declaration_name_and_kind_from_line(declaration_source)?;
if declaration_kind != DeclarationSymbolKind::Scalar {
return None;
}
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
diagnostic.symbol = Some(HenDiagnosticSymbol {
kind: declaration_kind.label().to_string(),
name: declaration_name.clone(),
role: "declaration".to_string(),
});
if let Some(serde_json::Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("declarationName".to_string(), json!(declaration_name));
map.insert("reason".to_string(), json!(reason));
} else {
diagnostic.data = Some(json!({
"declarationName": declaration_name,
"reason": reason,
}));
}
Some(diagnostic)
}
pub(super) fn remember_declaration_span(
declaration_spans: &mut DeclarationSpanIndex,
pair: &pest::iterators::Pair<Rule>,
) {
let inner = pair.clone().into_inner().next().unwrap();
let span = SourceSpan::from_pest_span(inner.as_span());
let name = inner.into_inner().next().unwrap().as_str().to_string();
declaration_spans.insert(name, span);
}
fn span_for_schema_registry_error<'a>(
error: &SchemaRegistryError,
source: &'a str,
declaration_spans: &DeclarationSpanIndex,
) -> pest::Span<'a> {
let span = match error {
SchemaRegistryError::ReservedName(name) | SchemaRegistryError::DuplicateName(name) => {
declaration_spans.get(name).copied()
}
SchemaRegistryError::UnknownReference { owner, .. } => {
declaration_spans.get(owner).copied()
}
SchemaRegistryError::InvalidScalarBaseReference { scalar, .. } => {
declaration_spans.get(scalar).copied()
}
SchemaRegistryError::CircularReference(path) => path
.iter()
.find_map(|name| declaration_spans.get(name).copied()),
};
span.map(|span| span.into_pest_span(source))
.unwrap_or_else(|| pest::Span::new(source, 0, source.len()).unwrap())
}
pub(super) fn reject_misplaced_declaration(
raw: &str,
span: pest::Span<'_>,
) -> Result<(), pest::error::Error<Rule>> {
if looks_like_declaration(raw) {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "schema and scalar declarations must appear before the first ---"
.to_string(),
},
span,
));
}
Ok(())
}
fn looks_like_declaration(raw: &str) -> bool {
let trimmed = raw.trim();
looks_like_scalar_declaration(trimmed) || looks_like_schema_declaration(trimmed)
}
fn is_misplaced_declaration_error(error: &pest::error::Error<Rule>) -> bool {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return false,
};
message == "schema and scalar declarations must appear before the first ---"
}
fn scalar_declaration_authoring_reason(error: &pest::error::Error<Rule>) -> Option<&'static str> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
match message {
"scalar expressions can only declare one base type" => Some("multipleBaseTypes"),
"scalar declaration must define a base type or predicate" => {
Some("missingBaseOrPredicate")
}
_ if message.starts_with("invalid ") && message.ends_with("() predicate") => {
Some("invalidPredicate")
}
_ if message.contains("() requires min..max syntax") => Some("invalidBoundsSyntax"),
_ if message.contains("() requires at least one bound") => Some("missingBounds"),
_ if message == "range() bounds must be numbers" => Some("invalidRangeBounds"),
_ => None,
}
}
fn declaration_name_and_kind_from_line(raw: &str) -> Option<(DeclarationSymbolKind, String)> {
let trimmed = raw.trim();
if let Some(remainder) = trimmed.strip_prefix("scalar ") {
let name = leading_identifier(remainder)?;
return Some((DeclarationSymbolKind::Scalar, name.to_string()));
}
if let Some(remainder) = trimmed.strip_prefix("schema ") {
let name = leading_identifier(remainder)?;
return Some((DeclarationSymbolKind::Schema, name.to_string()));
}
None
}
fn first_request_separator_span(source: &str) -> Option<SourceSpan> {
source
.lines()
.enumerate()
.find_map(|(index, line)| (line.trim() == "---").then_some(index + 1))
.and_then(|line_number| line_span(source, line_number))
}
fn error_primary_line(error: &pest::error::Error<Rule>) -> Option<usize> {
let line_number = match error.line_col {
pest::error::LineColLocation::Pos((line, _)) => line,
pest::error::LineColLocation::Span((line, _), _) => line,
};
(line_number > 0).then_some(line_number)
}
fn leading_identifier(raw: &str) -> Option<&str> {
let trimmed = raw.trim_start();
let identifier_len = trimmed
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.count();
(identifier_len > 0).then_some(&trimmed[..identifier_len])
}
fn duplicate_declaration_name(error: &pest::error::Error<Rule>) -> Option<&str> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
message.strip_suffix(" is already defined")
}
fn duplicate_environment_name(error: &pest::error::Error<Rule>) -> Option<&str> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
message
.strip_prefix("Duplicate environment '")
.and_then(|value| value.strip_suffix("' declared."))
}
fn duplicate_oauth_profile_name(error: &pest::error::Error<Rule>) -> Option<&str> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
message
.strip_prefix("Duplicate OAuth profile '")
.and_then(|value| value.strip_suffix("' declared."))
}
fn unknown_environment_variable_details(
error: &pest::error::Error<Rule>,
) -> Option<(String, String)> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
let remainder = message.strip_prefix("Environment '")?;
let (environment, remainder) = remainder.split_once("' overrides unknown or non-scalar variable '")?;
let variable = remainder.strip_suffix("'.")?;
Some((environment.to_string(), variable.to_string()))
}
fn reserved_declaration_name(error: &pest::error::Error<Rule>) -> Option<&str> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
message.strip_suffix(" is reserved and cannot be redefined")
}
fn schema_cycle_names(error: &pest::error::Error<Rule>) -> Option<Vec<String>> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
let cycle = message
.strip_prefix("schema declarations contain a circular reference: ")?
.trim_end_matches('.');
Some(cycle.split(" -> ").map(str::to_string).collect())
}
fn unknown_reference_names(error: &pest::error::Error<Rule>) -> Option<(String, String)> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
let (owner, reference) = message.split_once(" references unknown validation target ")?;
Some((owner.to_string(), reference.to_string()))
}
fn invalid_scalar_base_names(error: &pest::error::Error<Rule>) -> Option<(String, String)> {
let message = match &error.variant {
pest::error::ErrorVariant::CustomError { message } => message.as_str(),
pest::error::ErrorVariant::ParsingError { .. } => return None,
};
let remainder = message.strip_prefix("scalar ")?;
let (scalar, remainder) = remainder.split_once(" cannot use schema ")?;
let schema = remainder.strip_suffix(" as a scalar base")?;
Some((scalar.to_string(), schema.to_string()))
}
fn duplicate_declaration_spans(
collection: pest::iterators::Pair<'_, Rule>,
duplicate_name: &str,
) -> Option<(DeclarationSymbolKind, SourceSpan, SourceSpan)> {
let mut original_span = None;
for pair in collection.into_inner() {
if pair.as_rule() != Rule::declaration {
continue;
}
let inner = pair.into_inner().next().unwrap();
let kind = declaration_symbol_kind(inner.as_rule())?;
let span = SourceSpan::from_pest_span(inner.as_span());
let name = inner.into_inner().next().unwrap().as_str();
if name != duplicate_name {
continue;
}
if let Some((_, first_span)) = original_span {
return Some((kind, first_span, span));
}
original_span = Some((kind, span));
}
None
}
fn duplicate_environment_spans(
collection: pest::iterators::Pair<'_, Rule>,
duplicate_name: &str,
) -> Option<(SourceSpan, SourceSpan)> {
let mut original_span = None;
for pair in collection.into_inner() {
if pair.as_rule() != Rule::environment_block {
continue;
}
let span = SourceSpan::from_pest_span(pair.as_span());
let name = pair
.clone()
.into_inner()
.next()
.map(|value| value.as_str().trim().to_string())?;
if name != duplicate_name {
continue;
}
if let Some(first_span) = original_span {
return Some((first_span, span));
}
original_span = Some(span);
}
None
}
fn duplicate_oauth_profile_spans(
collection: pest::iterators::Pair<'_, Rule>,
duplicate_name: &str,
) -> Option<(SourceSpan, SourceSpan)> {
let mut original_span = None;
for pair in collection.into_inner() {
if pair.as_rule() != Rule::oauth_block {
continue;
}
let span = SourceSpan::from_pest_span(pair.as_span());
let name = pair
.clone()
.into_inner()
.next()
.map(|value| value.as_str().trim().to_string())?;
if name != duplicate_name {
continue;
}
if let Some(first_span) = original_span {
return Some((first_span, span));
}
original_span = Some(span);
}
None
}
fn declaration_span_and_kind(
collection: pest::iterators::Pair<'_, Rule>,
name: &str,
) -> Option<(DeclarationSymbolKind, SourceSpan)> {
for pair in collection.into_inner() {
if pair.as_rule() != Rule::declaration {
continue;
}
let inner = pair.into_inner().next().unwrap();
let kind = declaration_symbol_kind(inner.as_rule())?;
let span = SourceSpan::from_pest_span(inner.as_span());
let declaration_name = inner.into_inner().next().unwrap().as_str();
if declaration_name == name {
return Some((kind, span));
}
}
None
}
fn declaration_symbol_kind(rule: Rule) -> Option<DeclarationSymbolKind> {
match rule {
Rule::scalar_declaration => Some(DeclarationSymbolKind::Scalar),
Rule::schema_object_declaration | Rule::schema_assignment_declaration => {
Some(DeclarationSymbolKind::Schema)
}
_ => None,
}
}
fn declaration_spans_for_collection(
collection: pest::iterators::Pair<'_, Rule>,
) -> DeclarationSpanIndex {
let mut declaration_spans = DeclarationSpanIndex::new();
for pair in collection.into_inner() {
if pair.as_rule() == Rule::declaration {
remember_declaration_span(&mut declaration_spans, &pair);
}
}
declaration_spans
}
fn declaration_names(collection: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::declaration {
continue;
}
let Some(name) = pair
.into_inner()
.next()
.and_then(|value| value.into_inner().next())
.map(|value| value.as_str().trim().to_string())
else {
continue;
};
names.push(name);
}
names.sort();
names.dedup();
names
}
fn global_variable_names(collection: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::variable {
continue;
}
let Some(name) = pair
.into_inner()
.next()
.map(|value| value.as_str().trim().to_string())
else {
continue;
};
names.push(name);
}
names.sort();
names.dedup();
names
}
fn scalar_declaration_names(
collection: pest::iterators::Pair<'_, Rule>,
exclude_name: &str,
) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::declaration {
continue;
}
let mut declaration = pair.into_inner();
let Some(inner) = declaration.next() else {
continue;
};
if inner.as_rule() != Rule::scalar_declaration {
continue;
}
let Some(name) = inner
.into_inner()
.next()
.map(|value| value.as_str().trim().to_string())
else {
continue;
};
if name != exclude_name {
names.push(name);
}
}
names.sort();
names.dedup();
names
}
fn add_available_names_data(diagnostic: &mut HenDiagnostic, available_names: &[String]) {
match diagnostic.data.as_mut() {
Some(serde_json::Value::Object(map)) => {
map.insert("availableNames".to_string(), json!(available_names));
}
_ => {
diagnostic.data = Some(json!({
"availableNames": available_names,
}));
}
}
}
fn reference_range_in_source_span(
source: &str,
declaration_span: SourceSpan,
reference: &str,
) -> Option<HenDiagnosticRange> {
let declaration_source = &source[declaration_span.start..declaration_span.end];
for (offset, _) in declaration_source.match_indices(reference) {
let before = declaration_source[..offset].chars().next_back();
let after = declaration_source[offset + reference.len()..].chars().next();
if !is_identifier_boundary(before) || !is_identifier_boundary(after) {
continue;
}
let absolute_start = declaration_span.start + offset;
let absolute_end = absolute_start + reference.len();
return diagnostic_range_from_source_span(
source,
SourceSpan {
start: absolute_start,
end: absolute_end,
},
);
}
None
}
fn is_identifier_boundary(value: Option<char>) -> bool {
value.is_none_or(|ch| !(ch.is_ascii_alphanumeric() || ch == '_'))
}
fn schema_cycle_related_information(
cycle_path: &[String],
declaration_spans: &DeclarationSpanIndex,
source: &str,
path: Option<&Path>,
) -> Option<Vec<HenDiagnosticRelatedInformation>> {
let related_names = match (cycle_path.first(), cycle_path.last()) {
(Some(first), Some(last)) if cycle_path.len() > 1 && first == last => {
&cycle_path[1..cycle_path.len() - 1]
}
_ => &cycle_path[1..],
};
related_names
.iter()
.map(|name| {
let span = declaration_spans.get(name.as_str()).copied()?;
Some(HenDiagnosticRelatedInformation {
message: format!("Schema '{}' participates in the cycle here.", name),
location: HenDiagnosticLocation {
path: path.map(|value| value.display().to_string()),
range: diagnostic_range_from_source_span(source, span)?,
},
})
})
.collect()
}
fn diagnostic_range_from_source_span(
source: &str,
span: SourceSpan,
) -> Option<HenDiagnosticRange> {
let span = pest::Span::new(source, span.start, span.end)?;
let (start_line, start_character) = span.start_pos().line_col();
let (end_line, end_character) = span.end_pos().line_col();
Some(ensure_non_empty_diagnostic_range(HenDiagnosticRange {
start: zero_based_position(start_line, start_character),
end: zero_based_position(end_line, end_character),
}))
}
fn zero_based_position(line: usize, character: usize) -> HenDiagnosticPosition {
HenDiagnosticPosition {
line: line.saturating_sub(1),
character: character.saturating_sub(1),
}
}
fn ensure_non_empty_diagnostic_range(mut range: HenDiagnosticRange) -> HenDiagnosticRange {
if range.end.line < range.start.line
|| (range.end.line == range.start.line && range.end.character <= range.start.character)
{
range.end.line = range.start.line;
range.end.character = range.start.character.saturating_add(1);
}
range
}
fn looks_like_scalar_declaration(raw: &str) -> bool {
let Some(remainder) = raw.strip_prefix("scalar ") else {
return false;
};
let remainder = remainder.trim_start();
let identifier_len = remainder
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.count();
identifier_len > 0 && remainder[identifier_len..].trim_start().starts_with('=')
}
fn looks_like_schema_declaration(raw: &str) -> bool {
let Some(remainder) = raw.strip_prefix("schema ") else {
return false;
};
let remainder = remainder.trim_start();
let identifier_len = remainder
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.count();
if identifier_len == 0 {
return false;
}
matches!(
remainder[identifier_len..].trim_start().chars().next(),
Some('{') | Some('=')
)
}