Skip to content

Commit

Permalink
Add stringify method to mirror parse
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Nov 20, 2024
1 parent 8be4b82 commit 7aea427
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 5 deletions.
52 changes: 47 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,19 @@ export interface ParseOptions {
decode?: (str: string) => string | undefined;
}

/**
* Cookies object.
*/
export type Cookies = Record<string, string | undefined>;

/**
* 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<string, string | undefined> {
const obj: Record<string, string | undefined> = 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;
Expand Down Expand Up @@ -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.
*/
Expand Down
26 changes: 26 additions & 0 deletions src/stringify.spec.ts
Original file line number Diff line number Diff line change
@@ -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/,
);
});
});

0 comments on commit 7aea427

Please sign in to comment.