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

fix: allow parsing of empty keys in objects #433

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Coobaha
Copy link
Contributor

@Coobaha Coobaha commented Mar 5, 2022

Resolves #432

  • to add an option to both parse and stringify
  • to have tests, for both parse and stringify, for at least:
    • repeat keys with all array formats
    • bracketed keys with all array formats
    • a lone = (and =& and &= and &=&)
    • ❌ deep array/object case

If we are happy with this implementation and want to proceed:

Other:

  • Stringify works without an option, so it is potentially a patch but altering parse behaviour only via option seems a safe bet to not accidentally break ppl code, and can be considered as a default behaviour in the next major version?

lib/parse.js Show resolved Hide resolved
test/parse.js Outdated
st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3');
st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
st.deepEqual(qs.parse('[][0]=2&[][1]=3', { allowEmptyKeys: true }), { '': { '': ['2', '3'] } });
st.deepEqual(qs.parse('[][0]=2&[][1]=3&[a]=2', { allowEmptyKeys: true }), { '': { '': ['2', '3'], a: '2' } });
Copy link
Contributor Author

@Coobaha Coobaha Mar 5, 2022

Choose a reason for hiding this comment

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

This case fails, but this is ambiguous string which is treated as { : [ '2', '3' ] } and stringify <-> parse is not producing the same results

  ---
    operator: deepEqual
    expected: |-
      { : { : [ '2', '3' ] } }
    actual: |-
     { : [ '2', '3' ] }

    operator: deepEqual
    expected: |-
      { : { : [ '2', '3' ], a: '2' } }
    actual: |-
      { : { 0: '2', 1: '3', a: '2' } }

Copy link
Owner

Choose a reason for hiding this comment

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

it's ok to mark this case as a TODO for now; there's an existing issue about a nested array/object combo

@ljharb
Copy link
Owner

ljharb commented Mar 5, 2022

There's unlikely to be a major version any time soon; breaking changes just aren't worth it. Configuring it via option is the best bet for now.

Copy link
Owner

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

At the very least, let's add stringify tests using all of the outputs in the parse tests as input.

lib/parse.js Show resolved Hide resolved
lib/parse.js Outdated Show resolved Hide resolved
test/parse.js Outdated

test('parse/stringify empty key WIP', function (t) {
t.test('parses an object with empty string key', function (st) {
st.deepEqual(qs.parse('=a', { allowEmptyKeys: true }), { '': 'a' });
Copy link
Owner

Choose a reason for hiding this comment

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

for each of these test inputs, let's test both allowEmptyKeys true, false, and absent

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, please let me know what will be the correct place for this tests? They are testing both parse and stringify and current tests are split by this methods. To me it feels more reasonable to test them together in this case.

test/parse.js Outdated
st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3');
st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
st.deepEqual(qs.parse('[][0]=2&[][1]=3', { allowEmptyKeys: true }), { '': { '': ['2', '3'] } });
st.deepEqual(qs.parse('[][0]=2&[][1]=3&[a]=2', { allowEmptyKeys: true }), { '': { '': ['2', '3'], a: '2' } });
Copy link
Owner

Choose a reason for hiding this comment

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

it's ok to mark this case as a TODO for now; there's an existing issue about a nested array/object combo

test/parse.js Outdated
Comment on lines 887 to 894
st.deepEqual(qs.stringify(parsed, { encode: false }), testCase.stringifyOutput);
st.deepEqual(qs.parse(testCase.stringifyOutput, { allowEmptyKeys: true }), testCase.withEmptyKeys);
Copy link
Owner

Choose a reason for hiding this comment

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

i'd prefer to keep stringify tests in the stringify tests file; it's fine if the test cases are in a separate file both test files require.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey, sorry for the delay :) i've extracted test cases into own file and and separated stringify/parse parts accordingly

@codecov
Copy link

codecov bot commented May 15, 2022

Codecov Report

Patch coverage: 100.00% and no project coverage change.

Comparison is base (a5609c7) 99.80% compared to head (5e70a6a) 99.80%.

❗ Current head 5e70a6a differs from pull request most recent head c077991. Consider uploading reports for the commit c077991 to get more accurate results

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #433   +/-   ##
=======================================
  Coverage   99.80%   99.80%           
=======================================
  Files           9        9           
  Lines        1518     1531   +13     
  Branches      177      179    +2     
=======================================
+ Hits         1515     1528   +13     
  Misses          3        3           
Impacted Files Coverage Δ
test/empty-keys-cases.js 100.00% <ø> (ø)
lib/parse.js 100.00% <100.00%> (ø)
test/parse.js 99.80% <100.00%> (+<0.01%) ⬆️
test/stringify.js 99.79% <100.00%> (+<0.01%) ⬆️

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

Copy link
Owner

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

Looking great!

Comment on lines +874 to +926
qs.parse('[][0]=2&[][1]=3', { allowEmptyKeys: true }),
{ '': { '': ['2', '3'] } },
'array/object conversion',
{ skip: 'TODO: figure out what this should do' }
);

st.deepEqual(
qs.parse('[][0]=2&[][1]=3&[a]=2', { allowEmptyKeys: true }),
{ '': { '': ['2', '3'], a: '2' } },
'array/object conversion',
{ skip: 'TODO: figure out what this should do' }
Copy link
Owner

Choose a reason for hiding this comment

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

let's discuss these TODOs. what are your thoughts?

Copy link
Contributor Author

@Coobaha Coobaha May 21, 2022

Choose a reason for hiding this comment

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

Since we parse empty keys I assume output should be equal to key-full strings?

  • If we use a[b][0]=2&a[b][1]=3 the output object is { a: { b: [ '2', '3' ] } }
  • when we replace a and b with empty strings in this object: { '': { '': [ '2', '3' ] } }
    • so query string equivalent should be [][0]=2&[][1]=3 which is <EMPTY_KEY>[<EMPTY_KEY>][0]=2

Current output for [][0]=2&[][1]=3 without empty keys flag is { 0: '2', 1: '3' }

test('stringifies empty keys', function (t) {
emptyTestCases.forEach(function (testCase) {
t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
st.deepEqual(qs.stringify(testCase.withEmptyKeys, { encode: false }), testCase.stringifyOutput);
Copy link
Owner

Choose a reason for hiding this comment

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

can we also add a test case where the input is a stringified noEmptyKeys?

@ljharb ljharb force-pushed the empty-keys-fix branch 2 times, most recently from 5e70a6a to c077991 Compare May 11, 2023 06:24
Comment on lines +5 to +10
{ input: '&', withEmptyKeys: {}, stringifyOutput: '', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
{ input: '&&', withEmptyKeys: {}, stringifyOutput: '', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
{ input: '&=', withEmptyKeys: { '': '' }, stringifyOutput: '=', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
{ input: '&=&', withEmptyKeys: { '': '' }, stringifyOutput: '=', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
{ input: '&=&=', withEmptyKeys: { '': ['', ''] }, stringifyOutput: '[0]=&[1]=', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
{ input: '&=&=&', withEmptyKeys: { '': ['', ''] }, stringifyOutput: '[0]=&[1]=', noEmptyKeys: {}, stringifyOutputNoEmpty: '' },
Copy link
Owner

Choose a reason for hiding this comment

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

it makes me very nervous to be modifying test cases for a bug fix. is there any chance for a default algorithm to infer stringifyOutputNoEmpty rather than needing to add it to each case?

Copy link
Contributor Author

@Coobaha Coobaha May 11, 2023

Choose a reason for hiding this comment

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

🤷 I think explicit cases are better here. I assume you thought that we should default to '' if you want a smaller diff, then it also makes to do the same for stringifyOutput and withEmptyKeys, which will be hard to maintain.

Copy link
Owner

Choose a reason for hiding this comment

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

Explicit cases are great, it’s just that the best way to be sure there’s no breaking changes is to not have a diff on existing test cases :-)

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.

qs.parse omitting empty string keys
2 participants