Skip to content

Surface structured parse diagnostics from PolicySet.parsePolicies - #367

Open
jamesmulcahy wants to merge 1 commit into
cedar-policy:mainfrom
jamesmulcahy:surface-parse-diagnostics
Open

Surface structured parse diagnostics from PolicySet.parsePolicies#367
jamesmulcahy wants to merge 1 commit into
cedar-policy:mainfrom
jamesmulcahy:surface-parse-diagnostics

Conversation

@jamesmulcahy

Copy link
Copy Markdown

Problem

PolicySet.parsePolicies throws InternalException whose message is format!("Internal JNI Error: {e}") (CedarJavaFFI/src/interface.rs, in jni_failed). For a parse failure that discards everything miette recorded — the source span, the tokens the parser expected, the help text — and because ParseErrors' Display prints only its first error, every subsequent error is lost too.

A policy author writing this:

forbid(principal, Foo::Action::"Read", resource == Foo::Table::"t");

sees only:

Internal error: Internal JNI Error: unexpected token `::`

No location, and no hint that the mistake is a missing action ==. The Rust CLI, on the same input:

× failed to parse policy set
╰─▶ unexpected token `::`
 ╭────
 1 │ forbid(principal, Foo::Action::"Read", resource == Foo::Table::"t");
 ·                      ─┬
 ·                       ╰── expected `!=`, `)`, `,`, `:`, `<`, `<=`, `==`, `>`, `>=`, `in`, or `is`
 ╰────

Measured against cedar-java 4.10.0:

PolicySet.parsePolicies("forbid(principal, Foo::Action::\"Read\", resource);");
// getMessage(): Internal error: Internal JNI Error: unexpected token `::`
// getErrors():  [Internal JNI Error: unexpected token `::`]   // size 1, always

Change

Everything needed already exists in the crate:

  • PolicySet::from_str returns ParseErrors, which is IntoIterator<Item = ParseError>
  • each ParseError implements miette::Diagnostic
  • cedar_policy::ffi::DetailedError already has impl<E: miette::Diagnostic + ?Sized> From<&E> — the impl the validation path already uses

So parsePoliciesJni downcasts ParseErrors and throws a new PolicyParseException extends InternalException carrying List<DetailedError>, the same representation AuthorizationEngine.validate already returns:

PolicyParseException e = ...;
e.getDetailedErrors();
// [message=unexpected token `::`,
//  sourceLocations=[SourceLabel{label="expected `!=`, `)`, `,`, `:`, `<`, `<=`, `==`,
//                   `>`, `>=`, `in`, or `is`", start=21, end=23}]]

Existing catch (InternalException e) blocks are unaffected. If building the richer exception fails for any reason, the generic path is used, so a parse error can never turn into a different kind of failure.

Two message changes, both up for discussion

before after
getMessage() Internal error: Internal JNI Error: unexpected token ... Internal error: unexpected token ...
getErrors() [Internal JNI Error: unexpected token ...] [unexpected token ...]
  1. The "Internal JNI Error: " prefix is dropped — it describes the binding rather than the policy, and reading "Internal error" for an ordinary typo suggests a fault in the library rather than something the caller can fix.
  2. getErrors() carries one entry per parse error rather than a single entry for the whole document, which is what its plural contract always implied.

Both are independent of the diagnostics and easy to drop if you'd rather keep the strings frozen — happy to revise.

Breaking?

Not to any type or signature: PolicyParseException is a subclass and no existing method changes shape. Callers string-matching on the message text of a parse failure would see the two differences above.

Testing

  • 6 new tests in PolicyParseDiagnosticsTests — span covers the offending token, all errors reported not just the first, help text survives where Cedar supplies it, still catchable as InternalException, message/getErrors() strings pinned, valid policy sets unaffected.
  • Full CedarJava suite: 68,723 tests, 0 failures.
  • cargo test in CedarJavaFFI: 69 passed.

Local FFI builds used cargo build --release --features partial-eval for the host target rather than the cargo zigbuild cross-compile path, since zig was not available in my environment; CI exercises the normal path.

Scope

Deliberately limited to parsePolicies. Policy.parseStaticPolicy, Policy.parsePolicyTemplate, Schema.parse and PolicyFormatter lose detail the same way and would be natural follow-ups — kept out here to keep the change focused, per CONTRIBUTING.

Related: #68 asks for the id of a malformed policy, which is adjacent but stops short of diagnostics.

Parse failures are reduced to format!("Internal JNI Error: {e}") in
jni_failed, discarding the miette diagnostic cedar produced: the source
span, the tokens the parser expected, and the help text. ParseErrors'
Display also prints only its first error, so subsequent errors are lost.

Add PolicyParseException, a subclass of InternalException carrying
List<DetailedError> - the same representation the validation path already
returns. parsePoliciesJni downcasts ParseErrors and converts each
ParseError via the existing From<&E: miette::Diagnostic> impl. If building
the richer exception fails for any reason the generic path is used, so a
parse error can never become a different kind of failure.

Existing catch (InternalException) blocks are unaffected. Two message
details change deliberately: the "Internal JNI Error: " prefix is dropped,
since it describes the binding rather than the policy and reads as a
library fault rather than a typo the caller can fix; and getErrors()
carries one entry per parse error rather than a single entry for the whole
document, which is what its plural contract always implied.

Signed-off-by: James Mulcahy <jmulcahy@netflix.com>
@jamesmulcahy
jamesmulcahy force-pushed the surface-parse-diagnostics branch from 8d0587c to 5276059 Compare August 24, 2026 16:35
@jamesmulcahy

Copy link
Copy Markdown
Author

Full disclosure -- I've not written any rust before myself, and Claude helped with this change. Background/motivation is well summarized by Claude above. The TL;DR is that the cedar CLI gives much better error output than the Java API -- and I'm trying to improve the experience through Java so our users can be more meaningful & actionable feedback when they provide an invalid policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant