Skip to content

Commit 7aea427

Browse files
committed
Add stringify method to mirror parse
1 parent 8be4b82 commit 7aea427

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

src/index.ts

+47-5
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,19 @@ export interface ParseOptions {
8989
decode?: (str: string) => string | undefined;
9090
}
9191

92+
/**
93+
* Cookies object.
94+
*/
95+
export type Cookies = Record<string, string | undefined>;
96+
9297
/**
9398
* Parse a cookie header.
9499
*
95100
* Parse the given cookie header string into an object
96101
* The object has the various cookies as keys(names) => values
97102
*/
98-
export function parse(
99-
str: string,
100-
options?: ParseOptions,
101-
): Record<string, string | undefined> {
102-
const obj: Record<string, string | undefined> = new NullObject();
103+
export function parse(str: string, options?: ParseOptions): Cookies {
104+
const obj: Cookies = new NullObject();
103105
const len = str.length;
104106
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
105107
if (len < 2) return obj;
@@ -155,6 +157,46 @@ function endIndex(str: string, index: number, min: number) {
155157
return min;
156158
}
157159

160+
export interface StringifyOptions {
161+
/**
162+
* Specifies a function that will be used to encode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1).
163+
* Since value of a cookie has a limited character set (and must be a simple string), this function can be used to encode
164+
* a value into a string suited for a cookie's value, and should mirror `decode` when parsing.
165+
*
166+
* @default encodeURIComponent
167+
*/
168+
encode?: (str: string) => string;
169+
}
170+
171+
/**
172+
* Stringify a set of cookies into a `Cookie` header string.
173+
*/
174+
export function stringify(
175+
cookies: Cookies,
176+
options?: StringifyOptions,
177+
): string {
178+
const enc = options?.encode || encodeURIComponent;
179+
const cookieStrings: string[] = [];
180+
181+
for (const [name, val] of Object.entries(cookies)) {
182+
if (val === undefined) continue;
183+
184+
if (!cookieNameRegExp.test(name)) {
185+
throw new TypeError(`cookie name is invalid: ${name}`);
186+
}
187+
188+
const value = enc(val);
189+
190+
if (!cookieValueRegExp.test(value)) {
191+
throw new TypeError(`cookie val is invalid: ${val}`);
192+
}
193+
194+
cookieStrings.push(`${name}=${value}`);
195+
}
196+
197+
return cookieStrings.join("; ");
198+
}
199+
158200
/**
159201
* Serialize options.
160202
*/

src/stringify.spec.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from "vitest";
2+
import { stringify } from "./index.js";
3+
4+
describe("stringify", () => {
5+
it("should stringify object", () => {
6+
expect(stringify({ key: "value" })).toEqual("key=value");
7+
});
8+
9+
it("should stringify objects with multiple entries", () => {
10+
expect(stringify({ a: "1", b: "2" })).toEqual("a=1; b=2");
11+
});
12+
13+
it("should ignore undefined values", () => {
14+
expect(stringify({ a: "1", b: undefined })).toEqual("a=1");
15+
});
16+
17+
it("should error on invalid keys", () => {
18+
expect(() => stringify({ "test=": "" })).toThrow(/cookie name is invalid/);
19+
});
20+
21+
it("should error on invalid values", () => {
22+
expect(() => stringify({ test: ";" }, { encode: (x) => x })).toThrow(
23+
/cookie val is invalid/,
24+
);
25+
});
26+
});

0 commit comments

Comments
 (0)