New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[4.0] Consistent public error types in the Rust API #745
Comments
Errors to take a standardization pass over: (in alphabetical order) Edit 4/15/24: added tentative assignments
All of these are currently public types in our public API. |
Small amendment, since Pattern C doesn't say this explicitly: It's also OK for an enum variant to be itself another enum that complies with the Pattern C guidelines. It doesn't always have to be a struct with private fields. Unresolved question: Is it OK for an enum variant to be Another unresolved question: The current |
|
Currently, miette's |
that last suggestion became #748 |
This should be used inside of |
Signed-off-by: Shaobo He <shaobohe@amazon.com>
Category
Error message improvements
Describe the feature you'd like to request
Currently Cedar is inconsistent about what error data is public (accessible to Cedar consumers and thus part of our stable interface) and what error data is private (implementation details we're free to change at will, e.g., to improve error messages). In fact, at least six different patterns are currently in use in different places in the Cedar codebase, as described below. This issue requests that Cedar standardize all of its error types into Pattern C. This will be a breaking change to some APIs.
Pattern A
Methods that can return errors do not publicize what type they are returning, just
impl miette::Diagnostic
. Callers (Cedar consumers) can use the methods onmiette::Diagnostic
to get the error's message, help text, source location, etc but no other info.Policy::to_json()
currently takes this approach:This option is maximally flexible for Cedar developers, as Cedar can make any changes to error structures that it wants, even in patch releases, as long as whatever is returned still implements
miette::Diagnostic
(which will always be the case). But, this option is undesirable because it does not provide enough information to callers (Cedar consumers). Callers cannot distinguish between different error cases in a structured way, and cannot get other information about the error in a structured way -- for instance, seeing whichEntityUid
was responsible for anEntityDoesNotExist
error, except by inspecting the error message.Pattern B
Methods that can return errors return a named error type, but that type is a struct with private field(s). Of course, the error type implements
miette::Diagnostic
so provides all of those methods, to get the error's message, help text, source location, etc. The error type can also provide additional public methods if it wants.eval_expression()
(the public interface for the Cedar evaluator) currently takes this approach:where callers (Cedar consumers) just see
EvaluationError
as a struct with private fields in the docs.This option is still extremely flexible for Cedar developers, as Cedar can make any changes to error structures that it wants, even in patch releases, as long as whatever is returned still implements
miette::Diagnostic
and any additional public methods we've already committed to providing for that error type. By choosing what public methods to provide, we have full control over what's public for each error type. However, the public methods we provide do have to "make sense" for all enum variants, assuming there is an enum under the hood. We can't have different public methods per enum variant, like we can in Pattern C. For this reason, and to improve consumers' ability to pattern-match on Cedar error types, we prefer Pattern C.Pattern C (Proposed to standardize on)
Methods that can return errors return a public enum, but each enum variant is a single struct with private field(s). The enum itself, and also all of the member structs, implement
miette::Diagnostic
. The member structs can also provide additional public methods if they want, as could the enum.TypeErrorKind
currently takes something close to this approach. As you can see in its docs, it is a public enum with 13 variants, most of which are structs with private fields:where callers (Cedar consumers) just see
UnexpectedType
andIncompatibleTypes
as structs with private fields in the docs.This option is somewhat less flexible for Cedar developers than Patterns A or B. Cedar cannot remove or rename enum variants in patch releases, and can't add new enum variants either unless the overall enum is marked as
non_exhaustive
(likeTypeErrorKind
is). However, Cedar can still add or remove fields from the member structs (likeUnexpectedType
) at will, as long as all public methods are maintained.Callers (Cedar consumers) get significantly more structured information than they do with Patterns A or B. They get not only the methods on
miette::Diagnostic
and any other public methods provided by the enum, but also can match on the error kind, so they can for instance distinguish betweenUnexpectedType
andIncompatibleTypes
in a structured way, that is, without matching on the error message. And, they can use whatever variant-specific public methods Cedar chooses to provide on each member struct.Pattern D
Methods that can return errors return a public enum, where each enum variant contains its data inline, or, contains a struct with public fields. The enum itself, and also all of the member structs (if any), implement
miette::Diagnostic
. The member structs (if any), as well as the public enum, can also provide additional public methods if they want, but this would be only a convenience and not expose any new information, because all of the information is already public.SchemaError
currently takes something close to this approach. As you can see in its docs, it is a public enum with 21 variants, which contain a mix of inline data related to the error and/or other error types with public fields (likeEntityAttrEvaluationError
).Callers (Cedar consumers) see detailed data, like the
HashSet
of entity types that were found to be undeclared inUndeclaredEntityTypes
, or the public fields ofEntityAttrEvaluationError
. This is in contrast to Pattern C, whereUndeclaredEntityTypes
would contain a struct calledUndeclaredEntityTypes
instead of containing theHashSet
directly; and the member structUndeclaredEntityTypes
would perhaps have a public method for iterating over the individual types, but would perhaps not promise that it's aHashSet
(so Cedar would be free to change that implementation detail in patch releases), and would also allow Cedar to add more information toUndeclaredEntityType
(like a source location) without breaking changes.This option is much less flexible for Cedar developers than the above patterns. Like Pattern C, Cedar cannot remove or rename enum variants in patch releases, and cannot add enum variants unless the enum is marked
non_exhaustive
(whichSchemaError
is not). Unlike Pattern C, with Pattern D Cedar cannot add or remove data from the enum variants or their member structs, or change the type of any contained data. For instance, Cedar could not change theHashSet
above to aBTreeSet
orVec
, and could not add source locations to these error types. With Pattern C, Cedar could make those changes as long as we still provide the same public methods on each member struct. (We would write the public methods to, e.g., not expose the collection type contained inUndeclaredEntityTypes
, but only allow iterating over it.)Another complication of this option is the need to use only public-visible types in the enum variants and member structs. For instance, the
EntityUid
in theCycleInActionHierarchy
variant is a publicEntityUid
, not a CoreEntityUID
. Of course, the validator internally needs to return CoreEntityUID
as it has no way to name the public-interfaceEntityUid
(depending oncedar-policy
would be circular). So, Cedar currently duplicates the entireSchemaError
enum -- one declaration in the validator crate, using internal types, and another declaration in thecedar-policy
crate, using public-interface types, with conversion methods between them. Patterns B and C also have to deal with converting internal types to public-interface types, but this is both easier and potentially more performant to do within the type's public methods rather than in the type itself. Pattern C would probably require us to re-declare the public enum, with wrapped member structs and wrapped public methods.Pattern E (definitely bad)
Some Cedar errors are in weird in-between states, for instance
EntitiesError
(docs), which is a public enum with member structs that have public fields but are not themselves interface types, only Core types. This is unclear to callers (Cedar consumers) what they're "allowed" to match on. Depending on the syntactic situation, they might be able to access the fields of the member structs, or might have to take a dependency oncedar-policy-core
to do so (because they're unable to syntactically name the type without importing it from Core). This is both confusing and bad.Pattern F (definitely bad)
Another bad state is also exemplified by
EntitiesError
(sorry,EntitiesError
): one of its enum variants isDuplicate
which contains a CoreEntityUID
as inline data. To the extent that fields are public (like this one), they should only contain public-interface types. I'm not sure why Rust allows the Core typeEntityUID
to leak out of the public interface in this way. Regardless of whether Rust should or should not allow it, Cedar should not do this in its error types.Describe alternatives you've considered
Patterns A, B, D are plausible alternatives (with B the most plausible of those), but this issue proposes standardizing on Pattern C.
Additional context
No response
Is this something that you'd be interested in working on?
The text was updated successfully, but these errors were encountered: