From 7aea4277ea67d1ba1c8032fc4aba10f4bed19db2 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 20 Nov 2024 09:35:13 -0800 Subject: [PATCH 1/2] 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/, + ); + }); +}); From cd7ac130bbb69fc932474e3a74991de5af9f0869 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 20 Nov 2024 11:02:47 -0800 Subject: [PATCH 2/2] Add docs to README --- README.md | 21 +++++++++++++++++++++ src/index.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54e1cda..18bd082 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,27 @@ The default function is the global `decodeURIComponent`, wrapped in a `try..catc is thrown it will return the cookie's original value. If you provide your own encode/decode scheme you must ensure errors are appropriately handled. +### cookie.stringify(obj, options) + +Stringifies an object into a HTTP `Cookie` header. + +```js +const cookieHeader = cookie.stringify({ a: "foo", b: "bar" }); +// a=foo; b=bar +``` + +#### Options + +`cookie.stringify` accepts these properties in the options object. + +##### encode + +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. + +The default function is the global `encodeURIComponent`. + ### cookie.serialize(name, value, options) Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the diff --git a/src/index.ts b/src/index.ts index 9d6996c..8f9ee88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -169,7 +169,7 @@ export interface StringifyOptions { } /** - * Stringify a set of cookies into a `Cookie` header string. + * Stringifies an object into a HTTP `Cookie` header. */ export function stringify( cookies: Cookies,