-
Notifications
You must be signed in to change notification settings - Fork 349
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
feat: Add useOptionals=all to enable non-field members to be optional. #402
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! A few questions but otherwise lgtm direction wise.
src/main.ts
Outdated
${entryWriteSnippet} | ||
}); | ||
if (message.${fieldName} !== undefined && Object.keys(message.${fieldName}).length !== 0) { | ||
Object.entries(message.${fieldName}).forEach(([key, value]) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would if we could make this Object.entries(message.foo || []).forEach
. I know it's slightly less efficient, in terms of making a throw away []
and lambda
if the field is not set, but I'm thinking that @webmaster128 just did several PRs that a lot of explicit message.field !== undefined && ...
checks to reduce the code size ouptut.
So, I suppose it's a trade-off, do you prefer less code w/a few extra allocations, or more code w/a few less allocations?
I suppose in an ideal world, ts-proto would let the user pick, but we already have a ton of flags, so dunno, I think I'd prefer to just pick one.
@sgielen wdyt about || []
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd not worry about performance too much. JIT engines run this stuff amazingly fast these days. The better the optimizer understands the intend of the code, the better.
I'd use Object.entries(message.foo ?? {})
to
- Only use the fallback in case of
null
andundefined
and not bugs like""
,0
orfalse
, - Always have an onject as input to
Object.entries(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this extra undefined check is only needed in case of forceOptionals = true
? So maybe the code output should only be generated in that case. Same in the diff below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can switch to || []
and || {}
no problem.
I'll respond to the "extra undefined check" comment in another thread on this PR. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although - if we switch to || {}
, we do put empty maps on the wire (as in your other comment about lists), while if we keep if (.. !== undefined)
, we don't put the map on the wire. Shouldn't it be preferred not to put the map on the wire if it's undefined or empty? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derp, nevermind -- an empty map doesn't actually cause any writer calls, so there's no on-wire difference between an empty map and not writing the map at all. So, this is good to go.
src/main.ts
Outdated
writer.uint32(${tag}).fork(); | ||
for (const v of message.${fieldName}) { | ||
writer.${toReaderCall(field)}(${toNumber}(v)); | ||
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike the other spot where I suggested || []
, it does seem nice here to keep the tag completely off the wire if there are no elements in the collection.
src/main.ts
Outdated
(options.useOptionals && isMessage(field) && !isRepeated(field)) || | ||
(options.forceOptionals && !messageOptions?.mapEntry) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per discussion, I'm good with removing options.forceOptionals
and just remove the isMessage
/ !isRepeated
from the existing check.
Also I assume !...mapEntry
is in here just b/c there are some nuances to making it optional that you didn't want to tackle in this PR? If so, maybe just add an in-source comment/todo that it's for later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, otherwise I had to assert that the Map entry key is set in various places, which seems silly as the Map entry key is mandatory. I'll explain it a bit better in the comment.
src/types.ts
Outdated
@@ -251,28 +251,28 @@ export function notDefaultCheck(ctx: Context, field: FieldDescriptorProto, place | |||
const zerothValue = enumProto.value.find((v) => v.number === 0) || enumProto.value[0]; | |||
if (options.stringEnums) { | |||
const enumType = messageToTypeName(ctx, field.typeName); | |||
return code`${place} !== ${enumType}.${zerothValue.name}`; | |||
return code`${place} !== undefined && ${place} !== ${enumType}.${zerothValue.name}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, maybe we could do:
const maybeNotUndefined = options.useOptionals ? `${place} !== undefined && ` : "";
At the start of this method, and then in these places do:
return code`${maybeNotUndefined} ${place} !== ${enumType}.${zerothValue.name}`;
So that users that don't have useOptional
s set won't have the unnecessary !== undefined
in their output.
Granted, it doesn't hurt, but I think I'd like to keep it out if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually had problems with this before implementing force/useOptionals
. Suppose that you have a message FooRequest with a repeated int64 list = 0;
(remember you can't mark repeateds as optional), and an RPC SendFoo(FooRequest)
. This code, intended to send an empty list (as with Go semantics), compiles but then triggers a runtime error:
SendFoo({} as FooRequest)
The error triggered is quite deep inside ts-proto and reads something like "list is not iterable", regardless of the value of force/useOptionals. IMHO, it is a small price to pay to always do the undefined check to allow this code to work normally.
If you strongly prefer to only enable this behaviour with force/useOptionals enabled then I can apply your suggestion, but personally I would strongly prefer not to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, so for SendFoo({} as FooRequest)
, I would have expected that to go through Foo.fromPartial({})
which would have ended up doing message.list = (object.list ?? []).map(e => e)
. I.e. like in simple.ts
:
message.coins = (object.coins ?? []).map((e) => e);
But it's hard to say without seeing the stack trace...
I definitely get why this change is necessary with forceOptionals
, b/c undefined
are now valid values of these fields; but w/o that, the field shouldn't be undefined
, and if it's not, that seems like the bug I'd rather fix. Granted, these is something to be said for defensive coding, but code / output size is already a concern, so, dunno, yeah, would appreciate doing maybeNotUndefined
here, at least for this PR, and we can follow up on "list is not iterable" as a separate PR/bug.
Unless I'm missing something about "the field should never be undefined
" in the SendFoo({})
use case that means it's not going through fromPartial
and having the ?? []
applied that way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I'll revert to only add the undefined-check if useOptionals
is set and make a separate issue for the []
being undefined in a partial object, so we can discuss it further there. :)
Rebased and updated. I've kept the flag There's still two open points AFAIK:
|
Cool, yeah, I think now is a good time to do that.
I'd like to keep just the existing
I think this should let us achieve no breaking changes for current Wdyt/sgty? |
Yes, sounds good to me! I'll make a tiny change and make |
05c8475
to
f44a6da
Compare
Both changes now implemented. This also means that rerunning codegen is not necessary: the codegen output is now equal if I think this is ready for final review. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sgielen looks great! Thanks for iterations on this.
If you want to also update the readme, that'd be great, but that's also fine to do as a follow up/later.
${entryWriteSnippet} | ||
}); | ||
`); | ||
} else if (packedType(field.type) === undefined) { | ||
chunks.push(code` | ||
const listWriteSnippet = code` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, this is a nice way of conditionally adding this behavior.
@@ -32,7 +32,7 @@ export type Options = { | |||
context: boolean; | |||
snakeToCamel: boolean; | |||
forceLong: LongOption; | |||
useOptionals: boolean; | |||
useOptionals: boolean | 'none' | 'messages' | 'all'; // boolean is deprecated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One option would be to make this type:
useOptionals: ['messages' | 'primitives'];
And then around in here:
https://github.com/stephenh/ts-proto/blob/main/src/options.ts#L122
Do a translation of true
-> [messages, primitives]
, false
/ none
-> []
.
Hm, I guess parseParameter
is not fancy enough to like detect ,
and do a .split(
,)
. It probably could though, and seems like a reusable thing.
If you want to add this as part of this PR, that'd be great, but also fine to merge as-is.
6b27525
to
48bf9fa
Compare
@sgielen I went ahead and rebased this due to a minor import conflict and am going to merge. Thanks for the PR! |
# [1.95.0](v1.94.0...v1.95.0) (2021-12-14) ### Features * Add useOptionals=all to enable non-field members to be optional. ([#402](#402)) ([e7b70cb](e7b70cb))
🎉 This PR is included in version 1.95.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
A little late, but great to see this PR, as we had a local patch to do the same thing (we parse JSON responses from our gRPC servers directly into types generated by However, README.markdown is not updated with |
@oyvindwe thanks for the call out; I've updated the |
This PR also adds a test to verify writing empty objects and reading them back.