Skip to content

Commit

Permalink
Add Accept header support
Browse files Browse the repository at this point in the history
  • Loading branch information
mjackson committed Dec 20, 2024
1 parent 6d0d236 commit 1dc26d7
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/headers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ let headers = new SuperHeaders({ lastModified: ms });
```

- Adds support for
- `headers.accept`
- `headers.connection`
- `headers.host`
- `headers.referer`
Expand Down
35 changes: 32 additions & 3 deletions packages/headers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ import Headers from '@mjackson/headers';

let headers = new Headers();

// Accept-Language
// Accept
headers.accept = 'text/html,text/*;q=0.9';

console.log(headers.accept.mediaTypes); // [ 'text/html', 'text/*' ]
console.log(Object.fromEntries(headers.accept.entries())); // { 'text/html': 1, 'text/*': 0.9 }

headers.accept.set('text/plain', 0.9);
headers.accept.set('text/*', 0.8);

console.log(headers.get('Accept')); // "text/html,text/plain;q=0.9,text/*;q=0.8"

// Accept-Language
headers.acceptLanguage = 'en-US,en;q=0.9';

console.log(headers.acceptLanguage.languages); // [ 'en-US', 'en' ]
console.log(headers.acceptLanguage.entries());
// [Map Entries] { [ 'en-US', 1 ], [ 'en', 0.9 ] }
console.log(Object.fromEntries(headers.acceptLanguage.entries())); // { 'en-US': 1, en: 0.9 }

// Content-Type
headers.contentType = 'application/json; charset=utf-8';
Expand Down Expand Up @@ -168,6 +177,7 @@ The following headers are currently supported:
- [Installation](#installation)
- [Overview](#overview)
- [Low-level API](#low-level-api)
- [Accept](#accept)
- [Accept-Language](#accept-language)
- [Cache-Control](#cache-control)
- [Content-Disposition](#content-disposition)
Expand All @@ -178,6 +188,25 @@ The following headers are currently supported:

If you need support for a header that isn't listed here, please [send a PR](https://github.com/mjackson/remix-the-web/pulls)! The goal is to have first-class support for all common HTTP headers.

### Accept

```ts
let header = new Accept('text/html;text/*;q=0.9');
header.get('text/html'); // 1
header.set('text/html', 0.8);
header.delete('text/html');
header.has('text/*'); // true

// Iterate over media type/quality pairs
for (let [mediaType, quality] of header) {
// ...
}

// Alternative init styles
let header = new Accept({ 'text/html': 1, 'text/*': 0.9 });
let header = new Accept(['text/html', ['text/*', 0.9]]);
```

### Accept-Language

```ts
Expand Down
42 changes: 20 additions & 22 deletions packages/headers/src/lib/accept-language.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Accept-Language', () => {
assert.equal(header.get('en'), 0.9);
});

it('initializes with another Accept-Language', () => {
it('initializes with another AcceptLanguage', () => {
let header = new AcceptLanguage(new AcceptLanguage('en-US,en;q=0.9'));
assert.equal(header.get('en-US'), 1);
assert.equal(header.get('en'), 0.9);
Expand All @@ -40,7 +40,7 @@ describe('Accept-Language', () => {
});

it('sets and gets languages', () => {
let header = new AcceptLanguage('');
let header = new AcceptLanguage();
header.set('en', 0.9);
assert.equal(header.get('en'), 0.9);
});
Expand All @@ -64,6 +64,16 @@ describe('Accept-Language', () => {
assert.equal(header.size, 0);
});

it('gets all languages', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
assert.deepEqual(header.languages, ['en-US', 'en']);
});

it('gets all qualities', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
assert.deepEqual(header.qualities, [1, 0.9]);
});

it('iterates over entries', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
let entries = Array.from(header.entries());
Expand All @@ -73,14 +83,13 @@ describe('Accept-Language', () => {
]);
});

it('gets all languages', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
assert.deepEqual(header.languages, ['en-US', 'en']);
});

it('gets all qualities', () => {
it('is directly iterable', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
assert.deepEqual(header.qualities, [1, 0.9]);
let entries = Array.from(header);
assert.deepEqual(entries, [
['en-US', 1],
['en', 0.9],
]);
});

it('uses forEach correctly', () => {
Expand All @@ -105,20 +114,10 @@ describe('Accept-Language', () => {
assert.equal(header.toString(), 'en-US,en;q=0.9');
});

it('is directly iterable', () => {
let header = new AcceptLanguage('en-US,en;q=0.9');
let entries = Array.from(header);
assert.deepEqual(entries, [
['en-US', 1],
['en', 0.9],
]);
});

it('handles setting empty quality values', () => {
let header = new AcceptLanguage('');
let header = new AcceptLanguage();
header.set('en-US');
assert.equal(header.get('en-US'), 1);
assert.equal(header.toString(), 'en-US');
});

it('overwrites existing quality values', () => {
Expand All @@ -128,10 +127,9 @@ describe('Accept-Language', () => {
});

it('handles setting wildcard value', () => {
let header = new AcceptLanguage('');
let header = new AcceptLanguage();
header.set('*');
assert.equal(header.get('*'), 1);
assert.equal(header.toString(), '*');
});

it('sorts initial value', () => {
Expand Down
52 changes: 34 additions & 18 deletions packages/headers/src/lib/accept-language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ export class AcceptLanguage implements HeaderValue, Iterable<[string, number]> {

if (init) {
if (typeof init === 'string') {
let params = parseParams(init, ',');
for (let [language, quality] of params) {
if (typeof quality === 'string') {
language = language.slice(0, -2).trim();
} else {
quality = '1';
for (let piece of init.split(/\s*,\s*/)) {
let params = parseParams(piece);
if (params.length < 1) continue;

let language = params[0][0];
let quality = 1;

for (let i = 1; i < params.length; i++) {
let [key, value] = params[i];
if (key === 'q') {
quality = Number(value);
break;
}
}
this.#map.set(language, Number(quality));

this.#map.set(language, quality);
}
} else if (isIterable(init)) {
for (let language of init) {
Expand All @@ -52,6 +60,20 @@ export class AcceptLanguage implements HeaderValue, Iterable<[string, number]> {
this.#map = new Map([...this.#map].sort(([, a], [, b]) => b - a));
}

/**
* An array of all locale identifiers in the `Accept-Language` header.
*/
get languages(): string[] {
return Array.from(this.#map.keys());
}

/**
* An array of all quality values in the `Accept-Language` header.
*/
get qualities(): number[] {
return Array.from(this.#map.values());
}

/**
* Gets the quality of a language with the given locale identifier from the `Accept-Language` header.
*/
Expand All @@ -60,7 +82,7 @@ export class AcceptLanguage implements HeaderValue, Iterable<[string, number]> {
}

/**
* Sets a language with the given quality in the `Accept-Language` header.
* Sets a language with the given quality (defaults to 1) in the `Accept-Language` header.
*/
set(language: string, quality = 1): void {
this.#map.set(language, quality);
Expand Down Expand Up @@ -92,23 +114,17 @@ export class AcceptLanguage implements HeaderValue, Iterable<[string, number]> {
return this.#map.entries();
}

get languages(): string[] {
return Array.from(this.#map.keys());
}

get qualities(): number[] {
return Array.from(this.#map.values());
}

[Symbol.iterator](): IterableIterator<[string, number]> {
return this.entries();
}

forEach(
callback: (language: string, quality: number, map: Map<string, number>) => void,
callback: (language: string, quality: number, header: AcceptLanguage) => void,
thisArg?: any,
): void {
this.#map.forEach((quality, language, map) => callback(language, quality, map), thisArg);
for (let [language, quality] of this) {
callback.call(thisArg, language, quality, this);
}
}

/**
Expand Down
147 changes: 147 additions & 0 deletions packages/headers/src/lib/accept.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { Accept } from './accept.ts';

describe('Accept', () => {
it('initializes with an empty string', () => {
let header = new Accept('');
assert.equal(header.size, 0);
});

it('initializes with a string', () => {
let header = new Accept('text/html,application/json;q=0.9');
assert.equal(header.get('text/html'), 1);
assert.equal(header.get('application/json'), 0.9);
});

it('initializes with an array', () => {
let header = new Accept(['text/html', ['application/json', 0.9]]);
assert.equal(header.get('text/html'), 1);
assert.equal(header.get('application/json'), 0.9);
});

it('initializes with an object', () => {
let header = new Accept({ 'text/html': 1, 'application/json': 0.9 });
assert.equal(header.get('text/html'), 1);
assert.equal(header.get('application/json'), 0.9);
});

it('initializes with another Accept', () => {
let header = new Accept(new Accept('text/html,application/json;q=0.9'));
assert.equal(header.get('text/html'), 1);
assert.equal(header.get('application/json'), 0.9);
});

it('handles whitespace in initial value', () => {
let header = new Accept(' text/html , application/json;q= 0.9 ');
assert.equal(header.get('text/html'), 1);
assert.equal(header.get('application/json'), 0.9);
});

it('sets and gets media types', () => {
let header = new Accept();
header.set('application/json', 0.9);
assert.equal(header.get('application/json'), 0.9);
});

it('deletes media types', () => {
let header = new Accept('text/html');
assert.equal(header.delete('text/html'), true);
assert.equal(header.delete('application/json'), false);
assert.equal(header.get('text/html'), undefined);
});

it('checks if media type exists', () => {
let header = new Accept('text/html');
assert.equal(header.has('text/html'), true);
assert.equal(header.has('application/json'), false);
});

it('clears all media types', () => {
let header = new Accept('text/html,application/json;q=0.9');
header.clear();
assert.equal(header.size, 0);
});

it('gets all media types', () => {
let header = new Accept('text/html,application/json;q=0.9');
assert.deepEqual(header.mediaTypes, ['text/html', 'application/json']);
});

it('gets all qualities', () => {
let header = new Accept('text/html,application/json;q=0.9');
assert.deepEqual(header.qualities, [1, 0.9]);
});

it('iterates over entries', () => {
let header = new Accept('text/html,application/json;q=0.9');
let entries = Array.from(header.entries());
assert.deepEqual(entries, [
['text/html', 1],
['application/json', 0.9],
]);
});

it('is directly iterable', () => {
let header = new Accept('text/html,application/json;q=0.9');
let mediaTypes = Array.from(header);
assert.deepEqual(mediaTypes, [
['text/html', 1],
['application/json', 0.9],
]);
});

it('uses forEach correctly', () => {
let header = new Accept('text/html,application/json;q=0.9');
let result: [string, number][] = [];
header.forEach((mediaType, quality) => {
result.push([mediaType, quality]);
});
assert.deepEqual(result, [
['text/html', 1],
['application/json', 0.9],
]);
});

it('returns correct size', () => {
let header = new Accept('text/html,application/json;q=0.9');
assert.equal(header.size, 2);
});

it('converts to string correctly', () => {
let header = new Accept('text/html,application/json;q=0.9');
assert.equal(header.toString(), 'text/html,application/json;q=0.9');
});

it('handles setting empty quality values', () => {
let header = new Accept();
header.set('text/html');
assert.equal(header.get('text/html'), 1);
});

it('overwrites existing quality values', () => {
let header = new Accept('text/html,application/json;q=0.9');
header.set('application/json', 0.8);
assert.equal(header.get('application/json'), 0.8);
});

it('handles setting wildcard media types', () => {
let header = new Accept();
header.set('*/*');
assert.equal(header.get('*/*'), 1);
});

it('sorts initial value', () => {
let header = new Accept('application/json;q=0.9,text/html');
assert.equal(header.toString(), 'text/html,application/json;q=0.9');
assert.deepEqual(header.mediaTypes, ['text/html', 'application/json']);
});

it('sorts updated value', () => {
let header = new Accept('text/html,application/json;q=0.9');
header.set('application/json', 0.8);
assert.equal(header.toString(), 'text/html,application/json;q=0.8');
assert.deepEqual(header.mediaTypes, ['text/html', 'application/json']);
});
});
Loading

0 comments on commit 1dc26d7

Please sign in to comment.