use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::{self, Display, Write};
use std::iter;
use std::ops::{Deref, DerefMut};
use either::Either;
use lalrpop_util as lalr;
use lazy_static::lazy_static;
use miette::{Diagnostic, LabeledSpan, SourceSpan};
use nonempty::NonEmpty;
use smol_str::SmolStr;
use thiserror::Error;
use crate::ast::{self, ReservedNameError};
use crate::parser::fmt::join_with_conjunction;
use crate::parser::loc::Loc;
use crate::parser::node::Node;
use crate::parser::unescape::UnescapeError;
use super::cst;
pub(crate) type RawLocation = usize;
pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
pub(crate) type RawUserError = Node<String>;
pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, RawUserError>;
pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, RawUserError>;
type OwnedRawParseError = lalr::ParseError<RawLocation, String, RawUserError>;
#[derive(Clone, Debug, Diagnostic, Error, PartialEq, Eq)]
pub enum ParseError {
#[error(transparent)]
#[diagnostic(transparent)]
ToCST(#[from] ToCSTError),
#[error(transparent)]
#[diagnostic(transparent)]
ToAST(#[from] ToASTError),
}
#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq)]
pub enum LiteralParseError {
#[error(transparent)]
#[diagnostic(transparent)]
Parse(#[from] ParseErrors),
#[error("invalid literal: {0}")]
InvalidLiteral(ast::Expr),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{kind}")]
pub struct ToASTError {
kind: ToASTErrorKind,
loc: Loc,
}
impl Diagnostic for ToASTError {
impl_diagnostic_from_source_loc_field!(loc);
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.kind.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.url()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.kind.diagnostic_source()
}
}
impl ToASTError {
pub fn new(kind: ToASTErrorKind, loc: Loc) -> Self {
Self { kind, loc }
}
pub fn kind(&self) -> &ToASTErrorKind {
&self.kind
}
pub(crate) fn source_loc(&self) -> &Loc {
&self.loc
}
}
const POLICY_SCOPE_HELP: &str =
"policy scopes must contain a `principal`, `action`, and `resource` element in that order";
#[derive(Debug, Diagnostic, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToASTErrorKind {
#[error("a template with id `{0}` already exists in the policy set")]
DuplicateTemplateId(ast::PolicyID),
#[error("a policy with id `{0}` already exists in the policy set")]
DuplicatePolicyId(ast::PolicyID),
#[error(transparent)]
#[diagnostic(transparent)]
ExpectedStaticPolicy(#[from] parse_errors::ExpectedStaticPolicy),
#[error(transparent)]
#[diagnostic(transparent)]
ExpectedTemplate(#[from] parse_errors::ExpectedTemplate),
#[error("duplicate annotation: @{0}")]
DuplicateAnnotation(ast::AnyId),
#[error(transparent)]
#[diagnostic(transparent)]
SlotsInConditionClause(#[from] parse_errors::SlotsInConditionClause),
#[error("this policy is missing the `{0}` variable in the scope")]
#[diagnostic(help("{POLICY_SCOPE_HELP}"))]
MissingScopeVariable(ast::Var),
#[error("this policy has an extra element in the scope: {0}")]
#[diagnostic(help("{POLICY_SCOPE_HELP}"))]
ExtraScopeElement(cst::VariableDef),
#[error("this identifier is reserved and cannot be used: {0}")]
ReservedIdentifier(cst::Ident),
#[error("invalid identifier: {0}")]
InvalidIdentifier(String),
#[error("'=' is not a valid operator in Cedar")]
#[diagnostic(help("try using '==' instead"))]
InvalidSingleEq,
#[error("invalid policy effect: {0}")]
#[diagnostic(help("effect must be either `permit` or `forbid`"))]
InvalidEffect(cst::Ident),
#[error("invalid policy condition: {0}")]
#[diagnostic(help("condition must be either `when` or `unless`"))]
InvalidCondition(cst::Ident),
#[error("found an invalid variable in the policy scope: {0}")]
#[diagnostic(help("{POLICY_SCOPE_HELP}"))]
InvalidScopeVariable(cst::Ident),
#[error("found the variable `{got}` where the variable `{expected}` must be used")]
#[diagnostic(help("{POLICY_SCOPE_HELP}"))]
IncorrectVariable {
expected: ast::Var,
got: ast::Var,
},
#[error("invalid operator in the policy scope: {0}")]
#[diagnostic(help("policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"))]
InvalidScopeOperator(cst::RelOp),
#[error("invalid operator in the action scope: {0}")]
#[diagnostic(help("action scope clauses can only use `==` or `in`"))]
InvalidActionScopeOperator(cst::RelOp),
#[error("`is` cannot appear in the action scope")]
#[diagnostic(help("try moving `action is ..` into a `when` condition"))]
IsInActionScope,
#[error("`is` cannot be used together with `==`")]
#[diagnostic(help("try using `_ is _ in _`"))]
IsWithEq,
#[error(transparent)]
#[diagnostic(transparent)]
InvalidActionType(#[from] parse_errors::InvalidActionType),
#[error("{}condition clause cannot be empty", match .0 { Some(ident) => format!("`{}` ", ident), None => "".to_string() })]
EmptyClause(Option<cst::Ident>),
#[error("internal invariant violated. Membership chain did not resolve to an expression")]
#[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
MembershipInvariantViolation,
#[error("invalid string literal: {0}")]
InvalidString(String),
#[error("invalid variable: {0}")]
#[diagnostic(help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `{0}` in quotes to make a string?"))]
ArbitraryVariable(SmolStr),
#[error("invalid attribute name: {0}")]
#[diagnostic(help("attribute names can either be identifiers or string literals"))]
InvalidAttribute(SmolStr),
#[error("`{0}` cannot be used as an attribute as it contains a namespace")]
PathAsAttribute(String),
#[error("`{0}` is a method, not a function")]
#[diagnostic(help("use a method-style call `e.{0}(..)`"))]
FunctionCallOnMethod(ast::UnreservedId),
#[error("`{0}` is a function, not a method")]
#[diagnostic(help("use a function-style call `{0}(..)`"))]
MethodCallOnFunction(ast::UnreservedId),
#[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
InvalidPattern(String),
#[error("right hand side of an `is` expression must be an entity type name, but got `{0}`")]
#[diagnostic(help("try using `==` to test for equality"))]
InvalidIsType(String),
#[error("expected {expected}, found {got}")]
WrongNode {
expected: &'static str,
got: String,
#[help]
suggestion: Option<String>,
},
#[error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")]
AmbiguousOperators,
#[error("division is not supported")]
UnsupportedDivision,
#[error("remainder/modulo is not supported")]
UnsupportedModulo,
#[error(transparent)]
#[diagnostic(transparent)]
ExpressionConstructionError(#[from] ast::ExpressionConstructionError),
#[error("integer literal `{0}` is too large")]
#[diagnostic(help("maximum allowed integer literal is `{}`", ast::InputInteger::MAX))]
IntegerLiteralTooLarge(u64),
#[error("too many occurrences of `{0}`")]
#[diagnostic(help("cannot chain more the 4 applications of a unary operator"))]
UnaryOpLimit(ast::UnaryOp),
#[error("`{0}(...)` is not a valid function call")]
#[diagnostic(help("variables cannot be called as functions"))]
VariableCall(ast::Var),
#[error("attempted to call `{0}.{1}(...)`, but `{0}` does not have any methods")]
NoMethods(ast::Name, ast::UnreservedId),
#[error("`{id}` is not a valid method")]
UnknownMethod {
id: ast::UnreservedId,
#[help]
hint: Option<String>,
},
#[error("`{id}` is not a valid function")]
UnknownFunction {
id: ast::Name,
#[help]
hint: Option<String>,
},
#[error("invalid entity literal: {0}")]
#[diagnostic(help("entity literals should have a form like `Namespace::User::\"alice\"`"))]
InvalidEntityLiteral(String),
#[error("function calls must be of the form `<name>(arg1, arg2, ...)`")]
ExpressionCall,
#[error("invalid member access `{0}.{1}`, `{0}` has no fields or methods")]
InvalidAccess(ast::Name, SmolStr),
#[error("invalid indexing expression `{0}[\"{}\"]`, `{0}` has no fields", .1.escape_debug())]
InvalidIndex(ast::Name, SmolStr),
#[error("the contents of an index expression must be a string literal")]
NonStringIndex,
#[error("type constraints using `:` are not supported")]
#[diagnostic(help("try using `is` instead"))]
TypeConstraints,
#[error("`{kind}` needs to be normalized (e.g., whitespace removed): {src}")]
#[diagnostic(help("the normalized form is `{normalized_src}`"))]
NonNormalizedString {
kind: &'static str,
src: String,
normalized_src: String,
},
#[error("internal invariant violated. Parsed data node should not be empty")]
#[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
EmptyNodeInvariantViolation,
#[error("call to `{name}` requires exactly {expected} argument{}, but got {got} argument{}", if .expected == &1 { "" } else { "s" }, if .got == &1 { "" } else { "s" })]
WrongArity {
name: &'static str,
expected: usize,
got: usize,
},
#[error(transparent)]
#[diagnostic(transparent)]
Unescape(#[from] UnescapeError),
#[error(transparent)]
#[diagnostic(transparent)]
WrongEntityArgument(#[from] parse_errors::WrongEntityArgument),
#[error("`{0}` is not a valid template slot")]
#[diagnostic(help("a template slot may only be `?principal` or `?resource`"))]
InvalidSlot(SmolStr),
#[error(transparent)]
#[diagnostic(transparent)]
ReservedNamespace(#[from] ReservedNameError),
#[error("when `is` and `in` are used together, `is` must come first")]
#[diagnostic(help("try `_ is _ in _`"))]
InvertedIsIn,
}
impl ToASTErrorKind {
pub fn wrong_node(
expected: &'static str,
got: impl Into<String>,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::WrongNode {
expected,
got: got.into(),
suggestion: suggestion.map(Into::into),
}
}
pub fn wrong_arity(name: &'static str, expected: usize, got: usize) -> Self {
Self::WrongArity {
name,
expected,
got,
}
}
pub fn slots_in_condition_clause(slot: ast::Slot, clause_type: &'static str) -> Self {
parse_errors::SlotsInConditionClause { slot, clause_type }.into()
}
pub fn expected_static_policy(slot: ast::Slot) -> Self {
parse_errors::ExpectedStaticPolicy { slot }.into()
}
pub fn expected_template() -> Self {
parse_errors::ExpectedTemplate::new().into()
}
pub fn wrong_entity_argument_one_expected(
expected: parse_errors::Ref,
got: parse_errors::Ref,
) -> Self {
parse_errors::WrongEntityArgument {
expected: Either::Left(expected),
got,
}
.into()
}
pub fn wrong_entity_argument_two_expected(
r1: parse_errors::Ref,
r2: parse_errors::Ref,
got: parse_errors::Ref,
) -> Self {
let expected = Either::Right((r1, r2));
parse_errors::WrongEntityArgument { expected, got }.into()
}
}
pub mod parse_errors {
use std::sync::Arc;
use super::*;
#[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
#[error("expected a static policy, got a template containing the slot {}", slot.id)]
#[diagnostic(help("try removing the template slot(s) from this policy"))]
pub struct ExpectedStaticPolicy {
pub(crate) slot: ast::Slot,
}
impl From<ast::UnexpectedSlotError> for ExpectedStaticPolicy {
fn from(err: ast::UnexpectedSlotError) -> Self {
match err {
ast::UnexpectedSlotError::FoundSlot(slot) => Self { slot },
}
}
}
#[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
#[error("expected a template, got a static policy")]
#[diagnostic(help("a template should include slot(s) `?principal` or `?resource`"))]
pub struct ExpectedTemplate {
_dummy: (),
}
impl ExpectedTemplate {
pub(crate) fn new() -> Self {
Self { _dummy: () }
}
}
#[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
#[error("found template slot {} in a `{clause_type}` clause", slot.id)]
#[diagnostic(help("slots are currently unsupported in `{clause_type}` clauses"))]
pub struct SlotsInConditionClause {
pub(crate) slot: ast::Slot,
pub(crate) clause_type: &'static str,
}
#[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
#[diagnostic(help("action entities must have type `Action`, optionally in a namespace"))]
pub struct InvalidActionType {
pub(crate) euids: NonEmpty<Arc<ast::EntityUID>>,
}
impl std::fmt::Display for InvalidActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let subject = if self.euids.len() > 1 {
"entity uids"
} else {
"an entity uid"
};
write!(f, "expected {subject} with type `Action` but got ")?;
join_with_conjunction(f, "and", self.euids.iter(), |f, e| write!(f, "`{e}`"))
}
}
#[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
#[error("expected {}, found {got}", match .expected { Either::Left(r) => r.to_string(), Either::Right((r1, r2)) => format!("{r1} or {r2}") })]
pub struct WrongEntityArgument {
pub(crate) expected: Either<Ref, (Ref, Ref)>,
pub(crate) got: Ref,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ref {
Single,
Set,
Template,
}
impl std::fmt::Display for Ref {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ref::Single => write!(f, "single entity uid"),
Ref::Template => write!(f, "template slot"),
Ref::Set => write!(f, "set of entity uids"),
}
}
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub struct ToCSTError {
err: OwnedRawParseError,
}
impl ToCSTError {
pub fn primary_source_span(&self) -> SourceSpan {
match &self.err {
OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
OwnedRawParseError::UnrecognizedToken {
token: (token_start, _, token_end),
..
} => SourceSpan::from(*token_start..*token_end),
OwnedRawParseError::ExtraToken {
token: (token_start, _, token_end),
} => SourceSpan::from(*token_start..*token_end),
OwnedRawParseError::User { error } => error.loc.span,
}
}
pub(crate) fn from_raw_parse_err(err: RawParseError<'_>) -> Self {
Self {
err: err.map_token(|token| token.to_string()),
}
}
pub(crate) fn from_raw_err_recovery(recovery: RawErrorRecovery<'_>) -> Self {
Self::from_raw_parse_err(recovery.error)
}
}
impl Display for ToCSTError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.err {
OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
OwnedRawParseError::UnrecognizedToken {
token: (_, token, _),
..
} => write!(f, "unexpected token `{token}`"),
OwnedRawParseError::ExtraToken {
token: (_, token, _),
..
} => write!(f, "extra token `{token}`"),
OwnedRawParseError::User { error } => write!(f, "{error}"),
}
}
}
impl Diagnostic for ToCSTError {
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let primary_source_span = self.primary_source_span();
let labeled_span = match &self.err {
OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span),
OwnedRawParseError::UnrecognizedEof { expected, .. } => LabeledSpan::new_with_span(
expected_to_string(expected, &POLICY_TOKEN_CONFIG),
primary_source_span,
),
OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span(
expected_to_string(expected, &POLICY_TOKEN_CONFIG),
primary_source_span,
),
OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span),
OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
};
Some(Box::new(iter::once(labeled_span)))
}
}
#[derive(Debug)]
pub struct ExpectedTokenConfig {
pub friendly_token_names: HashMap<&'static str, &'static str>,
pub impossible_tokens: HashSet<&'static str>,
pub special_identifier_tokens: HashSet<&'static str>,
pub identifier_sentinel: &'static str,
pub first_set_identifier_tokens: HashSet<&'static str>,
pub first_set_sentinel: &'static str,
}
lazy_static! {
static ref POLICY_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig {
friendly_token_names: HashMap::from([
("TRUE", "`true`"),
("FALSE", "`false`"),
("IF", "`if`"),
("PERMIT", "`permit`"),
("FORBID", "`forbid`"),
("WHEN", "`when`"),
("UNLESS", "`unless`"),
("IN", "`in`"),
("HAS", "`has`"),
("LIKE", "`like`"),
("IS", "`is`"),
("THEN", "`then`"),
("ELSE", "`else`"),
("PRINCIPAL", "`principal`"),
("ACTION", "`action`"),
("RESOURCE", "`resource`"),
("CONTEXT", "`context`"),
("PRINCIPAL_SLOT", "`?principal`"),
("RESOURCE_SLOT", "`?resource`"),
("IDENTIFIER", "identifier"),
("NUMBER", "number"),
("STRINGLIT", "string literal"),
]),
impossible_tokens: HashSet::from(["\"=\"", "\"%\"", "\"/\"", "OTHER_SLOT"]),
special_identifier_tokens: HashSet::from([
"PERMIT",
"FORBID",
"WHEN",
"UNLESS",
"IN",
"HAS",
"LIKE",
"IS",
"THEN",
"ELSE",
"PRINCIPAL",
"ACTION",
"RESOURCE",
"CONTEXT",
]),
identifier_sentinel: "IDENTIFIER",
first_set_identifier_tokens: HashSet::from(["TRUE", "FALSE", "IF"]),
first_set_sentinel: "\"!\"",
};
}
pub fn expected_to_string(expected: &[String], config: &ExpectedTokenConfig) -> Option<String> {
let mut expected = expected
.iter()
.filter(|e| !config.impossible_tokens.contains(e.as_str()))
.map(|e| e.as_str())
.collect::<BTreeSet<_>>();
if expected.contains(config.identifier_sentinel) {
for token in config.special_identifier_tokens.iter() {
expected.remove(*token);
}
if !expected.contains(config.first_set_sentinel) {
for token in config.first_set_identifier_tokens.iter() {
expected.remove(*token);
}
}
}
if expected.is_empty() {
return None;
}
let mut expected_string = "expected ".to_owned();
#[allow(clippy::expect_used)]
join_with_conjunction(
&mut expected_string,
"or",
expected,
|f, token| match config.friendly_token_names.get(token) {
Some(friendly_token_name) => write!(f, "{}", friendly_token_name),
None => write!(f, "{}", token.replace('"', "`")),
},
)
.expect("failed to format expected tokens");
Some(expected_string)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseErrors(NonEmpty<ParseError>);
impl ParseErrors {
pub(crate) fn singleton(err: impl Into<ParseError>) -> Self {
Self(NonEmpty::singleton(err.into()))
}
pub(crate) fn new(first: ParseError, rest: impl IntoIterator<Item = ParseError>) -> Self {
Self(NonEmpty {
head: first,
tail: rest.into_iter().collect::<Vec<_>>(),
})
}
pub(crate) fn new_from_nonempty(errs: NonEmpty<ParseError>) -> Self {
Self(errs)
}
pub(crate) fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
NonEmpty::collect(i).map(Self::new_from_nonempty)
}
pub(crate) fn flatten(v: Vec<ParseErrors>) -> Option<Self> {
let (first, rest) = v.split_first()?;
let mut first = first.clone();
rest.iter()
.for_each(|errs| first.extend(errs.iter().cloned()));
Some(first)
}
pub(crate) fn transpose<T>(
i: impl IntoIterator<Item = Result<T, ParseErrors>>,
) -> Result<Vec<T>, Self> {
let mut errs = vec![];
let oks: Vec<_> = i
.into_iter()
.filter_map(|r| r.map_err(|e| errs.push(e)).ok())
.collect();
if let Some(combined_errs) = Self::flatten(errs) {
Err(combined_errs)
} else {
Ok(oks)
}
}
}
impl Display for ParseErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.first()) }
}
impl std::error::Error for ParseErrors {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.first().source()
}
#[allow(deprecated)]
fn description(&self) -> &str {
self.first().description()
}
#[allow(deprecated)]
fn cause(&self) -> Option<&dyn std::error::Error> {
self.first().cause()
}
}
impl Diagnostic for ParseErrors {
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
errs.next().map(move |first_err| match first_err.related() {
Some(first_err_related) => Box::new(first_err_related.chain(errs)),
None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
})
}
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().code()
}
fn severity(&self) -> Option<miette::Severity> {
self.first().severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().help()
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.first().source_code()
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.first().labels()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.first().diagnostic_source()
}
}
impl AsRef<NonEmpty<ParseError>> for ParseErrors {
fn as_ref(&self) -> &NonEmpty<ParseError> {
&self.0
}
}
impl AsMut<NonEmpty<ParseError>> for ParseErrors {
fn as_mut(&mut self) -> &mut NonEmpty<ParseError> {
&mut self.0
}
}
impl Deref for ParseErrors {
type Target = NonEmpty<ParseError>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ParseErrors {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Into<ParseError>> From<T> for ParseErrors {
fn from(err: T) -> Self {
ParseErrors::singleton(err.into())
}
}
impl<T: Into<ParseError>> Extend<T> for ParseErrors {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
self.0.extend(iter.into_iter().map(Into::into))
}
}
impl IntoIterator for ParseErrors {
type Item = ParseError;
type IntoIter = iter::Chain<iter::Once<Self::Item>, std::vec::IntoIter<Self::Item>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a ParseErrors {
type Item = &'a ParseError;
type IntoIter = iter::Chain<iter::Once<Self::Item>, std::slice::Iter<'a, ParseError>>;
fn into_iter(self) -> Self::IntoIter {
iter::once(&self.head).chain(self.tail.iter())
}
}