Skip to content

Commit a613ae1

Browse files
committed
Add serialization methods
1 parent f6ad46a commit a613ae1

File tree

14 files changed

+320
-46
lines changed

14 files changed

+320
-46
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
[![npm](https://img.shields.io/npm/v/@seamapi/url-search-params-serializer.svg)](https://www.npmjs.com/package/@seamapi/url-search-params-serializer)
44
[![GitHub Actions](https://github.com/seamapi/url-search-params-serializer/actions/workflows/check.yml/badge.svg)](https://github.com/seamapi/url-search-params-serializer/actions/workflows/check.yml)
55

6-
Serializes JavaScript objects to The URLSearchParams.
6+
Serializes JavaScript objects to URLSearchParams.
77

88
## Description
99

10-
TODO
10+
Defines the standard for how Seam SDK and Seam API consumers
11+
should serialize objects to [URLSearchParams] in HTTP GET requests.
12+
13+
Serves as a reference implementation for Seam SDKs in other languages.
14+
15+
See the test for the [Serialization behavior](./test/serialization.test.ts).
16+
17+
[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
1118

1219
## Installation
1320

examples/axios.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import axios from 'axios'
2+
import type { Builder, Command, Describe, Handler } from 'landlubber'
3+
4+
import { serializeUrlSearchParams } from '@seamapi/url-search-params-serializer'
5+
6+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
7+
interface Options {}
8+
9+
export const command: Command = 'axios'
10+
11+
export const describe: Describe = 'Serialize Axios params'
12+
13+
export const builder: Builder = {}
14+
15+
export const handler: Handler<Options> = async ({ logger }) => {
16+
const client = axios.create({
17+
paramsSerializer: serializeUrlSearchParams,
18+
baseURL: 'https://httpbin.org',
19+
})
20+
const { data } = await client.get('/get', {
21+
params: {
22+
a: 'bar',
23+
b: 2,
24+
c: true,
25+
d: null,
26+
e: ['a', 2],
27+
f: [],
28+
},
29+
})
30+
logger.info({ data }, 'Response')
31+
}

examples/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import landlubber from 'landlubber'
44

5-
import * as todo from './todo.js'
5+
import * as axios from './axios.js'
66

7-
const commands = [todo]
7+
const commands = [axios]
88

99
await landlubber(commands).parse()

examples/todo.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

package-lock.json

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@seamapi/url-search-params-serializer",
33
"version": "1.0.0",
4-
"description": "Serializes JavaScript objects to The URLSearchParams.",
4+
"description": "Serializes JavaScript objects to URLSearchParams.",
55
"type": "module",
66
"main": "index.js",
77
"types": "index.d.ts",
@@ -60,6 +60,7 @@
6060
"devDependencies": {
6161
"@types/node": "^20.8.10",
6262
"ava": "^6.0.1",
63+
"axios": "^1.6.5",
6364
"c8": "^9.0.0",
6465
"del-cli": "^5.0.0",
6566
"eslint": "^8.9.0",

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { default } from 'lib/index.js'
21
export * from 'lib/index.js'

src/lib/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { todo as default } from './todo.js'
2-
export { todo } from './todo.js'
1+
export * from './serialize.js'

src/lib/serialize.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from 'ava'
2+
3+
import { serializeUrlSearchParams, updateUrlSearchParams } from './serialize.js'
4+
5+
test('serializeUrlSearchParams', (t) => {
6+
t.is(serializeUrlSearchParams({ foo: 'd', bar: 2 }), 'bar=2&foo=d')
7+
})
8+
9+
test('updateUrlSearchParams', (t) => {
10+
const searchParams = new URLSearchParams()
11+
updateUrlSearchParams(searchParams, { foo: 'd', bar: 2 })
12+
t.is(searchParams.toString(), 'bar=2&foo=d')
13+
})

src/lib/serialize.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
type Params = Record<string, unknown>
2+
3+
export const serializeUrlSearchParams = (params: Params): string => {
4+
const searchParams = new URLSearchParams()
5+
updateUrlSearchParams(searchParams, params)
6+
return searchParams.toString()
7+
}
8+
9+
export const updateUrlSearchParams = (
10+
searchParams: URLSearchParams,
11+
params: Record<string, unknown>,
12+
): void => {
13+
for (const [name, value] of Object.entries(params)) {
14+
if (value == null) continue
15+
16+
if (Array.isArray(value)) {
17+
if (value.length === 0) searchParams.set(name, '')
18+
if (value.length === 1 && value[0] === '') {
19+
throw new UnserializableParamError(
20+
name,
21+
`is a single element array containing the empty string which is unsupported because it serializes to the empty array`,
22+
)
23+
}
24+
for (const v of value) {
25+
searchParams.append(name, serialize(name, v))
26+
}
27+
continue
28+
}
29+
30+
searchParams.set(name, serialize(name, value))
31+
}
32+
33+
searchParams.sort()
34+
}
35+
36+
const serialize = (k: string, v: unknown): string => {
37+
if (typeof v === 'string') return v.toString()
38+
if (typeof v === 'number') return v.toString()
39+
if (typeof v === 'bigint') return v.toString()
40+
if (typeof v === 'boolean') return v.toString()
41+
throw new UnserializableParamError(k, `is a ${typeof v}`)
42+
}
43+
44+
export class UnserializableParamError extends Error {
45+
constructor(name: string, message: string) {
46+
super(`Could not serialize parameter: '${name}' ${message}`)
47+
this.name = this.constructor.name
48+
Error.captureStackTrace(this, this.constructor)
49+
}
50+
}

0 commit comments

Comments
 (0)