Skip to content

Commit

Permalink
feat: add support for serializing complex objects (#49)
Browse files Browse the repository at this point in the history
Adds a `shouldSerializeObject` function which determines if a value
should be serialized or should attempt to be nested.

The default implementation checks if a value is a non-date object,
meaning all objects which are not dates will trigger nesting.
  • Loading branch information
43081j authored Jul 27, 2024
1 parent 5d0a39f commit d663bd1
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 6 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,50 @@ parse('300=foo', {
// {300: 'foo'}
```

### `shouldSerializeObject`

Can be set to a function which determines if an _object-like_ value should be
serialized instead of being treated as a nested object.

**All non-object primitives will always be serialized.**

For example:

```
// Assuming `StringifableObject` returns its constructor value when `toString`
// is called.
stringify({
foo: new StringifiableObject('test')
}, {
shouldSerializeObject(val) {
return val instanceof StringifableObject;
},
valueSerializer: (value) => {
return String(value);
}
});
// foo=test
```

If you want to fall back to the default logic, you can import the default
function:

```
import {defaultShouldSerializeObject, stringify} from 'picoquery';
stringify({
foo: new StringifiableObject('test')
}, {
shouldSerializeObject(val) {
if (val instanceof StringifableObject) {
return true;
}
return defaultShouldSerializeObject(val);
}
});
```

### `valueSerializer`

Can be set to a function which will be used to serialize each value during
Expand Down
9 changes: 7 additions & 2 deletions src/object-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export function stringifyObject(
arrayRepeatSyntax = defaultOptions.arrayRepeatSyntax,
nesting = defaultOptions.nesting,
delimiter = defaultOptions.delimiter,
valueSerializer = defaultOptions.valueSerializer
valueSerializer = defaultOptions.valueSerializer,
shouldSerializeObject = defaultOptions.shouldSerializeObject
} = options;
const strDelimiter =
typeof delimiter === 'number' ? String.fromCharCode(delimiter) : delimiter;
Expand Down Expand Up @@ -96,7 +97,11 @@ export function stringifyObject(
result += strDelimiter;
}

if (typeof value === 'object' && value !== null) {
if (
typeof value === 'object' &&
value !== null &&
!shouldSerializeObject(value)
) {
valueIsProbableArray = (value as unknown[]).pop !== undefined;

if (nesting || (arrayRepeat && valueIsProbableArray)) {
Expand Down
22 changes: 19 additions & 3 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ export type SerializeValueFunction = (
key: PropertyKey
) => string;

export type ShouldSerializeObjectFunction = (value: unknown) => boolean;

export type DeserializeKeyFunction = (key: string) => PropertyKey;

export function defaultValueSerializer(value: unknown): string {
export const defaultValueSerializer: SerializeValueFunction = (
value: unknown
): string => {
switch (typeof value) {
case 'string':
// Length check is handled inside encodeString function
Expand All @@ -41,8 +45,18 @@ export function defaultValueSerializer(value: unknown): string {
break;
}

if (value instanceof Date) {
return encodeString(value.toISOString());
}

return '';
}
};

export const defaultShouldSerializeObject: ShouldSerializeObjectFunction = (
val
) => {
return val instanceof Date;
};

export interface Options {
// Enable parsing nested objects and arrays
Expand Down Expand Up @@ -70,6 +84,7 @@ export interface Options {
valueDeserializer: DeserializeValueFunction;
keyDeserializer: DeserializeKeyFunction;
valueSerializer: SerializeValueFunction;
shouldSerializeObject: ShouldSerializeObjectFunction;
}

const identityFunc = <T>(v: T): T => v;
Expand All @@ -82,5 +97,6 @@ export const defaultOptions: Options = {
delimiter: 38,
valueDeserializer: identityFunc,
valueSerializer: defaultValueSerializer,
keyDeserializer: identityFunc
keyDeserializer: identityFunc,
shouldSerializeObject: defaultShouldSerializeObject
};
20 changes: 20 additions & 0 deletions src/test/object-util_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,24 @@ test('stringifyObject', async (t) => {
await t.test('null values result in empty string', () => {
assert.deepEqual(stringifyObject({foo: null}, {}), 'foo=');
});

await t.test('custom shouldSerializeObject function', () => {
const foo = {
toString() {
return 'bar';
}
};
const obj = {
foo
};
const result = stringifyObject(obj, {
shouldSerializeObject: (val) => {
return val === foo;
},
valueSerializer: (val) => {
return String(val);
}
});
assert.equal(result, 'foo=bar');
});
});
19 changes: 18 additions & 1 deletion src/test/stringify_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
import {test} from 'node:test';
import {stringify} from '../main.js';
import {testCases} from './test-cases.js';
import {encodeString} from '../string-util.js';

test('stringify', async (t) => {
for (const testCase of testCases) {
Expand Down Expand Up @@ -40,7 +41,23 @@ test('stringify', async (t) => {
assert.equal(result, 'foo=400');
});

await t.test('skips infinite numbers', () => {
await t.test('date values', () => {
const date = new Date('2000-01-01');
const result = stringify({foo: date});
assert.equal(result, `foo=${encodeString(date.toISOString())}`);
});

await t.test('complex objects', () => {
const cls = class {
foo = 123;
bar = 456;
};
const instance = new cls();
const result = stringify(instance);
assert.equal(result, 'foo=123&bar=456');
});

await t.test('stringifies infinite numbers as empty', () => {
const result = stringify({foo: Infinity});
assert.equal(result, 'foo=');
});
Expand Down

0 comments on commit d663bd1

Please sign in to comment.