-
-
Notifications
You must be signed in to change notification settings - Fork 735
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
Feature> emptyValue options for parse & stringify #226
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.
Parsing works properly, but I have a question regarding the best way to handle a certain stringification behavior. Thanks!
lib/stringify.js
Outdated
@@ -27,13 +29,15 @@ var defaults = { | |||
}, | |||
skipNulls: false, | |||
strictNullHandling: false | |||
// emptyValue: some flag |
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'm not sure what the best way to handle this case is, as there needs to way to recognize "user provided no option" so the default behavior of outputting foo=
as opposed to foo
can be used.
I've seen things like using the global Infinity
, storing the option in an object and using hasOwnProperty
, etc. among others but not sure what you'd prefer. Thanks!
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.
Hmm, I don't think I'm understanding the question here. Can you restate? A test case (that currently fails) would be most helpful.
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.
Yeah -- let's say we don't provide a default. This means when stringifying { foo: undefined }
we get 'foo'
whereas before we get ''
(line 77). If we do provide a default, stringifying { foo: [whatever our chosen default is]}
gives 'foo'
instead of whatever it gave before my change. So without some more involved code, stringifying whatever value we choose as the default would change from whatever it was before to 'foo'
.
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 like this not to be a breaking change, so qs.stringify({ foo: undefined })
needs to return the same value in master as it does in 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.
cc @timhwang21, have you had any time to work 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.
In all honesty, no, this entirely fell off my radar. Will catch up on the past few years of discussion 😅
@timhwang21 is this still something you're interested in? |
@ljharb sure! Sorry this fell off my radar as we ended up solving the problem we were facing on our end. |
5713e30
to
ea789cc
Compare
lib/stringify.js
Outdated
if (obj === null) { | ||
if (strictNullHandling) { | ||
if (obj === null || obj === emptyValue) { | ||
if (strictNullHandling || (obj === emptyValue && emptyValue !== null)) { | ||
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset) : prefix; |
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.
This code is sort of confusing, but I think this results in the most expected outcomes:
- No options:
{ a: null }
becomes'a='
strictNullHandling
:{ a: null }
becomes'a'
emptyValue: 'something'
:{ a: 'something' }
becomes'a'
emptyValue: null
:{ a: null }
becomes'a='
(corner case; fornull
we defer tostrictNullHandling
)emptyValue: null
+strictNullHandling
:{ a: null }
becomes'a'
(same as case 1;emptyValue: null
is redundant here)
The relevant cases for this patch are 3, 4 and 5. Without the additional check on line 71, case 3 would give 'a='
. However to me it seems if the user provides emptyValue
they implicitly want "strict empty handling" to be true, as I don't think returning 'a='
for 3 is ever a useful outcome.
To avoid changing existing behavior, when emptyValue
is null
we require strictNullHandling
on top of that.
An alternative would be to make emptyValue
ALWAYS assume strictNullHandling
is true, even for null
. We'd have to change the default value from null
to some flag that would never be used as an actual value, like Infinity
or something.
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.
Hmm, I'm not sure I agree - a=
explicitly means "empty" to me, whereas a
could be shorthand for a: true
. I think I might expect case 3 to become a=
, and case 6 (emptyValue: 'something'
+ strictNullHandling
) makes { a: 'something' }
become a
?
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 do agree that case 3 giving a=
is more expected, but I think it's less useful. I'm imagining the following:
- User does
qs.parse('foo&bar=1', { emptyValue: true })
and gets{ foo: true, bar: 1 }
- User increments
bar
by 1 and re-serializes:qs.stringify({ foo: true, bar: 2 }, { emptyValue: true })
. I'd expect to getfoo&bar=2
which follows the format of the original input, notfoo=&bar=2
here.
Regardless... I do agree that requiring strictNullHandling: true
for this behavior for stringify
does feel more logical. Returning a
just strikes me as more in line with the use case this PR is meant to address.
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 guess the confusion is because we have an implicit empty value of null, and a separate “strict null handling” var. if it was “strict empty value handling”, then it’d be easy to make it make sense. I’m not sure what the best approach here is.
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. What if we move the check for emptyValue
to a block under the check for null
? Because line 75 sets obj = ''
if !strictNullHandling
, obj
will never equal emptyValue
's implicit default value of null
. Then the concerns will be more segregated.
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 think it makes sense to forbid the strictNullHandling option entirely when emptyValue is not null.
However, then it may make sense to add a new option, strictEmptyHandling, that subsumes strictNullHandling in all cases?
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.
+1 on the first part, that makes it more consistent with parse
behavior.
I'm honestly still unconvinced on the second part, as I still don't think anyone would ever want to use stringify
+ emptyValue
without also wanting "strict" behavior. It just seems like it would lead to user error since the desired outcome requires emptyValue
to always be used in tandem with strictFooHandling
...
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.
Both with and without the strict option, what’s the roundtrip behavior of a null?
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.
stringify({ a: null })
// => 'a='
parse('a=')
// => { a: '' }
// strict - matches
stringify({ a: null }, { strictNullHandling: true })
// => 'a'
parse('a', { strictNullHandling: true })
// => { a: null }
I'd expect emptyValue: 'foobar'
to basically be strictFoobarHandling: true
:
stringify({ a: 'foobar' }, { emptyValue: 'foobar' })
// => 'a'
parse('a', { emptyValue: 'foobar' })
// => { a: 'foobar' }
lib/stringify.js
Outdated
if (obj === null) { | ||
if (strictNullHandling) { | ||
if (obj === null || obj === emptyValue) { | ||
if (strictNullHandling || (obj === emptyValue && emptyValue !== null)) { | ||
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset) : prefix; |
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.
Hmm, I'm not sure I agree - a=
explicitly means "empty" to me, whereas a
could be shorthand for a: true
. I think I might expect case 3 to become a=
, and case 6 (emptyValue: 'something'
+ strictNullHandling
) makes { a: 'something' }
become a
?
ea789cc
to
d159599
Compare
I suggest the following.
I believe interpreting the presense of a key as its value being truthy is up to the users. If how others interpret the stringified is concerned, why don’t you add “Do not convey (do omit) the presense of |
Just adding a note that I'm interested in this functionality too. While I mostly agree with @issuefiler's desired behaviour ( Given that truthy/falsy checks can be tricky and non-obvious to reason about, it's worth being careful to reduce impact to existing codebases (especially since there are many of them) when they upgrade |
@ljharb after thinking about this some more, I'm liking the idea of Existing options:
New options:
Edge cases:
Summary:
Please let me know what you think. Thanks! |
d159599
to
66f1bf6
Compare
@timhwang21 I like your new API suggestion. I like throwing an error when conceptually conflicting options are passed. I'm not sure I understand the last bullet point in the "Summary", but if you're able to update the PR, maybe it will become clear to me :-) |
@timhwang21 do you no longer have this use case? I'd still love to see this PR updated to match #226 (comment) so I can land it :-) |
I do not, and I unfortunately am not going to have time to work on this. I'm cleaning up my stale PRs because my dashboard is getting hard to manage. |
It's really unfortunate that github's PR dashboard incentivizes people to close old PRs :-/ if this stays open, then someone else can take up the work very easily. |
Addresses #223.
Behavior:
opt.emptyValue (default to '')
without a value