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

Compiler flag oneof=unions generates type unions for oneof fields instead of individual properties #95

Merged
merged 1 commit into from
Jul 11, 2020

Conversation

philikon
Copy link
Contributor

Addresses #74

@philikon
Copy link
Contributor Author

I can't see what the failure is, circleci wants me to log in. :(

@cliedeman
Copy link
Contributor

Summary of all failing tests
 
 FAIL  integration/simple/simple-oneofs-test.ts
 
  ● Test suite failed to run
 

 
    ENOMEM: not enough memory, read
 

 
      at ScriptTransformer._getTransformer (node_modules/@jest/transform/build/ScriptTransformer.js:355:19)
 
      at ScriptTransformer.transformSource (node_modules/@jest/transform/build/ScriptTransformer.js:444:28)
 

 
 FAIL  integration/bytes-as-base64/bytes-as-base64-test.ts
 
  ● Test suite failed to run
 

 
    ENOMEM: not enough memory, read
 

 
      at Object.<anonymous> (node_modules/bs-logger/dist/logger/index.js:36:15)
 

A transient failure

@philikon This looks great. Will give it a try on my project

@cliedeman
Copy link
Contributor

@philikon one thing I noticed

    PleaseChoose.fromPartial({
      name: 'Debbie',
      age: 37,
      choice: { aBool: true, aString: 'a' }, // Valid
      choice: { $case: 'aBool', aBool: true, aString: 'a' }, // Invalid
      eitherOr: { or: 'perhaps not' },
    });

the From Partial allows the user to omit the case.

This seems to fix it. but here be dragons...

type DeepPartial<T> = T extends Builtin
  ? T
  : T extends Array<infer U>
  ? Array<DeepPartial<U>>
  : T extends ReadonlyArray<infer U>
  ? ReadonlyArray<DeepPartial<U>>
  : T extends { $case: string }
  // Try to make $case mandatory
  ? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { $case: T['$case'] }
  : T extends {}
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

This forces the user to allows provide a $case

@cliedeman
Copy link
Contributor

A more conceptual issue:

Proto

  oneof choice {

    // Use this if you want a number. Numbers are great. Who doesn't
    // like them?
    double a_number = 2;

    // Use this if you want a string. Strings are also nice. Not as
    // nice as numbers, but what are you going to do...
    string a_string = 3;

    Submessage a_message = 4;

    // We also added a bool option! This was added after the 'age'
    // field, so it has a higher number.
    bool a_bool = 6;

    bytes buncha_bytes = 10;
  }

Generated Code

  choice?:
    | { $case: 'aNumber'; aNumber: number }
    | { $case: 'aString'; aString: string }
    | { $case: 'aMessage'; aMessage: PleaseChoose_Submessage | undefined }
    | { $case: 'aBool'; aBool: boolean }
    | { $case: 'bunchaBytes'; bunchaBytes: Uint8Array };

I think that the generated code should actually be

  choice?:
    | { $case: 'aNumber'; aNumber: number }
    | { $case: 'aString'; aString: string }
    | { $case: 'aMessage'; aMessage: PleaseChoose_Submessage /* undefined removed */ }
    | { $case: 'aBool'; aBool: boolean }
    | { $case: 'bunchaBytes'; bunchaBytes: Uint8Array };

my reasoning is that these 2 cases are equivalent from a protobuf point of view

{ choice: { '$case': 'aMessage', aMessage: undefined }}
{ choice: undefined }

This also allows us to remove some tedious if statements

@philikon
Copy link
Contributor Author

philikon commented Jun 19, 2020

the From Partial allows the user to omit the case.

Yes, I think that's the whole point of fromPartial yes? :)

I think that the generated code should actually be
| { $case: 'aMessage'; aMessage: PleaseChoose_Submessage /* undefined removed */ }

Hmm, I need to think about this some more, but I think you might be right. Either you chose the aMessage case and then you need to provide the value, or you didn't want to provide a value at all, but then there's no point to provide a $case. Yeah, I think that makes sense, but before I go back into the code, I might want @stephenh and @timostamm to weigh in.

@philikon
Copy link
Contributor Author

(Btw, in your example you can do switch (data.choice?.$case) and avoid the outermost if that way.)

@cliedeman
Copy link
Contributor

Yes, I think that's the whole point of fromPartial yes? :)

Definitely. I guess its just a bit of a footgun because erroneous code would compile. But this was not the goal of the PR I think, because the existing code has the same issue that you attempt to set multiple oneOf fields.

I think a runtime exception in this case might be good enough? I tried to spin up a comparative example in go but the generation style prevents settings 2 cases of a one of field.

but then there's no point to provide a $case

I almost came to the same conclusion but then there is no clean way to narrow the type

switch (data.choice?.$case)

Thanks :), I was being intentionally pedantic to make sure typescript was doing the right thing.

@timostamm
Copy link
Contributor

Wow, looks good, @philikon :)

{ choice: { '$case': 'aMessage', aMessage: undefined }}

I think $case should only point to aMessage when it has a value. But hey, this is miles better than before. The user should be well aware now that this is a oneof construct. Before, you would have to look at the .proto to notice.

Regarding fromPartial(): I think @cliedeman is on the right track. The following seems to work out for me:

export type PartialMessage<T> =
      T extends (Date | Uint8Array | Long | boolean | string | number) ?
        T
    : T extends Array<infer U> ?
        Array<PartialMessage<U>>
    : T extends ReadonlyArray<infer U> ?
        ReadonlyArray<PartialMessage<U>>
    : T extends { oneofKind: string } ?
        T
    : T extends { oneofKind: undefined } ?
        T
    : T extends {} ?
        { [K in keyof T]?: PartialMessage<T[K]> }
    : Partial<T>;

You can read it like a if, else if, ... else statement.

Don't think that this is a drop-in replacement, though. Have been working on yet another typescript plugin... Using the compiler api to generate .ts from .proto. Seems to work alright so far.

@philikon
Copy link
Contributor Author

I think I understand the fromPartial concern now. @cliedeman, your example illustrates it perfectly and I think your proposed solution

T extends { $case: string }
  ? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { $case: T['$case'] }

works great. I think it's fine to require that if choice is filled out, you have to provide $case, but you don't have to provide anything else. I'll update the PR accordingly.

I'll also remove the optionality of message payloads inside the oneof as discussed.

@philikon philikon force-pushed the oneofUnions branch 2 times, most recently from 5798b3a to 2cf9ebe Compare June 21, 2020 03:55
@philikon
Copy link
Contributor Author

@stephenh any chance you could look at this over the next few days?

@philikon
Copy link
Contributor Author

philikon commented Jul 6, 2020

Ping?

@stephenh
Copy link
Owner

stephenh commented Jul 6, 2020

@philikon yep, sorry, thanks for the patience; I spent my free cycles over the weekend hacking on something else. Will get to this this week.

@philikon
Copy link
Contributor Author

philikon commented Jul 6, 2020

Awesome, thanks

Copy link
Owner

@stephenh stephenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really great, will merge and release. Thanks!

//
// When oneof=unions, we generate a single property for the entire `oneof`
// clause, spelling each option out inside a large type union. No need for
// union with `undefined` here, either.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment, thanks!

if (options.oneof === OneofOption.UNIONS) {
oneofCase = `
: T extends { $case: string }
? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { $case: T['$case'] }`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


// Ideally we'd put the comments for each oneof field next to the anonymous
// type we've created in the type union above, but ts-poet currently lacks
// that ability. For now just concatenate all comments into one big one.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the in-source comment.

@@ -604,6 +679,9 @@ function generateDecode(
.addStatement(`message.%L.push(%L)`, fieldName, readSnippet)
.endControlFlow();
}
} else if (isWithinOneOf(field) && options.oneof === OneofOption.UNIONS) {
let oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func.addStatement(`message.%L = {$case: '%L', %L: %L}`, oneofName, fieldName, fieldName, readSnippet);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, that wasn't too bad. Nice.

@stephenh stephenh merged commit ce3ca37 into stephenh:master Jul 11, 2020
@philikon philikon deleted the oneofUnions branch July 14, 2020 01:24
@philikon
Copy link
Contributor Author

Woot! Thank you!

zfy0701 added a commit to sentioxyz/ts-proto that referenced this pull request Jan 5, 2023
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.

4 participants