From 7aea4277ea67d1ba1c8032fc4aba10f4bed19db2 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 20 Nov 2024 09:35:13 -0800 Subject: [PATCH] Add stringify method to mirror parse --- src/index.ts | 52 ++++++++++++++++++++++++++++++++++++++----- src/stringify.spec.ts | 26 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/stringify.spec.ts diff --git a/src/index.ts b/src/index.ts index de9eeac..9d6996c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,17 +89,19 @@ export interface ParseOptions { decode?: (str: string) => string | undefined; } +/** + * Cookies object. + */ +export type Cookies = Record; + /** * Parse a cookie header. * * Parse the given cookie header string into an object * The object has the various cookies as keys(names) => values */ -export function parse( - str: string, - options?: ParseOptions, -): Record { - const obj: Record = new NullObject(); +export function parse(str: string, options?: ParseOptions): Cookies { + const obj: Cookies = new NullObject(); const len = str.length; // RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='. if (len < 2) return obj; @@ -155,6 +157,46 @@ function endIndex(str: string, index: number, min: number) { return min; } +export interface StringifyOptions { + /** + * Specifies a function that will be used to encode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1). + * Since value of a cookie has a limited character set (and must be a simple string), this function can be used to encode + * a value into a string suited for a cookie's value, and should mirror `decode` when parsing. + * + * @default encodeURIComponent + */ + encode?: (str: string) => string; +} + +/** + * Stringify a set of cookies into a `Cookie` header string. + */ +export function stringify( + cookies: Cookies, + options?: StringifyOptions, +): string { + const enc = options?.encode || encodeURIComponent; + const cookieStrings: string[] = []; + + for (const [name, val] of Object.entries(cookies)) { + if (val === undefined) continue; + + if (!cookieNameRegExp.test(name)) { + throw new TypeError(`cookie name is invalid: ${name}`); + } + + const value = enc(val); + + if (!cookieValueRegExp.test(value)) { + throw new TypeError(`cookie val is invalid: ${val}`); + } + + cookieStrings.push(`${name}=${value}`); + } + + return cookieStrings.join("; "); +} + /** * Serialize options. */ diff --git a/src/stringify.spec.ts b/src/stringify.spec.ts new file mode 100644 index 0000000..bed036b --- /dev/null +++ b/src/stringify.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { stringify } from "./index.js"; + +describe("stringify", () => { + it("should stringify object", () => { + expect(stringify({ key: "value" })).toEqual("key=value"); + }); + + it("should stringify objects with multiple entries", () => { + expect(stringify({ a: "1", b: "2" })).toEqual("a=1; b=2"); + }); + + it("should ignore undefined values", () => { + expect(stringify({ a: "1", b: undefined })).toEqual("a=1"); + }); + + it("should error on invalid keys", () => { + expect(() => stringify({ "test=": "" })).toThrow(/cookie name is invalid/); + }); + + it("should error on invalid values", () => { + expect(() => stringify({ test: ";" }, { encode: (x) => x })).toThrow( + /cookie val is invalid/, + ); + }); +});