Skip to content

Commit

Permalink
feat: Serialize plain objects to dot path notation
Browse files Browse the repository at this point in the history
  • Loading branch information
razor-x committed Feb 24, 2025
1 parent ca89e53 commit d9ba3c3
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 13 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,20 @@ which encodes most non-alphanumeric characters.
- The array `{ foo: [1, 2] }` serializes to `foo=1&foo=2`.
- The single element array `{ foo: [1] }` serializes to `foo=1`.
- The empty array `{ foo: [] }` serializes to `foo=`.
- Serialization of the single element array containing the empty string is not supported
and will throw an `UnserializableParamError`.
- Serialization of arrays containing `null` or `undefined` values
is not supported and will throw an `UnserializableParamError`.
- Serialization of the single element array containing the empty string
is not supported and will throw an `UnserializableParamError`.
Otherwise, the serialization of `{ foo: [''] }` would conflict with `{ foo: [] }`.
This serializer chooses to support the more common and more useful case of an empty array.
- Serialization of functions or other objects is not supported
and will throw an `UnserializableParamError`.
- Serialization of objects and nested objects first serializes the keys
- to dot-path format and then serializes the values as above, e.g.,
`{ foo: 'a', bar: { baz: 'b', fizz: [1, 2] } }` serializes to
`foo=a&bar.baz=b&bar.fizz=1&bar.fizz=2`.
- Serialization of nested arrays or objects nested inside arrays
is not supported and will throw an `UnserializableParamError`.
- Serialization of functions or other objects is
is not supported and will throw an `UnserializableParamError`.

## Installation

Expand Down
1 change: 1 addition & 0 deletions examples/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const handler: Handler<Options> = async ({ logger }) => {
f: [],
g: new Date(),
h: Temporal.Now.instant(),
i: { foo: 1, bar: { baz: 2, fizz: [1, 'a'] } },
},
})
logger.info({ data }, 'Response')
Expand Down
22 changes: 19 additions & 3 deletions src/lib/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isDateLike, isTemporalInstantLike } from './date.js'
import { isPlainObject } from './object.js'

type Params = Record<string, unknown>

Expand All @@ -12,7 +13,24 @@ export const updateUrlSearchParams = (
searchParams: URLSearchParams,
params: Record<string, unknown>,
): void => {
for (const [name, value] of Object.entries(params)) {
nestedUpdateUrlSearchParams(searchParams, params, [])
searchParams.sort()
}

const nestedUpdateUrlSearchParams = (
searchParams: URLSearchParams,
params: Record<string, unknown>,
path: string[],
): void => {
for (const [key, value] of Object.entries(params)) {
const currentPath = [...path, key]
if (isPlainObject(value)) {

Check failure on line 27 in src/lib/serialize.ts

View workflow job for this annotation

GitHub Actions / Format code

Unexpected any value in conditional. An explicit comparison or type cast is required
nestedUpdateUrlSearchParams(searchParams, value, currentPath)
return
}

const name = currentPath.join('.')

if (value == null) continue

if (Array.isArray(value)) {
Expand All @@ -31,8 +49,6 @@ export const updateUrlSearchParams = (

searchParams.set(name, serialize(name, value))
}

searchParams.sort()
}

const serialize = (k: string, v: unknown): string => {
Expand Down
55 changes: 49 additions & 6 deletions test/serialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,54 @@ test('serializes Temporal.Instant', (t) => {
)
})

test('cannot serialize unserializable values', (t) => {
t.throws(() => serializeUrlSearchParams({ foo: {} }), {
instanceOf: UnserializableParamError,
})
t.throws(() => serializeUrlSearchParams({ foo: { x: 2 } }), {
test('serializes plain objects', (t) => {
t.is(
serializeUrlSearchParams({
foo: 1,
bar: { baz: 'a' },
}),
'bar.baz=a&foo=1',
)

t.is(
serializeUrlSearchParams({
foo: 1,
bar: { baz: { x: { z: 1 } } },
}),
'bar.baz.x.z=1&foo=1',
)

t.is(
serializeUrlSearchParams({
foo: 1,
bar: { baz: { x: { z: null } } },
}),
'foo=1',
)

t.is(
serializeUrlSearchParams({
foo: 1,
bar: { baz: [1, 'a'] },
}),
'bar.baz=1&bar.baz=a&foo=1',
)
})

test('cannot serialize functions', (t) => {
t.throws(() => serializeUrlSearchParams({ foo: () => {} }), {
instanceOf: UnserializableParamError,
})
t.throws(() => serializeUrlSearchParams({ foo: () => {} }), {
})

test('cannot serialize non-plain objects', (t) => {
class Foo {
bar: string
constructor() {
this.bar = 'a'
}
}
t.throws(() => serializeUrlSearchParams({ foo: new Foo() }), {
instanceOf: UnserializableParamError,
})
})
Expand All @@ -119,6 +159,9 @@ test('cannot serialize array params with unserializable values', (t) => {
t.throws(() => serializeUrlSearchParams({ bar: ['a', []] }), {
instanceOf: UnserializableParamError,
})
t.throws(() => serializeUrlSearchParams({ bar: ['a', ['']] }), {
instanceOf: UnserializableParamError,
})
t.throws(() => serializeUrlSearchParams({ bar: ['a', {}] }), {
instanceOf: UnserializableParamError,
})
Expand Down

0 comments on commit d9ba3c3

Please sign in to comment.