Generate sealed interfaces to match against endpoint associated errors#2696
Conversation
Generate changelog in
|
| record Unknown(RemoteException e) implements TestBasicErrorErrors {} | ||
| } | ||
|
|
||
| sealed interface TestImportedErrorErrors { |
There was a problem hiding this comment.
Probably nicer for the suffix to be singular.
There was a problem hiding this comment.
For reviewers: this file (or the async interface) contains the relevant newly generated code.
There was a problem hiding this comment.
Singular definitely makes sense since the actual instance will be a single type. Also thoughts on ErrorType or something for the suffix? Was also thinking AssociatedError, but that feels like a mouthful
There was a problem hiding this comment.
hm, what are your thoughts on just keeping the name <EndpointName>Error? Feel like it's cleaner.
8c13809 to
65ee1b8
Compare
| ErrorGenerationUtils.errorTypesClassName(error.getError().getNamespace()), | ||
| ErrorGenerationUtils.errorExceptionClassName(errorName)); | ||
| TypeSpec errorRecord = createRecordForEndpointErrorUtility(errorName, exceptionClassName, utiltyClassName); | ||
| // Not strictly required. Seems a bit cleaner to not have it. |
There was a problem hiding this comment.
This is because of JLS-9.1.4. I'll remove the comment.
There was a problem hiding this comment.
I suspect these sealed interfaces are small so it might be quite easy to just see all of the sub-interfaces. For things like the generated union classes (which were much larger), I think it's nice to have the list of permitted interfaces in one line at a glance. I can be convinced that these sealed interfaces could be large for endpoints with loads of associated errors and we should add them. Not super opinionated here.
|
|
||
| record EndpointError(EndpointSpecificErrors.EndpointErrorException e) implements TestImportedErrorErrors {} | ||
|
|
||
| record Unknown(RemoteException e) implements TestImportedErrorErrors {} |
There was a problem hiding this comment.
I initially did not have this Unknown type, and just threw an exception if the RemoteException wasn't an endpoint associated error, but I think this will lead to nested try-catch blocks in code. Devs will likely want to handle the "unknown" case anyway so creating a separate record for it that they can match on makes sense.
| } catch (RemoteException e) { | ||
| TestBasicErrorErrors error = ErrorServiceBlocking.TestBasicErrorErrors.from(e); | ||
| assertThat(error).isInstanceOfSatisfying(TestBasicErrorErrors.InvalidArgument.class, invalidArgument -> { | ||
| assertThat(invalidArgument.e().error().parameters().field()).isEqualTo("field"); |
There was a problem hiding this comment.
I can change this to .exception.
| record Unknown(RemoteException e) implements TestBasicErrorErrors {} | ||
| } | ||
|
|
||
| sealed interface TestImportedErrorErrors { |
There was a problem hiding this comment.
Singular definitely makes sense since the actual instance will be a single type. Also thoughts on ErrorType or something for the suffix? Was also thinking AssociatedError, but that feels like a mouthful
| if (e instanceof TestErrors.InvalidArgumentException ex) { | ||
| return new InvalidArgument(ex); | ||
| } else if (e instanceof ConjureErrors.ConflictingCauseSafeArgErrException ex) { | ||
| return new ConflictingCauseSafeArgErr(ex); |
There was a problem hiding this comment.
Aren't the series of instanceof checks unideal from a performance standpoint? If we were on source 21+ I would say a switch statement would be an easy replacement over a visitor
There was a problem hiding this comment.
Do we have an option without instanceof in the absence of source 21+? Visitor patterns with overloads would have to rely on the RemoteException having an accept method to take a visitor, which simply won't work here 🤔
There was a problem hiding this comment.
Yeah, I think we can actually match on the error name here. I can make that change.
| } | ||
| } | ||
|
|
||
| record InvalidArgument(TestErrors.InvalidArgumentException e) implements TestBasicErrorErrors {} |
There was a problem hiding this comment.
nit: we should name the parameter error or something so that the getter method is not .e()
| ClassName utiltyClassName = ClassName.get( | ||
| packageName, | ||
| className.simpleName(), | ||
| CaseFormat.LOWER_CAMEL.to( | ||
| CaseFormat.UPPER_CAMEL, | ||
| endpointDef.getEndpointName().get()) + "Errors"); |
There was a problem hiding this comment.
I think you could use ClassName#nestedClass here
There was a problem hiding this comment.
Didn't look through the codegen pieces, but at the generated code. The new helpers seem like a good addition and shouldn't break anything.
The only ABI risks I could find are 1. having it adopted and reverting usage of the flag 2. Removing associations between errors and endpoints (even with the error still existing) I think this is probably fine.
I'm not entirely sure the abi checker would protect us from the removal in all cases however, specifically if the usage looks like
switch(TestImportedErrorErrors.from(exception)) {
EndpointError -> log.error("Found an endpoint error");
Unknown -> log.error("Found an unknown error");
}
where the class itself isn't used anywhere
Beyond this, I agree with Kunal's comments and will leave the deeper review to him
Invalidated by push of 0e40e01
✅ Successfully generated changelog entry!Need to regenerate?Simply interact with the changelog bot comment again to regenerate these entries. 📋Changelog Preview💡 Improvements
|
|
Released 8.61.0 |
Before this PR
In #2401 we added support for defining endpoint associated errors. This allowed Conjure service owners to define the errors that clients should expect to handle. This work was extended in #2435 where Dialogue client interfaces were generated to return sealed interfaces permitting records that corresponded to each endpoint error, and a "success" type, acting as a "result" type.
Initially the stance was that endpoint errors defined by services need to be handled by clients: using result types (or checked exceptions) would nudge callers of a client's endpoint to handle all of the potential errors a service has associated with the endpoint. (This could be worked around by utility methods that extract specific "success" types out of the provided result type but that would be an intentional choice made by clients.) In practice, this is a restrictive stance that hurts adoption: client code does not always need to handle the errors that endpoints have associated but if they would like to, they should have a way to know what errors should be handled.
Another reason to not change the return type of all endpoints in all Dialogue client interfaces is that it could lead to ABI breaks.
After this PR
For each endpoint that has associated errors in its definition, there will be a new sealed interface generated that allows devs to convert a generic
RemoteExceptioninto one of the error-specific exceptions. The sealed interface allows devs on Java 21+ to use exhaustive pattern matching switch statements providing compile-time guarantees that they have handled all possible endpoint associated errors.For example, consider this endpoint definition:
The following Dialogue client interface will be generated:
This allows clients to catch
RemoteExceptions thrown by the call totestBasicErrorand convert it into aTestBasicErrorErrorsobject.==COMMIT_MSG==
Generate sealed interfaces to match against endpoint associated errors
==COMMIT_MSG==
Possible downsides?