Skip to content
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

Open union validation #3363

Open
timotheeguerin opened this issue May 15, 2024 · 1 comment
Open

Open union validation #3363

timotheeguerin opened this issue May 15, 2024 · 1 comment
Labels
compiler:core Issues for @typespec/compiler design:needed A design request has been raised that needs a proposal triaged:core
Milestone

Comments

@timotheeguerin
Copy link
Member

Differentiate open union where values are exhaustive at a point in type

How to differentiate an open union where certain emitters might know the whole set of values and want a more precise type

We estalished that when dealing with union of values that might return different values in the future to an existing API version we should always use an open union

union Open {"a", "b", string }

This is an accurate representation of the API.

If a service can guarantee that the values will never change then they can use a closed union or an enum

union Closed {"a", "b"}
enum Closed { a, b }

Problem

Some emitters however would like to represent the more accurate type as they might know all the values and are the ones controlling it(Service) or just want a more precise type that won't affect the runtime(TypeScript client)

And we currently wouldn't be able to differentiate between an union that is open because it might change in the future and an union that currently allows extra values.

union CurrentValues {"a", "b", string }
union WithKnownValues {"a", "b", string }

Ideas

Opt-in decorator

If a service is using this pattern to represent union that might change in the future it is probably the 99% case that those open union represent the set of current values and doesn't actually allow any values at the latest version. So having a decorator to opt-in to a more precise type would be very tedious as you'd most likely have to apply to to every union.

@exhaustiveAtVersion
union CurrentValues {"a", "b", string }
union WithKnownValues {"a", "b", string }

However if a service doesn't actually care about this - control both end(client and service) or maybe even went and did the work to guarantee that values defined at a version never change(showhow) then the decorator above does seem like a good idea as if they actually use union WithKnownValues {"a", "b", string } it most likely mean they accept the extra values.

Opt-in service and opt-out decorator

An alternative would be to opt-in a service to this behavior using a decorator on the service namespace

@useExhaustiveOpenUnion
namespace MyService;

union CurrentValues { "a", "b", string }
@allowsUnknownValues
union WithKnownValues { "a", "b", string }

The emitter could be the one deciding to opt-in to this behavior with a flag as well

options:
  my-emitter:
    open-union-handling: exhaustive | open
namespace MyService;

union CurrentValues { "a", "b", string }
@allowsUnknownValues
union WithKnownValues { "a", "b", string }
model Foo {
  @pattern("[a-z]+")
  name: string;
}

Usage scenarios

  1. If a single service version will only ever accept or return a fixed set, use fixed union, adding new values in new service versions is not a breaking change.
enum OnlyOutput {a, b, @added(v2) c }
// In this case the service does guarantee that in v1 c would never get returned
  1. If a single service version might return a new value in the future, use an extensible union, adding new values in new service versions is not a breaking change.
enum OnlyOutput {a, b, @added(v2) c, string }
// in v1 we might get c
  1. If a single service version validates today that an input is inside a fixed set of known values, but may allow new values in the future, use a fixed union. Accepting new values in the future is not a breaking change.
enum OnlyUsedAsInput {a, b, @added(v2) c }
// sending c in v1 is invalid
  1. If a single service version has an input AND output type that it validates on input but may accept and return new values in the future... I think this is the issue we are talking about.
enum InputAndOutput {a, b, @added(v2) c, string }
// in v1 we might get
// but we want to validate at this point only "a", "b", "c" can be sent
  1. Treat open union as an exhaustive list for validation and TypeScript emitter by default
  2. Decorator to opt-out
  3. Emitter flag to disable validation/behavior
@timotheeguerin timotheeguerin added the design:needed A design request has been raised that needs a proposal label May 15, 2024
@markcowl markcowl modified the milestones: [2024] July, [2024] June May 20, 2024
@markcowl markcowl added the compiler:core Issues for @typespec/compiler label May 20, 2024
@weidongxu-microsoft
Copy link

weidongxu-microsoft commented May 29, 2024

If we have extends, could it be

enum Closed { a, b } for values that never change.
union CurrentValues extends string {"a", "b"} for values that is exhaustive currently, but can change in future. <-- It would be current syntaxly. It would be SDK emitter's job to decide whether it going to generate open enum for the sake of version resiliency.
union WithKnownValues extends string {"a", "b", string} for values that include known values and unknown values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:core Issues for @typespec/compiler design:needed A design request has been raised that needs a proposal triaged:core
Projects
None yet
Development

No branches or pull requests

3 participants