polar_core/
error.rs

1use std::{borrow::Borrow, fmt, sync::Arc};
2
3use indoc::formatdoc;
4use serde::Serialize;
5use strum_macros::AsRefStr;
6
7use super::{
8    resource_block::Declaration,
9    rules::Rule,
10    sources::{Context, Source},
11    terms::{Operation, Symbol, Term},
12};
13
14pub type PolarResult<T> = Result<T, PolarError>;
15
16#[derive(Debug, Clone, Serialize)]
17pub enum ErrorKind {
18    Parse(ParseError),
19    Runtime(RuntimeError),
20    Operational(OperationalError),
21    Validation(ValidationError),
22}
23
24impl fmt::Display for ErrorKind {
25    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26        match self {
27            Self::Parse(e) => write!(f, "{}", e),
28            Self::Runtime(e) => write!(f, "{}", e),
29            Self::Operational(e) => write!(f, "{}", e),
30            Self::Validation(e) => write!(f, "{}", e),
31        }
32    }
33}
34
35// NOTE(gj): `ErrorKind` is a layer of indirection so we can avoid infinite recursion when
36// serializing `PolarError` into `FormattedPolarError`, which references the error kind. If
37// `PolarError` were the enum (without `ErrorKind`), then `PolarError` would serialize into
38// `FormattedPolarError`, which has a field of type `PolarError`... etc. There's probably a better
39// way to structure this, but for now this is the path of least resistance.
40#[derive(Debug, Clone, Serialize)]
41#[serde(into = "FormattedPolarError")]
42pub struct PolarError(pub ErrorKind);
43
44impl std::error::Error for PolarError {}
45
46impl fmt::Display for PolarError {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        write!(f, "{}", self.0)?;
49        if let Some(context) = self.get_context() {
50            write!(f, "{}", context)?;
51        }
52        Ok(())
53    }
54}
55
56impl PolarError {
57    pub fn kind(&self) -> String {
58        use ErrorKind::*;
59        match &self.0 {
60            Operational(o) => "OperationalError::".to_string() + o.as_ref(),
61            Parse(p) => "ParseError::".to_string() + p.kind.as_ref(),
62            Runtime(r) => "RuntimeError::".to_string() + r.as_ref(),
63            Validation(v) => "ValidationError::".to_string() + v.as_ref(),
64        }
65    }
66
67    pub fn get_context(&self) -> Option<Context> {
68        use ErrorKind::*;
69        use OperationalError::*;
70        use ParseErrorKind::*;
71        use RuntimeError::*;
72        use ValidationError::*;
73
74        match &self.0 {
75            Parse(e) => match &e.kind {
76                // These errors track `loc` (left bound) and `token`, and we calculate right bound
77                // as `loc + token.len()`.
78                DuplicateKey { key: token, loc }
79                | ExtraToken { token, loc }
80                | IntegerOverflow { token, loc }
81                | InvalidFloat { token, loc }
82                | ReservedWord { token, loc }
83                | UnrecognizedToken { token, loc } => {
84                    Some(Context::new(e.source.clone(), *loc, loc + token.len()))
85                }
86
87                // These errors track `loc` and only pertain to a single character, so right bound
88                // of context is also `loc`.
89                InvalidTokenCharacter { loc, .. }
90                | InvalidToken { loc }
91                | UnrecognizedEOF { loc } => Some(Context::new(e.source.clone(), *loc, *loc)),
92
93                // These errors track `term`, from which we calculate the context.
94                WrongValueType { term, .. } => term.parsed_context().cloned(),
95            },
96
97            Runtime(e) => match e {
98                // These errors sometimes track `term`, from which we derive context.
99                Application { term, .. } => term.as_ref().and_then(Term::parsed_context).cloned(),
100
101                // These errors track `term`, from which we derive the context.
102                ArithmeticError { term }
103                | TypeError { term, .. }
104                | UnhandledPartial { term, .. }
105                | Unsupported { term, .. } => term.parsed_context().cloned(),
106
107                // These errors never have context.
108                StackOverflow { .. }
109                | QueryTimeout { .. }
110                | IncompatibleBindings { .. }
111                | DataFilteringFieldMissing { .. }
112                | DataFilteringUnsupportedOp { .. }
113                | InvalidRegistration { .. }
114                | QueryForUndefinedRule { .. }
115                | MultipleLoadError => None,
116            },
117
118            Validation(e) => match e {
119                // These errors track `term`, from which we calculate the context.
120                ResourceBlock { term, .. }
121                | SingletonVariable { term, .. }
122                | UndefinedRuleCall { term }
123                | DuplicateResourceBlockDeclaration {
124                    declaration: term, ..
125                }
126                | UnregisteredClass { term, .. } => term.parsed_context().cloned(),
127
128                // These errors track `rule`, from which we calculate the context.
129                InvalidRule { rule, .. }
130                | InvalidRuleType {
131                    rule_type: rule, ..
132                } => rule.parsed_context().cloned(),
133
134                // These errors track `rule_type`, from which we sometimes calculate the context.
135                MissingRequiredRule { rule_type } => {
136                    if rule_type.name.0 == "has_relation" {
137                        rule_type.parsed_context().cloned()
138                    } else {
139                        // TODO(gj): copy source info from the appropriate resource block term for
140                        // `has_role()` rule type we create.
141                        None
142                    }
143                }
144
145                // These errors pertain to a specific file but not to a specific place therein.
146                FileLoading {
147                    filename, contents, ..
148                } => {
149                    let source = Arc::new(Source::new_with_name(filename, contents));
150                    Some(Context::new(source, 0, 0))
151                }
152            },
153
154            Operational(e) => match e {
155                // These errors track `received`, from which we calculate the context.
156                UnexpectedValue { received, .. } => received.parsed_context().cloned(),
157                // These errors never have context.
158                InvalidState { .. } | Serialization { .. } | Unknown => None,
159            },
160        }
161    }
162}
163
164#[cfg(test)]
165impl PolarError {
166    pub fn unwrap_parse(self) -> ParseErrorKind {
167        match self.0 {
168            ErrorKind::Parse(ParseError { kind, .. }) => kind,
169            e => panic!("Expected ErrorKind::Parse; was: {}", e),
170        }
171    }
172
173    pub fn unwrap_runtime(self) -> RuntimeError {
174        match self.0 {
175            ErrorKind::Runtime(e) => e,
176            e => panic!("Expected ErrorKind::Runtime; was: {}", e),
177        }
178    }
179
180    pub fn unwrap_validation(self) -> ValidationError {
181        match self.0 {
182            ErrorKind::Validation(e) => e,
183            e => panic!("Expected ErrorKind::Validation; was: {}", e),
184        }
185    }
186}
187
188#[derive(Clone, Serialize)]
189pub struct FormattedPolarError {
190    pub kind: ErrorKind,
191    pub formatted: String,
192}
193
194impl From<PolarError> for FormattedPolarError {
195    fn from(other: PolarError) -> Self {
196        Self {
197            formatted: other.to_string(),
198            kind: other.0,
199        }
200    }
201}
202
203#[derive(Clone, Serialize)]
204#[serde(transparent)]
205pub struct ParseError {
206    pub kind: ParseErrorKind,
207    #[serde(skip_serializing)]
208    pub source: Arc<Source>,
209}
210
211// Ignore `source` field when `Debug`-formatting `ParseError`.
212impl fmt::Debug for ParseError {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        f.debug_struct("ParseError")
215            .field("kind", &self.kind)
216            .finish()
217    }
218}
219
220impl fmt::Display for ParseError {
221    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222        write!(f, "{}", self.kind)
223    }
224}
225
226impl From<ParseError> for PolarError {
227    fn from(err: ParseError) -> Self {
228        Self(ErrorKind::Parse(err))
229    }
230}
231
232#[derive(AsRefStr, Clone, Debug, Serialize)]
233pub enum ParseErrorKind {
234    IntegerOverflow {
235        token: String,
236        loc: usize,
237    },
238    InvalidTokenCharacter {
239        token: String,
240        c: char,
241        loc: usize,
242    },
243    InvalidToken {
244        loc: usize,
245    },
246    #[allow(clippy::upper_case_acronyms)]
247    UnrecognizedEOF {
248        loc: usize,
249    },
250    UnrecognizedToken {
251        token: String,
252        loc: usize,
253    },
254    ExtraToken {
255        token: String,
256        loc: usize,
257    },
258    ReservedWord {
259        token: String,
260        loc: usize,
261    },
262    InvalidFloat {
263        token: String,
264        loc: usize,
265    },
266    WrongValueType {
267        loc: usize,
268        term: Term,
269        expected: String,
270    },
271    DuplicateKey {
272        loc: usize,
273        key: String,
274    },
275}
276
277impl fmt::Display for ParseErrorKind {
278    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279        match self {
280            Self::IntegerOverflow { token, .. } => {
281                write!(f, "'{}' caused an integer overflow", token.escape_debug())
282            }
283            Self::InvalidTokenCharacter { token, c, .. } => write!(
284                f,
285                "'{}' is not a valid character. Found in {}",
286                c.escape_debug(),
287                token.escape_debug()
288            ),
289            Self::InvalidToken { .. } => write!(f, "found an unexpected sequence of characters"),
290            Self::UnrecognizedEOF { .. } => write!(
291                f,
292                "hit the end of the file unexpectedly. Did you forget a semi-colon"
293            ),
294            Self::UnrecognizedToken { token, .. } => write!(
295                f,
296                "did not expect to find the token '{}'",
297                token.escape_debug()
298            ),
299            Self::ExtraToken { token, .. } => write!(
300                f,
301                "did not expect to find the token '{}'",
302                token.escape_debug()
303            ),
304            Self::ReservedWord { token, .. } => write!(
305                f,
306                "{} is a reserved Polar word and cannot be used here",
307                token.escape_debug()
308            ),
309            Self::InvalidFloat { token, .. } => write!(
310                f,
311                "{} was parsed as a float, but is invalid",
312                token.escape_debug()
313            ),
314            Self::WrongValueType { term, expected, .. } => {
315                write!(f, "Wrong value type: {}. Expected a {}", term, expected)
316            }
317            Self::DuplicateKey { key, .. } => {
318                write!(f, "Duplicate key: {}", key)
319            }
320        }
321    }
322}
323
324#[derive(AsRefStr, Clone, Debug, Serialize)]
325pub enum RuntimeError {
326    ArithmeticError {
327        /// Term<Operation> where the error arose, tracked for lexical context.
328        term: Term,
329    },
330    Unsupported {
331        msg: String,
332        /// Term where the error arose, tracked for lexical context.
333        term: Term,
334    },
335    TypeError {
336        msg: String,
337        stack_trace: String,
338        /// Term where the error arose, tracked for lexical context.
339        term: Term,
340    },
341    StackOverflow {
342        msg: String,
343    },
344    QueryTimeout {
345        elapsed: u64,
346        timeout: u64,
347    },
348    Application {
349        msg: String,
350        stack_trace: String,
351        /// Option<Term> where the error arose, tracked for lexical context.
352        term: Option<Term>,
353    },
354    IncompatibleBindings {
355        msg: String,
356    },
357    UnhandledPartial {
358        var: Symbol,
359        /// Term where the error arose, tracked for lexical context.
360        term: Term,
361    },
362    DataFilteringFieldMissing {
363        var_type: String,
364        field: String,
365    },
366    DataFilteringUnsupportedOp {
367        operation: Operation,
368    },
369    // TODO(gj): consider moving to ValidationError.
370    InvalidRegistration {
371        sym: Symbol,
372        msg: String,
373    },
374    MultipleLoadError,
375    /// The user queried for an undefined rule. This is the runtime analogue of
376    /// `ValidationError::UndefinedRuleCall`.
377    QueryForUndefinedRule {
378        name: String,
379    },
380}
381
382impl From<RuntimeError> for PolarError {
383    fn from(err: RuntimeError) -> Self {
384        Self(ErrorKind::Runtime(err))
385    }
386}
387
388impl fmt::Display for RuntimeError {
389    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
390        match self {
391            Self::ArithmeticError { term } => write!(f, "Arithmetic error: {}", term),
392            Self::Unsupported { msg, .. } => write!(f, "Not supported: {}", msg),
393            Self::TypeError {
394                msg, stack_trace, ..
395            } => {
396                writeln!(f, "{}", stack_trace)?;
397                write!(f, "Type error: {}", msg)
398            }
399            Self::StackOverflow { msg } => {
400                write!(f, "{}", msg)
401            }
402            Self::QueryTimeout { elapsed, timeout } => write!(f, "Query timeout: Query running for {}ms, which exceeds the timeout of {}ms. To disable timeouts, set the POLAR_TIMEOUT_MS environment variable to 0.", elapsed, timeout),
403            Self::Application {
404                msg, stack_trace, ..
405            } => {
406                writeln!(f, "{}", stack_trace)?;
407                write!(f, "Application error: {}", msg)
408            }
409            Self::IncompatibleBindings { msg } => {
410                write!(f, "Attempted binding was incompatible: {}", msg)
411            }
412            Self::UnhandledPartial { var, term } => {
413                write!(
414                    f,
415                    "Found an unhandled partial in the query result: {var}
416
417This can happen when there is a variable used inside a rule
418which is not related to any of the query inputs.
419
420For example: f(_x) if y.a = 1 and y.b = 2;
421
422In this example, the variable `y` is constrained by `a = 1 and b = 2`,
423but we cannot resolve these constraints without further information.
424
425The unhandled partial is for variable {var}.
426The expression is: {expr}
427",
428                    var = var,
429                    expr = term,
430                )
431            }
432            Self::DataFilteringFieldMissing { var_type, field } => {
433                let msg = formatdoc!(
434                    r#"Unregistered field or relation: {var_type}.{field}
435
436                    Please include `{field}` in the `fields` parameter of your
437                    `register_class` call for {var_type}.  For example, in Python:
438
439                        oso.register_class({var_type}, fields={{
440                            "{field}": <type or relation>
441                        }})
442
443                    For more information please refer to our documentation:
444                        https://docs.osohq.com/guides/data_filtering.html
445                    "#,
446                    var_type = var_type,
447                    field = field
448                );
449                write!(f, "{}", msg)
450            }
451            Self::DataFilteringUnsupportedOp { operation } => {
452                let msg = formatdoc!(
453                    r#"Unsupported operation:
454                        {:?}/{}
455                    in the expression:
456                        {}
457                    in a data filtering query.
458
459                    This operation is not currently supported for data filtering.
460                    For more information please refer to our documentation:
461                        https://docs.osohq.com/guides/data_filtering.html
462                    "#,
463                    operation.operator,
464                    operation.args.len(),
465                    operation
466                );
467                write!(f, "{}", msg)
468            }
469            Self::InvalidRegistration { sym, msg } => {
470                write!(f, "Invalid attempt to register '{}': {}", sym, msg)
471            }
472            Self::MultipleLoadError => write!(f, "Cannot load additional Polar code -- all Polar code must be loaded at the same time."),
473            Self::QueryForUndefinedRule { name } => write!(f, "Query for undefined rule `{}`", name),
474        }
475    }
476}
477
478#[derive(AsRefStr, Clone, Debug, Serialize)]
479pub enum OperationalError {
480    /// An invariant has been broken internally.
481    InvalidState { msg: String },
482    /// Serialization errors in the `polar-c-api` crate.
483    Serialization { msg: String },
484    // This should go away once we can constrain the value variant of a particular term in the type
485    // system, e.g., `Term<String>` instead of `Term::value().as_string()`.
486    UnexpectedValue {
487        expected: &'static str,
488        received: Term,
489    },
490    /// Rust panics caught in the `polar-c-api` crate.
491    Unknown,
492}
493
494impl From<OperationalError> for PolarError {
495    fn from(err: OperationalError) -> Self {
496        Self(ErrorKind::Operational(err))
497    }
498}
499
500impl fmt::Display for OperationalError {
501    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
502        match self {
503            Self::InvalidState { msg } => write!(f, "Invalid state: {}", msg),
504            Self::Serialization { msg } => write!(f, "Serialization error: {}", msg),
505            Self::UnexpectedValue { expected, received } => write!(
506                f,
507                "Unexpected value.\n  Expected: {expected}\n  Received: {received}"
508            ),
509            Self::Unknown => write!(
510                f,
511                "We hit an unexpected error.\n\
512                Please submit a bug report at <https://github.com/osohq/oso/issues>"
513            ),
514        }
515    }
516}
517
518#[derive(AsRefStr, Clone, Debug, Serialize)]
519pub enum ValidationError {
520    FileLoading {
521        #[serde(skip_serializing)]
522        filename: String,
523        #[serde(skip_serializing)]
524        contents: String,
525        msg: String,
526    },
527    MissingRequiredRule {
528        rule_type: Rule,
529    },
530    InvalidRule {
531        /// Rule where the error arose, tracked for lexical context.
532        rule: Rule,
533        msg: String,
534    },
535    InvalidRuleType {
536        /// Rule type where the error arose, tracked for lexical context.
537        rule_type: Rule,
538        msg: String,
539    },
540    /// The policy contains a call to an undefined rule. This is the validation analogue of
541    /// `RuntimeError::QueryForUndefinedRule`.
542    UndefinedRuleCall {
543        /// Term<Call> where the error arose, tracked for lexical context.
544        term: Term,
545    },
546    ResourceBlock {
547        /// Term where the error arose, tracked for lexical context.
548        term: Term,
549        msg: String,
550        // TODO(gj): enum for RelatedInformation that has a variant for capturing "other relevant
551        // terms" for a particular diagnostic, e.g., for a DuplicateResourceBlock error the
552        // already-declared resource block would be relevant info for the error emitted on
553        // redeclaration.
554    },
555    SingletonVariable {
556        /// Term<Symbol> where the error arose, tracked for lexical context.
557        term: Term,
558    },
559    UnregisteredClass {
560        /// Term<Symbol> where the error arose, tracked for lexical context.
561        term: Term,
562    },
563    DuplicateResourceBlockDeclaration {
564        /// Term<Symbol> where the error arose.
565        resource: Term,
566        /// Term<String> where the error arose, tracked for lexical context.
567        declaration: Term,
568        existing: Declaration,
569        new: Declaration,
570    },
571}
572
573impl From<ValidationError> for PolarError {
574    fn from(err: ValidationError) -> Self {
575        Self(ErrorKind::Validation(err))
576    }
577}
578
579impl fmt::Display for ValidationError {
580    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
581        match self {
582            Self::FileLoading { msg, .. } => write!(f, "Problem loading file: {}", msg),
583            Self::InvalidRule { rule, msg } => {
584                write!(f, "Invalid rule: {} {}", rule, msg)
585            }
586            Self::InvalidRuleType { rule_type, msg } => {
587                write!(f, "Invalid rule type: {}\n\t{}", rule_type, msg)
588            }
589            Self::UndefinedRuleCall { term } => {
590                write!(f, "Call to undefined rule: {}", term)
591            }
592            Self::MissingRequiredRule { rule_type } => {
593                write!(f, "Missing implementation for required rule {}", rule_type)
594            }
595            Self::ResourceBlock { msg, .. } => {
596                write!(f, "{}", msg)
597            }
598            Self::SingletonVariable { term } => {
599                write!(f, "Singleton variable {term} is unused or undefined; try renaming to _{term} or _", term=term)
600            }
601            Self::UnregisteredClass { term } => {
602                write!(f, "Unregistered class: {}", term)
603            }
604            Self::DuplicateResourceBlockDeclaration {
605                resource,
606                declaration,
607                existing,
608                new,
609            } => {
610                write!(
611                    f,
612                    "Cannot overwrite existing {} declaration {} in resource {} with {}",
613                    existing, declaration, resource, new
614                )
615            }
616        }
617    }
618}
619
620pub(crate) fn invalid_state<T, U>(msg: T) -> PolarResult<U>
621where
622    T: AsRef<str>,
623{
624    let msg = msg.as_ref().into();
625    Err(OperationalError::InvalidState { msg }.into())
626}
627
628pub(crate) fn unexpected_value<T>(expected: &'static str, received: Term) -> PolarResult<T> {
629    Err(OperationalError::UnexpectedValue { expected, received }.into())
630}
631
632pub(crate) fn unsupported<T, U, V>(msg: T, term: U) -> PolarResult<V>
633where
634    T: AsRef<str>,
635    U: Borrow<Term>,
636{
637    let msg = msg.as_ref().into();
638    let term = term.borrow().clone();
639    Err(RuntimeError::Unsupported { msg, term }.into())
640}
641
642pub(crate) fn df_unsupported_op<T>(operation: Operation) -> PolarResult<T> {
643    Err(RuntimeError::DataFilteringUnsupportedOp { operation }.into())
644}
645
646pub(crate) fn df_field_missing<T, U, V>(var_type: T, field: U) -> PolarResult<V>
647where
648    T: AsRef<str>,
649    U: AsRef<str>,
650{
651    Err(RuntimeError::DataFilteringFieldMissing {
652        var_type: var_type.as_ref().into(),
653        field: field.as_ref().into(),
654    }
655    .into())
656}