diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 44369c8..49bcabb 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,13 +1,13 @@ name: ci on: - push: - branches: - - master - - next - pull_request: - workflow_dispatch: + push: + branches: + - master + - next + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/API.md b/API.md index 4726a51..4115ef3 100755 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ ## Introduction -Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is, what language should be used, and what charsets and media types are accepted. +Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is, what language should be used, and what charsets and media types are accepted. Additional details about Accept headers and content negotiation can be found in [IETF RFC 7231, Section 5.3](https://tools.ietf.org/html/rfc7231#section-5.3). @@ -8,7 +8,7 @@ Additional details about Accept headers and content negotiation can be found in ### `charset(charsetHeader, [preferences])` -Given a string of acceptable charsets from a HTTP request Accept-Charset header, and an optional array of charset preferences, it will return a string indicating the best charset option that can be used in the HTTP response. This takes into account any weighting parameters given in the header for ordering and exclusion. +Given a string of acceptable charsets from a HTTP request Accept-Charset header, and an optional array of charset preferences, it will return a string indicating the best charset option that can be used in the HTTP response. This takes into account any weighting parameters given in the header for ordering and exclusion. ``` const charset = Accept.charsets("iso-8859-5, unicode-1-1;q=0.8"); // charset === "iso-8859-5" @@ -26,7 +26,7 @@ const charsets = Accept.charsets("iso-8859-5;q=0.5, unicode-1-1;q=0.8"); // char ### `encoding(encodingHeader, [preferences])` -Given a string of acceptable encodings from a HTTP request Accept-Encoding header, and optionally an array of preferences, it will return a string with the best fit encoding that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered encoding is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). +Given a string of acceptable encodings from a HTTP request Accept-Encoding header, and optionally an array of preferences, it will return a string with the best fit encoding that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered encoding is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). ``` const encoding = Accept.encoding("gzip, deflate, sdch"); // encoding === "gzip" @@ -43,9 +43,9 @@ const encodings = Accept.encodings("compress;q=0.5, gzip;q=1.0"); // encodings = ### `language(languageHeader, [preferences])` -Given a string of acceptable language ranges from a HTTP request Accept-Language header, and an optional array of language-tag preferences, it will return a string indicating the best language that can be used in the HTTP response. It respects the [q weightings](#weightings) of the languages in the header, returning the matched preference with the highest weighting. The case of the preference does not have to match the case of the option in the header. +Given a string of acceptable language ranges from a HTTP request Accept-Language header, and an optional array of language-tag preferences, it will return a string indicating the best language that can be used in the HTTP response. It respects the [q weightings](#weightings) of the languages in the header, returning the matched preference with the highest weighting. The case of the preference does not have to match the case of the option in the header. -If preferences is missing or an empty array, the highest weighted language is returned. If no preference matches, an empty string is returned. +If preferences is missing or an empty array, the highest weighted language is returned. If no preference matches, an empty string is returned. ``` const language = Accept.language("en;q=0.7, en-GB;q=0.8"); // language === "en-gb" @@ -64,7 +64,7 @@ const languages = Accept.languages("da, en;q=0.7, en-GB;q=0.8"); // languages == ### `mediaType(mediaTypeHeader, [preferences])` -Given a string of acceptable media types from a HTTP request Accept header, and optionally an array of preferences, it will return a string with the best fit media type that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered media type is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). +Given a string of acceptable media types from a HTTP request Accept header, and optionally an array of preferences, it will return a string with the best fit media type that should be used in the HTTP response. If no preferences array parameter is given the highest weighted or first ordered media type is returned. If weightings are given in the header (using the q parameter) they are taken into account and the highest weighted match is returned. If a preferences array is given the best match from the array is returned. For more information about how the preferences array works see the section below on [Preferences](#preferences). ``` const mediaType = Accept.mediaType("text/plain, application/json;q=0.5, text/html, */*;q=0.1"); @@ -85,7 +85,7 @@ const mediaTypes = Accept.mediaTypes("text/plain, application/json;q=0.5, text/h ### `parseAll(headers)` -Given the headers from a Hapi request object, `parseAll()` will parse all of the Accepts-* headers it currently understands into an object. +Given the headers from a Hapi request object, `parseAll()` will parse all of the Accepts-\* headers it currently understands into an object. ``` const all = Accept.parseAll(request.headers); @@ -97,12 +97,11 @@ const all = Accept.parseAll(request.headers); // } ``` - ## Q Weightings -The Accept-* headers may optionally include preferential weighting to indicate which options are best for the requester. It does this with `q` parameters in the headers (which stands for quality). These q weightings must be in the range of 0 to 1, with a max of three decimal places. The weightings are used to order the data given in the header, with the highest number being most preferential. Anything with a q rating of 0 is not allowed at all. +The Accept-\* headers may optionally include preferential weighting to indicate which options are best for the requester. It does this with `q` parameters in the headers (which stands for quality). These q weightings must be in the range of 0 to 1, with a max of three decimal places. The weightings are used to order the data given in the header, with the highest number being most preferential. Anything with a q rating of 0 is not allowed at all. -If a particular Accept method allows a `preferences` array parameter, such as `encoding()`, the weightings in the header affect which preference will be returned. Your preferences are matched with the weighting in mind, and the highest weighted option will be returned, no matter what order you list your preferences. The header weighting is most important. +If a particular Accept method allows a `preferences` array parameter, such as `encoding()`, the weightings in the header affect which preference will be returned. Your preferences are matched with the weighting in mind, and the highest weighted option will be returned, no matter what order you list your preferences. The header weighting is most important. ``` const encoding = Accept.encoding("gzip;q=1.0, identity;q=0.5", ["identity", "gzip"]); @@ -110,18 +109,17 @@ const encoding = Accept.encoding("gzip;q=1.0, identity;q=0.5", ["identity", "gzi // despite identity getting listed first in the preferences array, gzip has a higher q weighting, so it is returned. ``` - ## Encodings ### Preferences -If you are looking for a set of specific encodings you can pass that in as an array to the `preferences` parameter. Your preferences **must** be an array. In the preferences array you specify a list of possible encodings you want to look for, in order of preference. Accept will return back the most preferential option it can find, if any match. The preferences array does not support parameters, only base types. +If you are looking for a set of specific encodings you can pass that in as an array to the `preferences` parameter. Your preferences **must** be an array. In the preferences array you specify a list of possible encodings you want to look for, in order of preference. Accept will return back the most preferential option it can find, if any match. The preferences array does not support parameters, only base types. ``` const encoding = Accept.encoding("gzip, deflate, sdch", ["deflate", "identity"]); // encoding === "delate" ``` -Your preferences are evaluated without any case sensitivity, to better match what the browser sends. This means that "gZip" will match a preference of ["gzip"]. +Your preferences are evaluated without any case sensitivity, to better match what the browser sends. This means that "gZip" will match a preference of ["gzip"]. ``` const encoding = Accept.encoding("gZip, deflate, sdch", ["gzip"]); // encoding === "gzip" @@ -133,13 +131,12 @@ If you supply a preferences array, and no match is found, `encoding()` will retu const encoding = Accept.encoding("gZip", ["deflate"]); // encoding === "" ``` -If the encoding header is the special "*" that indicates the browser will accept any encoding. In that case the top preference from your supplied options will be returned. +If the encoding header is the special "\*" that indicates the browser will accept any encoding. In that case the top preference from your supplied options will be returned. ``` const encoding = Accept.encoding("*", ["gzip"]); // encoding === "gzip" ``` - ### Identity When you ask Accept for a list of all the supported encodings from the request, using the `encodings()` function (plural, not singular), you will be returned an array of strings in order from most preferred to least as determined by the encoding weight. @@ -148,4 +145,4 @@ When you ask Accept for a list of all the supported encodings from the request, const encodings = Accept.encodings("compress;q=0.5, gzip;q=1.0"); // encodings === ["gzip", "compress", "identity"] ``` -You'll notice that `identity` was returned in the array, even though it's not in the encoding header. Identity is always an option for encoding, unless specifically excluded in the header using a weighting of zero. Identity just means respond with no special encoding. +You'll notice that `identity` was returned in the array, even though it's not in the encoding header. Identity is always an option for encoding, unless specifically excluded in the header using a weighting of zero. Identity just means respond with no special encoding. diff --git a/README.md b/README.md index 865de15..c4f6504 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # @hapi/accept -#### HTTP Accept-* headers parsing. +#### HTTP Accept-\* headers parsing. **accept** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together. diff --git a/lib/index.js b/lib/index.js deleted file mode 100755 index a406cbd..0000000 --- a/lib/index.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const Header = require('./header'); -const Media = require('./media'); - - -const internals = { - options: { - charset: { - type: 'accept-charset' - }, - encoding: { - type: 'accept-encoding', - default: 'identity', - equivalents: new Map([ - ['x-compress', 'compress'], - ['x-gzip', 'gzip'] - ]) - }, - language: { - type: 'accept-language', - prefixMatch: true - } - } -}; - - -for (const type in internals.options) { - exports[type] = (header, preferences) => Header.selection(header, preferences, internals.options[type]); - - exports[`${type}s`] = (header, preferences) => Header.selections(header, preferences, internals.options[type]); -} - - -exports.mediaType = (header, preferences) => Media.selection(header, preferences); - -exports.mediaTypes = (header, preferences) => Media.selections(header, preferences); - - -exports.parseAll = function (requestHeaders) { - - return { - charsets: exports.charsets(requestHeaders['accept-charset']), - encodings: exports.encodings(requestHeaders['accept-encoding']), - languages: exports.languages(requestHeaders['accept-language']), - mediaTypes: exports.mediaTypes(requestHeaders.accept) - }; -}; diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index ce187ff..58ea704 100755 --- a/package.json +++ b/package.json @@ -1,38 +1,51 @@ { - "name": "@hapi/accept", - "description": "HTTP Accept-* headers parsing", - "version": "6.0.3", - "repository": "git://github.com/hapijs/accept", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "files": [ - "lib" - ], - "keywords": [ - "HTTP", - "header", - "accept", - "accept-encoding" - ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, - "dependencies": { - "@hapi/boom": "^10.0.1", - "@hapi/hoek": "^11.0.2" - }, - "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/eslint-plugin": "^7.0.0", - "@hapi/lab": "^26.0.0", - "@types/node": "^17.0.31", - "typescript": "~4.6.4" - }, - "scripts": { - "test": "lab -a @hapi/code -t 100 -L -Y", - "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" - }, - "license": "BSD-3-Clause" + "name": "@hapi/accept", + "version": "6.0.3", + "description": "HTTP Accept-* headers parsing", + "keywords": [ + "HTTP", + "accept", + "accept-encoding", + "header" + ], + "license": "BSD-3-Clause", + "repository": "git://github.com/hapijs/accept", + "files": [ + "src", + "API.md", + "README.md" + ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + } + }, + "scripts": { + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^12.0.0-rc.0" + }, + "devDependencies": { + "@hapi/oxc-plugin": "^1.0.4", + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.10", + "oxfmt": "^0.61.0", + "oxlint": "^1.76.0", + "typescript": "^6.0.3", + "vitest": "^4.1.10" + }, + "engines": { + "node": ">=22" + } } diff --git a/lib/header.js b/src/header.js similarity index 74% rename from lib/header.js rename to src/header.js index ced9e2b..09b995d 100755 --- a/lib/header.js +++ b/src/header.js @@ -1,27 +1,17 @@ -'use strict'; +import * as Boom from '@hapi/boom'; +import * as Hoek from '@hapi/hoek'; -const Hoek = require('@hapi/hoek'); -const Boom = require('@hapi/boom'); - - -const internals = {}; - - -exports.selection = function (header, preferences, options) { - - const selections = exports.selections(header, preferences, options); - return selections.length ? selections[0] : ''; +export const selection = function (header, preferences, options) { + const results = selections(header, preferences, options); + return results.length ? results[0] : ''; }; - -exports.selections = function (header, preferences, options) { - +export const selections = function (header, preferences, options) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); - return internals.parse(header || '', preferences, options); + return parse(header || '', preferences, options); }; - // RFC 7231 Section 5.3.3 (https://tools.ietf.org/html/rfc7231#section-5.3.3) // // Accept-Charset = *( "," OWS ) ( ( charset / "*" ) [ weight ] ) *( OWS "," [ OWS ( ( charset / "*" ) [ weight ] ) ] ) @@ -29,7 +19,6 @@ exports.selections = function (header, preferences, options) { // // Accept-Charset: iso-8859-5, unicode-1-1;q=0.8 - // RFC 7231 Section 5.3.4 (https://tools.ietf.org/html/rfc7231#section-5.3.4) // // Accept-Encoding = [ ( "," / ( codings [ weight ] ) ) *( OWS "," [ OWS ( codings [ weight ] ) ] ) ] @@ -42,7 +31,6 @@ exports.selections = function (header, preferences, options) { // Accept-Encoding: compress;q=0.5, gzip;q=1.0 // Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 - // RFC 7231 Section 5.3.5 (https://tools.ietf.org/html/rfc7231#section-5.3.5) // // Accept-Language = *( "," OWS ) ( language-range [ weight ] ) *( OWS "," [ OWS ( language-range [ weight ] ) ] ) @@ -51,7 +39,6 @@ exports.selections = function (header, preferences, options) { // // Accept-Language: da, en-gb;q=0.8, en;q=0.7 - // token = 1*tchar // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" @@ -59,7 +46,6 @@ exports.selections = function (header, preferences, options) { // ; any VCHAR, except delimiters // OWS = *( SP / HTAB ) - // RFC 7231 Section 5.3.1 (https://tools.ietf.org/html/rfc7231#section-5.3.1) // // The weight is normalized to a real number in the range 0 through 1, @@ -70,9 +56,7 @@ exports.selections = function (header, preferences, options) { // weight = OWS ";" OWS "q=" qvalue // qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) - -internals.parse = function (raw, preferences, options) { - +const parse = function (raw, preferences, options) { // Normalize header (remove spaces and tabs) const header = raw.replace(/[ \t]/g, ''); @@ -88,7 +72,7 @@ internals.parse = function (raw, preferences, options) { if (options.prefixMatch) { const parts = lower.split('-'); - while (parts.pop(), parts.length > 0) { + while ((parts.pop(), parts.length > 0)) { const joined = parts.join('-'); if (!lowers.has(joined)) { lowers.set(joined, { orig: preference, pos: pos++ }); @@ -101,12 +85,14 @@ internals.parse = function (raw, preferences, options) { // Parse selections const parts = header.split(','); - const selections = []; + const results = []; const map = new Set(); for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (!part) { // Ignore empty parts or leading commas + // Ignore empty parts or leading commas + + if (!part) { continue; } @@ -126,19 +112,17 @@ internals.parse = function (raw, preferences, options) { token = options.equivalents.get(token); } - const selection = { + const item = { token, pos: i, - q: 1 + q: 1, }; - if (preferences && - lowers.has(token)) { - - selection.pref = lowers.get(token).pos; + if (preferences && lowers.has(token)) { + item.pref = lowers.get(token).pos; } - map.add(selection.token); + map.add(item.token); // Parse q=value @@ -146,9 +130,7 @@ internals.parse = function (raw, preferences, options) { const q = params[1]; const [key, value] = q.split('='); - if (!value || - key !== 'q' && key !== 'Q') { - + if (!value || (key !== 'q' && key !== 'Q')) { throw Boom.badRequest(`Invalid ${options.type} header`); } @@ -157,28 +139,23 @@ internals.parse = function (raw, preferences, options) { continue; } - if (Number.isFinite(score) && - score <= 1 && - score >= 0.001) { - - selection.q = score; + if (Number.isFinite(score) && score <= 1 && score >= 0.001) { + item.q = score; } } - selections.push(selection); // Only add allowed selections (q !== 0) + results.push(item); // Only add allowed selections (q !== 0) } // Sort selection based on q and then position in header - selections.sort(internals.sort); + results.sort(sort); // Extract tokens - const values = selections.map((selection) => selection.token); - - if (options.default && - !map.has(options.default)) { + const values = results.map((item) => item.token); + if (options.default && !map.has(options.default)) { values.push(options.default); } @@ -187,16 +164,15 @@ internals.parse = function (raw, preferences, options) { } const preferred = []; - for (const selection of values) { - if (selection === '*') { - for (const [preference, value] of lowers) { + for (const value of values) { + if (value === '*') { + for (const [preference, prefValue] of lowers) { if (!map.has(preference)) { - preferred.push(value.orig); + preferred.push(prefValue.orig); } } - } - else { - const lower = selection.toLowerCase(); + } else { + const lower = value.toLowerCase(); if (lowers.has(lower)) { preferred.push(lowers.get(lower).orig); } @@ -206,9 +182,7 @@ internals.parse = function (raw, preferences, options) { return preferred; }; - -internals.sort = function (a, b) { - +const sort = function (a, b) { const aFirst = -1; const bFirst = 1; diff --git a/lib/index.d.ts b/src/index.d.ts similarity index 58% rename from lib/index.d.ts rename to src/index.d.ts index a9be6da..113189f 100755 --- a/lib/index.d.ts +++ b/src/index.d.ts @@ -1,101 +1,83 @@ /** * Identifies the best character-set for an HTTP response based on the HTTP request Accept-Charset header. - * - * @param header - the HTTP Accept-Charset header content. - * @param preferences - an optional array of character-set strings in order of server preference. - * - * @return a string with the preferred accepted character-set. + * + * @param header - The HTTP Accept-Charset header content. + * @param preferences - An optional array of character-set strings in order of server preference. + * @returns A string with the preferred accepted character-set. */ export function charset(header?: string, preferences?: readonly string[]): string; - /** - * Sorts the character-sets in the HTTP request Accept-Charset header based on client preference from most to least desired. - * - * @param header - the HTTP Accept-Charset header content. + * Sorts the character-sets in the HTTP request Accept-Charset header based on client preference from most to least + * desired. * - * @return an array of strings of character-sets sorted from the most to the least desired. + * @param header - The HTTP Accept-Charset header content. + * @returns An array of strings of character-sets sorted from the most to the least desired. */ export function charsets(header?: string): string[]; - /** * Identifies the best encoding for an HTTP response based on the HTTP request Accept-Encoding header. * - * @param header - the HTTP Accept-Encoding header content. - * @param preferences - an optional array of encoding strings in order of server preference. - * - * @return a string with the preferred accepted encoding. + * @param header - The HTTP Accept-Encoding header content. + * @param preferences - An optional array of encoding strings in order of server preference. + * @returns A string with the preferred accepted encoding. */ export function encoding(header?: string, preferences?: readonly string[]): string; - /** * Sorts the encodings in the HTTP request Accept-Encoding header based on client preference from most to least desired. * - * @param header - the HTTP Accept-Encoding header content. - * - * @return an array of strings of encodings sorted from the most to the least desired. + * @param header - The HTTP Accept-Encoding header content. + * @returns An array of strings of encodings sorted from the most to the least desired. */ export function encodings(header?: string): string[]; - /** * Identifies the best language for an HTTP response based on the HTTP request Accept-Language header. * - * @param header - the HTTP Accept-Language header content. - * @param preferences - an optional array of language strings in order of server preference. - * - * @return a string with the preferred accepted language. + * @param header - The HTTP Accept-Language header content. + * @param preferences - An optional array of language strings in order of server preference. + * @returns A string with the preferred accepted language. */ export function language(header?: string, preferences?: readonly string[]): string; - /** * Sorts the languages in the HTTP request Accept-Language header based on client preference from most to least desired. * - * @param header - the HTTP Accept-Language header content. - * - * @return an array of strings of languages sorted from the most to the least desired. + * @param header - The HTTP Accept-Language header content. + * @returns An array of strings of languages sorted from the most to the least desired. */ export function languages(header?: string): string[]; - /** * Identifies the best media-type for an HTTP response based on the HTTP request Accept header. * - * @param header - the HTTP Accept header content. - * @param preferences - an optional array of media-type strings in order of server preference. - * - * @return a string with the preferred accepted media-type. + * @param header - The HTTP Accept header content. + * @param preferences - An optional array of media-type strings in order of server preference. + * @returns A string with the preferred accepted media-type. */ export function mediaType(header?: string, preferences?: readonly string[]): string; - /** * Sorts the media-types in the HTTP request Accept header based on client preference from most to least desired. * - * @param header - the HTTP Accept header content. - * @param preferences - an optional array of media-type strings in order of server preference. - * - * @return an array of strings of media-types sorted from the most to the least desired. + * @param header - The HTTP Accept header content. + * @param preferences - An optional array of media-type strings in order of server preference. + * @returns An array of strings of media-types sorted from the most to the least desired. */ export function mediaTypes(header?: string, preferences?: readonly string[]): string[]; - /** * Parses the Accept-* headers of an HTTP request and returns an array of client preferences for each header. - * - * @param headers - the HTTP request headers object. - * - * @return an object with a key for each accept header result. + * + * @param headers - The HTTP request headers object. + * @returns An object with a key for each accept header result. */ export function parseAll(headers: parseAll.Headers): parseAll.Result; export namespace parseAll { - interface Headers { - readonly 'accept-charset'?: string; readonly 'accept-encoding'?: string; readonly 'accept-language'?: string; @@ -105,7 +87,6 @@ export namespace parseAll { } interface Result { - charsets: string[]; encodings: string[]; languages: string[]; diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..29d1918 --- /dev/null +++ b/src/index.js @@ -0,0 +1,45 @@ +import * as Header from './header.js'; +import * as Media from './media.js'; + +const options = { + charset: { + type: 'accept-charset', + }, + encoding: { + type: 'accept-encoding', + default: 'identity', + equivalents: new Map([ + ['x-compress', 'compress'], + ['x-gzip', 'gzip'], + ]), + }, + language: { + type: 'accept-language', + prefixMatch: true, + }, +}; + +export const charset = (header, preferences) => Header.selection(header, preferences, options.charset); + +export const charsets = (header, preferences) => Header.selections(header, preferences, options.charset); + +export const encoding = (header, preferences) => Header.selection(header, preferences, options.encoding); + +export const encodings = (header, preferences) => Header.selections(header, preferences, options.encoding); + +export const language = (header, preferences) => Header.selection(header, preferences, options.language); + +export const languages = (header, preferences) => Header.selections(header, preferences, options.language); + +export const mediaType = (header, preferences) => Media.selection(header, preferences); + +export const mediaTypes = (header, preferences) => Media.selections(header, preferences); + +export const parseAll = function (requestHeaders) { + return { + charsets: charsets(requestHeaders['accept-charset']), + encodings: encodings(requestHeaders['accept-encoding']), + languages: languages(requestHeaders['accept-language']), + mediaTypes: mediaTypes(requestHeaders.accept), + }; +}; diff --git a/lib/media.js b/src/media.js similarity index 68% rename from lib/media.js rename to src/media.js index eeb046f..bebca77 100755 --- a/lib/media.js +++ b/src/media.js @@ -1,27 +1,17 @@ -'use strict'; +import * as Boom from '@hapi/boom'; +import * as Hoek from '@hapi/hoek'; -const Hoek = require('@hapi/hoek'); -const Boom = require('@hapi/boom'); - - -const internals = {}; - - -exports.selection = function (header, preferences) { - - const selections = exports.selections(header, preferences); - return selections.length ? selections[0] : ''; +export const selection = function (header, preferences) { + const results = selections(header, preferences); + return results.length ? results[0] : ''; }; - -exports.selections = function (header, preferences) { - +export const selections = function (header, preferences) { Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); - return internals.parse(header, preferences); + return parse(header, preferences); }; - // RFC 7231 Section 5.3.2 (https://tools.ietf.org/html/rfc7231#section-5.3.2) // // Accept = [ ( "," / ( media-range [ accept-params ] ) ) *( OWS "," [ OWS ( media-range [ accept-params ] ) ] ) ] @@ -48,7 +38,6 @@ exports.selections = function (header, preferences) { // Accept: text/*, text/plain, text/plain;format=flowed, */* // Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 - // RFC 7231 Section 5.3.1 (https://tools.ietf.org/html/rfc7231#section-5.3.1) // // The weight is normalized to a real number in the range 0 through 1, @@ -59,26 +48,26 @@ exports.selections = function (header, preferences) { // weight = OWS ";" OWS "q=" qvalue // qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) - // */* type/* type/subtype -internals.validMediaRx = /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; - - -internals.parse = function (raw, preferences) { +const validMediaRx = + /^(?:\*\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/\*)|(?:[\w\!#\$%&'\*\+\-\.\^`\|~]+\/[\w\!#\$%&'\*\+\-\.\^`\|~]+)$/; +const parse = function (raw, preferences) { // Normalize header (remove spaces and temporary remove quoted strings) - const { header, quoted } = internals.normalize(raw); + const { header, quoted } = normalize(raw); // Parse selections const parts = header.split(','); - const selections = []; + const results = []; const map = {}; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (!part) { // Ignore empty parts or leading commas + // Ignore empty parts or leading commas + + if (!part) { continue; } @@ -87,15 +76,17 @@ internals.parse = function (raw, preferences) { const pairs = part.split(';'); const token = pairs.shift().toLowerCase(); - if (!internals.validMediaRx.test(token)) { // Ignore invalid types + // Ignore invalid types + + if (!validMediaRx.test(token)) { continue; } - const selection = { + const item = { token, params: {}, exts: {}, - pos: i + pos: i, }; // Parse key=value @@ -103,79 +94,72 @@ internals.parse = function (raw, preferences) { let target = 'params'; for (const pair of pairs) { const kv = pair.split('='); - if (kv.length !== 2 || - !kv[1]) { - + if (kv.length !== 2 || !kv[1]) { throw Boom.badRequest(`Invalid accept header`); } const key = kv[0]; let value = kv[1]; - if (key === 'q' || - key === 'Q') { - + if (key === 'q' || key === 'Q') { target = 'exts'; value = parseFloat(value); - if (!Number.isFinite(value) || - value > 1 || - (value < 0.001 && value !== 0)) { - + if (!Number.isFinite(value) || value > 1 || (value < 0.001 && value !== 0)) { value = 1; } - selection.q = value; - } - else { + item.q = value; + } else { if (value[0] === '"') { value = `"${quoted[value]}"`; } - selection[target][kv[0]] = value; + item[target][kv[0]] = value; } } - const params = Object.keys(selection.params); - selection.original = [''].concat(params.map((key) => `${key}=${selection.params[key]}`)).join(';'); - selection.specificity = params.length; + const params = Object.keys(item.params); + item.original = [''].concat(params.map((key) => `${key}=${item.params[key]}`)).join(';'); + item.specificity = params.length; + + // Default no preference to q=1 (top preference) - if (selection.q === undefined) { // Default no preference to q=1 (top preference) - selection.q = 1; + if (item.q === undefined) { + item.q = 1; } - const tparts = selection.token.split('/'); - selection.type = tparts[0]; - selection.subtype = tparts[1]; + const tparts = item.token.split('/'); + item.type = tparts[0]; + item.subtype = tparts[1]; - map[selection.token] = selection; + map[item.token] = item; - if (selection.q) { // Skip denied selections (q=0) - selections.push(selection); + // Skip denied selections (q=0) + + if (item.q) { + results.push(item); } } // Sort selection based on q and then position in header - selections.sort(internals.sort); + results.sort(sort); - return internals.preferences(map, selections, preferences); + return filterPreferences(map, results, preferences); }; - -internals.normalize = function (raw) { - +const normalize = function (raw) { raw = raw || '*/*'; const normalized = { header: raw, - quoted: {} + quoted: {}, }; if (raw.includes('"')) { let i = 0; normalized.header = raw.replace(/="([^"]*)"/g, ($0, $1) => { - const key = '"' + ++i; normalized.quoted[key] = $1; return '=' + key; @@ -186,9 +170,7 @@ internals.normalize = function (raw) { return normalized; }; - -internals.sort = function (a, b) { - +const sort = function (a, b) { // Sort by quality score if (b.q !== a.q) { @@ -198,13 +180,13 @@ internals.sort = function (a, b) { // Sort by type if (a.type !== b.type) { - return internals.innerSort(a, b, 'type'); + return innerSort(a, b, 'type'); } // Sort by subtype if (a.subtype !== b.subtype) { - return internals.innerSort(a, b, 'subtype'); + return innerSort(a, b, 'subtype'); } // Sort by specificity @@ -216,9 +198,7 @@ internals.sort = function (a, b) { return a.pos - b.pos; }; - -internals.innerSort = function (a, b, key) { - +const innerSort = function (a, b, key) { const aFirst = -1; const bFirst = 1; @@ -230,16 +210,14 @@ internals.innerSort = function (a, b, key) { return aFirst; } - return a[key] < b[key] ? aFirst : bFirst; // Group alphabetically + return a[key] < b[key] ? aFirst : bFirst; // Group alphabetically }; - -internals.preferences = function (map, selections, preferences) { - +const filterPreferences = function (map, results, preferences) { // Return selections if no preferences if (!preferences?.length) { - return selections.map((selection) => selection.token + selection.original); + return results.map((item) => item.token + item.original); } // Map wildcards and filter selections to preferences @@ -266,8 +244,8 @@ internals.preferences = function (map, selections, preferences) { } const preferred = []; - for (const selection of selections) { - const token = selection.token; + for (const item of results) { + const token = item.token; const { type, subtype } = map[token]; const subtypes = lowers[type]; @@ -290,7 +268,7 @@ internals.preferences = function (map, selections, preferences) { // any if (any) { - preferred.push((flat[token] || token) + selection.original); + preferred.push((flat[token] || token) + item.original); continue; } @@ -298,10 +276,8 @@ internals.preferences = function (map, selections, preferences) { if (subtype !== '*') { const pref = flat[token]; - if (pref || - (subtypes && subtypes['*'])) { - - preferred.push((pref || token) + selection.original); + if (pref || (subtypes && subtypes['*'])) { + preferred.push((pref || token) + item.original); } continue; diff --git a/test/charset.js b/test/charset.js index a0a9a04..2c47f0d 100755 --- a/test/charset.js +++ b/test/charset.js @@ -1,129 +1,98 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; describe('charset()', () => { - it('parses header', () => { - const charset = Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('respects weights', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001'); - expect(charset).to.equal('unicode-1-1'); + expect(charset).toBe('unicode-1-1'); }); it('requires that preferences parameter must be an array', () => { - expect(() => { - Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns empty string when there are no charsets', () => { - const charset = Accept.charset('*;q=0'); - expect(charset).to.equal(''); + expect(charset).toBe(''); }); it('returns first charset when preferences array is empty', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', []); - expect(charset).to.equal('unicode-1-1'); + expect(charset).toBe('unicode-1-1'); }); it('looks for top preference', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('find anything in preferences', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8', ['utf-8', 'iso-8859-5']); - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); it('returns empty string if no preference match is found', () => { - const charset = Accept.charset('iso-8859-5; q=0.1, unicode-1-1;q=0.8', ['utf-8']); - expect(charset).to.equal(''); + expect(charset).toBe(''); }); it('accepts any charset preference with *', () => { - const charset = Accept.charset('*;q=0.001', ['utf-8']); - expect(charset).to.equal('utf-8'); + expect(charset).toBe('utf-8'); }); it('ignores preference case', () => { - - expect(Accept.charset('UTF-8', ['utf-8'])).to.equal('utf-8'); - expect(Accept.charset('utf-8', ['UTF-8'])).to.equal('UTF-8'); + expect(Accept.charset('UTF-8', ['utf-8'])).toBe('utf-8'); + expect(Accept.charset('utf-8', ['UTF-8'])).toBe('UTF-8'); }); it('obeys disallow with wildcard', () => { - const charset = Accept.charset('*, not-this;q=0, UTF-8;q=0', ['utf-8', 'iso-8859-5']); // utf-8 is disallowed - expect(charset).to.equal('iso-8859-5'); + expect(charset).toBe('iso-8859-5'); }); }); describe('charsets()', () => { - it('parses header', () => { - const charsets = Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1', '*']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1', '*']); }); it('orders by weight(q)', () => { - const charsets = Accept.charsets('iso-8859-5;q=0.5, unicode-1-1;q=0.8'); - expect(charsets).to.equal(['unicode-1-1', 'iso-8859-5']); + expect(charsets).toEqual(['unicode-1-1', 'iso-8859-5']); }); it('ignores case', () => { - const charsets = Accept.charsets('ISO-8859-5, uNIcode-1-1;q=0.8, *;q=0.001'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1', '*']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1', '*']); }); it('drops zero weighted charsets', () => { - const charsets = Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, drop-me;q=0'); - expect(charsets).to.equal(['iso-8859-5', 'unicode-1-1']); + expect(charsets).toEqual(['iso-8859-5', 'unicode-1-1']); }); it('ignores invalid weights', () => { - const charsets = Accept.charsets('too-low;q=0.0001, unicode-1-1;q=0.8, too-high;q=1.1, letter-weight;q=a'); - expect(charsets).to.equal(['too-low', 'too-high', 'letter-weight', 'unicode-1-1']); + expect(charsets).toEqual(['too-low', 'too-high', 'letter-weight', 'unicode-1-1']); }); it('return empty array when no header is present', () => { - const charsets = Accept.charsets(); - expect(charsets).to.equal([]); + expect(charsets).toEqual([]); }); it('return empty array when header is empty', () => { - const charsets = Accept.charsets(''); - expect(charsets).to.equal([]); + expect(charsets).toEqual([]); }); }); diff --git a/test/encoding.js b/test/encoding.js index e2f5d23..2d8f709 100755 --- a/test/encoding.js +++ b/test/encoding.js @@ -1,15 +1,6 @@ -'use strict'; +import { describe, expect, it } from 'vitest'; -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import * as Accept from '../src/index.js'; /* Accept-Encoding: compress, gzip @@ -20,146 +11,121 @@ const expect = Code.expect; */ describe('encoding()', () => { - it('parses header', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0'); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with weightings', () => { - const encoding = Accept.encoding('gzip;q=0.001, identity; q=0.05, *;q=0'); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('requires that preferences be an array', () => { - expect(() => { - Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', 'identity, deflate'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('parses header with preferences', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gzip']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with preferences (server priority)', () => { - - expect(Accept.encoding('gzip, identity, deflate', ['deflate', 'gzip', 'identity'])).to.equal('deflate'); - expect(Accept.encoding('gzip, identity, deflate', ['gzip', 'identity'])).to.equal('gzip'); - expect(Accept.encoding('gzip, identity, deflate;q=0.9', ['deflate', 'gzip', 'identity'])).to.equal('gzip'); + expect(Accept.encoding('gzip, identity, deflate', ['deflate', 'gzip', 'identity'])).toBe('deflate'); + expect(Accept.encoding('gzip, identity, deflate', ['gzip', 'identity'])).toBe('gzip'); + expect(Accept.encoding('gzip, identity, deflate;q=0.9', ['deflate', 'gzip', 'identity'])).toBe('gzip'); }); it('parses header with preferences (case insensitive)', () => { - const encoding = Accept.encoding('GZIP;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gZip']); - expect(encoding).to.equal('gZip'); + expect(encoding).toBe('gZip'); }); it('parses header with preferences (x-)', () => { - const encoding = Accept.encoding('x-gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate', 'gzip']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('parses header with preferences (secondary match)', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['identity', 'deflate']); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('parses header with preferences (no match)', () => { - const encoding = Accept.encoding('gzip;q=1.0, identity; q=0.5, *;q=0', ['deflate']); - expect(encoding).to.equal(''); + expect(encoding).toBe(''); }); it('returns top preference on *', () => { - const encoding = Accept.encoding('*', ['gzip', 'deflate']); - expect(encoding).to.equal('gzip'); + expect(encoding).toBe('gzip'); }); it('returns top preference on * (identity)', () => { - const encoding = Accept.encoding('*', ['identity', 'gzip', 'deflate']); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('returns identity on empty', () => { - const encoding = Accept.encoding(''); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('returns none on empty with non identity preferences', () => { - const encoding = Accept.encoding('', ['gzip', 'deflate']); - expect(encoding).to.equal(''); + expect(encoding).toBe(''); }); it('returns identity on undefined without preference', () => { - const encoding = Accept.encoding(); - expect(encoding).to.equal('identity'); + expect(encoding).toBe('identity'); }); it('excludes q=0', () => { - const encoding = Accept.encoding('compress;q=0.5, gzip;q=0.0', ['gzip', 'compress']); - expect(encoding).to.equal('compress'); + expect(encoding).toBe('compress'); }); it('ignores improper weightings', () => { - const encoding = Accept.encoding('gzip;q=0.01, identity; q=0.5, deflate;q=1.1, *;q=0'); - expect(encoding).to.equal('deflate'); + expect(encoding).toBe('deflate'); }); it('errors on invalid header', () => { - - expect(() => Accept.encoding('a;b')).to.throw(); - expect(() => Accept.encoding('a;b;q=1')).to.throw(); - expect(() => Accept.encoding('a;q')).to.throw(); - expect(() => Accept.encoding('a;q=')).to.throw(); - expect(() => Accept.encoding(';q=1')).to.throw(); - expect(() => Accept.encoding('gzip;a=1')).to.throw(); + expect(() => Accept.encoding('a;b')).toThrow(); + expect(() => Accept.encoding('a;b;q=1')).toThrow(); + expect(() => Accept.encoding('a;q')).toThrow(); + expect(() => Accept.encoding('a;q=')).toThrow(); + expect(() => Accept.encoding(';q=1')).toThrow(); + expect(() => Accept.encoding('gzip;a=1')).toThrow(); }); it('obeys disallow with wildcard', () => { - const encoding = Accept.encoding('*, gzip;q=0, deflate;q=1.1', ['gzip', 'deflate']); // gzip is disallowed - expect(encoding).to.equal('deflate'); + expect(encoding).toBe('deflate'); }); }); describe('encodings()', () => { - it('parses header', () => { - const encodings = Accept.encodings('gzip;q=1.0, identity; q=0.5, *;q=0'); - expect(encodings).to.equal(['gzip', 'identity']); + expect(encodings).toEqual(['gzip', 'identity']); }); it('parses header (reverse header)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=1.0'); - expect(encodings).to.equal(['gzip', 'compress', 'identity']); + expect(encodings).toEqual(['gzip', 'compress', 'identity']); }); it('parses header (exclude encodings)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=0.0'); - expect(encodings).to.equal(['compress', 'identity']); + expect(encodings).toEqual(['compress', 'identity']); }); it('parses header (exclude identity)', () => { - const encodings = Accept.encodings('compress;q=0.5, gzip;q=1.0, identity;q=0'); - expect(encodings).to.equal(['gzip', 'compress']); + expect(encodings).toEqual(['gzip', 'compress']); }); }); diff --git a/test/index.js b/test/index.js index 4f52a6a..7739806 100755 --- a/test/index.js +++ b/test/index.js @@ -1,24 +1,13 @@ -'use strict'; +import { describe, expect, it } from 'vitest'; -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import * as Accept from '../src/index.js'; /* Accept-Charset: iso-8859-5, unicode-1-1;q=0.8 */ describe('parseAll()', () => { - it('parses all Accept headers', () => { - const headers = {}; headers.accept = 'text/plain, application/json;q=0.5, text/html;q=0.6, */*;q=0.1'; headers['accept-charset'] = 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'; @@ -26,12 +15,12 @@ describe('parseAll()', () => { headers['accept-language'] = 'da, en;q=0.7, en-gb;q=0.8'; const accept = Accept.parseAll(headers); - expect(accept.isBoom).to.not.exist(); - expect(accept).to.equal({ + expect(accept.isBoom).toBeUndefined(); + expect(accept).toStrictEqual({ charsets: ['iso-8859-5', 'unicode-1-1', '*'], encodings: ['gzip', 'compress', 'identity'], languages: ['da', 'en-gb', 'en'], - mediaTypes: ['text/plain', 'text/html', 'application/json', '*/*'] + mediaTypes: ['text/plain', 'text/html', 'application/json', '*/*'], }); }); }); diff --git a/test/index.ts b/test/index.ts index 82608ae..1bb0585 100755 --- a/test/index.ts +++ b/test/index.ts @@ -1,142 +1,212 @@ -import * as Accept from '..'; -import * as Lab from '@hapi/lab'; - - -const { expect } = Lab.types; - - -// charset() - -Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); -Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); -Accept.charset(''); -Accept.charset(); - -expect.type(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')); -expect.type(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5'])); - -expect.error(Accept.charset(123)); -expect.error(Accept.charset('', '')); -expect.error(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5')); - - -// charsets() - -Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001'); -Accept.charsets(''); -Accept.charsets(); - -expect.type(Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')); - -expect.error(Accept.charsets(123)); -expect.error(Accept.charsets('', '')); -expect.error(Accept.charsets('', [''])); - - -// encoding() - -Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0'); -Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip']); -Accept.encoding(''); -Accept.encoding(); - -expect.type(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0')); -expect.type(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip'])); - -expect.error(Accept.encoding(123)); -expect.error(Accept.encoding('', '')); -expect.error(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', 'gzip')); - - -// encodings() - -Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0'); -Accept.encodings(''); -Accept.encodings(); - -expect.type(Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0')); - -expect.error(Accept.encodings(123)); -expect.error(Accept.encodings('', '')); -expect.error(Accept.encodings('', [''])); - - -// language() - -Accept.language('en;q=0.6, en-GB;q=0.8'); -Accept.language('en;q=0.6, en-GB;q=0.8', ['en']); -Accept.language(''); -Accept.language(); - -expect.type(Accept.language('en;q=0.6, en-GB;q=0.8')); -expect.type(Accept.language('en;q=0.6, en-GB;q=0.8', ['en'])); - -expect.error(Accept.language(123)); -expect.error(Accept.language('', '')); -expect.error(Accept.language('en;q=0.6, en-GB;q=0.8', 'en')); - - -// languages() - -Accept.languages('en;q=0.6, en-GB;q=0.8'); -Accept.languages(''); -Accept.languages(); - -expect.type(Accept.languages('en;q=0.6, en-GB;q=0.8')); - -expect.error(Accept.languages(123)); -expect.error(Accept.languages('', '')); -expect.error(Accept.languages('', [''])); - - -// mediaType() - -Accept.mediaType('application/json;q=0.6, text/plain;q=0.8'); -Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain']); -Accept.mediaType(''); -Accept.mediaType(); - -expect.type(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8')); -expect.type(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain'])); - -expect.error(Accept.mediaType(123)); -expect.error(Accept.mediaType('', '')); -expect.error(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', 'test/plain')); - - -// mediaTypes() - -Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8'); -Accept.mediaTypes('', ['text/plain']); -Accept.mediaTypes(''); -Accept.mediaTypes(); - -expect.type(Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8')); - -expect.error(Accept.mediaTypes(123)); -expect.error(Accept.mediaTypes('', '')); - - -// parseAll() - -Accept.parseAll({}); -Accept.parseAll({ accept: '' }); - -const headers = { - accept: 'application/json;q=0.6, text/plain;q=0.8', - 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', - 'accept-encoding': 'gzip;q=1.0, identity; Q=0.5, *;q=0', - 'accept-language': 'en;q=0.6, en-GB;q=0.8', - ignore: {} -}; - -const all = Accept.parseAll(headers); - -expect.type(all); -expect.type(all.charsets); -expect.type(all.encodings); -expect.type(all.languages); -expect.type(all.mediaTypes); - -expect.error(Accept.parseAll()); -expect.error(Accept.parseAll({ accept: {} })); +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Accept from '../src/index.js'; + +// Negative cases assert through `toBeCallableWith`, which is erased at runtime — a bare +// `// @ts-expect-error`-ed call would still execute and throw, since vitest loads this file +// under both `include` and `typecheck.include`. + +describe('typings', () => { + describe('charset()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.charset).toBeCallableWith(); + expectTypeOf(Accept.charset).toBeCallableWith(''); + expectTypeOf(Accept.charset).toBeCallableWith('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')).toEqualTypeOf(); + expectTypeOf( + Accept.charset('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', ['iso-8859-5']), + ).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.charset).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.charset).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.charset).toBeCallableWith('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', 'iso-8859-5'); + }); + }); + + describe('charsets()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.charsets).toBeCallableWith(); + expectTypeOf(Accept.charsets).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.charsets('iso-8859-5, unicode-1-1;q=0.8, *;q=0.001')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.charsets).toBeCallableWith(123); + // @ts-expect-error charsets() takes no preferences + expectTypeOf(Accept.charsets).toBeCallableWith('', ''); + // @ts-expect-error charsets() takes no preferences + expectTypeOf(Accept.charsets).toBeCallableWith('', ['']); + }); + }); + + describe('encoding()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.encoding).toBeCallableWith(); + expectTypeOf(Accept.encoding).toBeCallableWith(''); + expectTypeOf(Accept.encoding).toBeCallableWith('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0')).toEqualTypeOf(); + expectTypeOf(Accept.encoding('gzip;q=1.0, identity; Q=0.5, *;q=0', ['gzip'])).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.encoding).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.encoding).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.encoding).toBeCallableWith('gzip;q=1.0, identity; Q=0.5, *;q=0', 'gzip'); + }); + }); + + describe('encodings()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.encodings).toBeCallableWith(); + expectTypeOf(Accept.encodings).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.encodings('gzip;q=1.0, identity; Q=0.5, *;q=0')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.encodings).toBeCallableWith(123); + // @ts-expect-error encodings() takes no preferences + expectTypeOf(Accept.encodings).toBeCallableWith('', ''); + // @ts-expect-error encodings() takes no preferences + expectTypeOf(Accept.encodings).toBeCallableWith('', ['']); + }); + }); + + describe('language()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.language).toBeCallableWith(); + expectTypeOf(Accept.language).toBeCallableWith(''); + expectTypeOf(Accept.language).toBeCallableWith('en;q=0.6, en-GB;q=0.8', ['en']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.language('en;q=0.6, en-GB;q=0.8')).toEqualTypeOf(); + expectTypeOf(Accept.language('en;q=0.6, en-GB;q=0.8', ['en'])).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.language).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.language).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.language).toBeCallableWith('en;q=0.6, en-GB;q=0.8', 'en'); + }); + }); + + describe('languages()', () => { + it('accepts an optional header', () => { + expectTypeOf(Accept.languages).toBeCallableWith(); + expectTypeOf(Accept.languages).toBeCallableWith(''); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.languages('en;q=0.6, en-GB;q=0.8')).toEqualTypeOf(); + }); + + it('rejects a non-string header and any second argument', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.languages).toBeCallableWith(123); + // @ts-expect-error languages() takes no preferences + expectTypeOf(Accept.languages).toBeCallableWith('', ''); + // @ts-expect-error languages() takes no preferences + expectTypeOf(Accept.languages).toBeCallableWith('', ['']); + }); + }); + + describe('mediaType()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.mediaType).toBeCallableWith(); + expectTypeOf(Accept.mediaType).toBeCallableWith(''); + expectTypeOf(Accept.mediaType).toBeCallableWith('application/json;q=0.6, text/plain;q=0.8', ['test/plain']); + }); + + it('returns a string', () => { + expectTypeOf(Accept.mediaType('application/json;q=0.6, text/plain;q=0.8')).toEqualTypeOf(); + expectTypeOf( + Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', ['test/plain']), + ).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.mediaType).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaType).toBeCallableWith('', ''); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaType).toBeCallableWith('application/json;q=0.6, text/plain;q=0.8', 'test/plain'); + }); + }); + + describe('mediaTypes()', () => { + it('accepts an optional header and optional preferences', () => { + expectTypeOf(Accept.mediaTypes).toBeCallableWith(); + expectTypeOf(Accept.mediaTypes).toBeCallableWith(''); + expectTypeOf(Accept.mediaTypes).toBeCallableWith('', ['text/plain']); + }); + + it('returns a string array', () => { + expectTypeOf(Accept.mediaTypes('application/json;q=0.6, text/plain;q=0.8')).toEqualTypeOf(); + }); + + it('rejects a non-string header and non-array preferences', () => { + // @ts-expect-error number is not assignable to string + expectTypeOf(Accept.mediaTypes).toBeCallableWith(123); + // @ts-expect-error string is not assignable to readonly string[] + expectTypeOf(Accept.mediaTypes).toBeCallableWith('', ''); + }); + }); + + describe('parseAll()', () => { + it('accepts a headers object', () => { + expectTypeOf(Accept.parseAll).toBeCallableWith({}); + expectTypeOf(Accept.parseAll).toBeCallableWith({ accept: '' }); + }); + + it('returns a result of string arrays', () => { + const headers = { + accept: 'application/json;q=0.6, text/plain;q=0.8', + 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8, *;q=0.001', + 'accept-encoding': 'gzip;q=1.0, identity; Q=0.5, *;q=0', + 'accept-language': 'en;q=0.6, en-GB;q=0.8', + ignore: {}, + }; + + const all = Accept.parseAll(headers); + + expectTypeOf(all).toEqualTypeOf(); + expectTypeOf(all.charsets).toEqualTypeOf(); + expectTypeOf(all.encodings).toEqualTypeOf(); + expectTypeOf(all.languages).toEqualTypeOf(); + expectTypeOf(all.mediaTypes).toEqualTypeOf(); + }); + + it('requires headers with string-typed accept values', () => { + // @ts-expect-error headers is required + expectTypeOf(Accept.parseAll).toBeCallableWith(); + // @ts-expect-error object is not assignable to string + expectTypeOf(Accept.parseAll).toBeCallableWith({ accept: {} }); + }); + }); +}); diff --git a/test/language.js b/test/language.js index 55bf008..a12b9d8 100755 --- a/test/language.js +++ b/test/language.js @@ -1,153 +1,118 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; describe('language()', () => { - it('parses the header', () => { - const language = Accept.language('da, en-GB, en'); - expect(language).to.equal('da'); + expect(language).toBe('da'); }); it('respects weights', () => { - const language = Accept.language('en;q=0.6, en-GB;q=0.8'); - expect(language).to.equal('en-gb'); + expect(language).toBe('en-gb'); }); it('requires the preferences parameter to be an array', () => { - expect(() => { - Accept.language('en;q=0.6, en-GB;q=0.8', 'en'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns empty string with header is empty', () => { - const language = Accept.language(''); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('returns empty string if header is missing', () => { - const language = Accept.language(); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('ignores an empty preferences array', () => { - const language = Accept.language('da, en-GB, en', []); - expect(language).to.equal('da'); + expect(language).toBe('da'); }); it('returns empty string if none of the preferences match', () => { - const language = Accept.language('da, en-GB, en', ['es']); - expect(language).to.equal(''); + expect(language).toBe(''); }); it('returns first preference if header has * and is unmatched', () => { - const language = Accept.language('da, en-GB, *', ['en-US']); - expect(language).to.equal('en-US'); + expect(language).toBe('en-US'); }); it('returns first found preference that header includes', () => { - const language = Accept.language('da, en-GB, en', ['en-US', 'en-GB']); - expect(language).to.equal('en-US'); + expect(language).toBe('en-US'); }); it('returns preference with highest order when equal weigths', () => { - - expect(Accept.language('da, en, en-GB', ['en', 'en-GB'])).to.equal('en'); - expect(Accept.language('da, en, en-GB', ['en-GB', 'en'])).to.equal('en-GB'); - expect(Accept.language('en, en-GB, en-US')).to.equal('en'); + expect(Accept.language('da, en, en-GB', ['en', 'en-GB'])).toBe('en'); + expect(Accept.language('da, en, en-GB', ['en-GB', 'en'])).toBe('en-GB'); + expect(Accept.language('en, en-GB, en-US')).toBe('en'); }); it('return language with heighest weight', () => { - const language = Accept.language('da;q=0.5, en;q=1', ['da', 'en']); - expect(language).to.equal('en'); + expect(language).toBe('en'); }); it('ignores preference case when matching', () => { - const language = Accept.language('da, en-GB, en-us', ['en-gb', 'en-us']); // en-GB vs en-gb - expect(language).to.equal('en-gb'); + expect(language).toBe('en-gb'); }); it('returns language using range match', () => { - - expect(Accept.language('da', ['da-DK'])).to.equal('da-DK'); - expect(Accept.language('en-US, en', ['en-GB', 'en-US'])).to.equal('en-GB'); - expect(Accept.language('da, en', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, da', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, da', ['en', 'en-GB'])).to.equal('en'); - expect(Accept.language('da, en-GB', ['da-DK', 'en-GB'])).to.equal('da-DK'); - expect(Accept.language('en, en-GB', ['en-US', 'en-GB', 'da-DK'])).to.equal('en-US'); + expect(Accept.language('da', ['da-DK'])).toBe('da-DK'); + expect(Accept.language('en-US, en', ['en-GB', 'en-US'])).toBe('en-GB'); + expect(Accept.language('da, en', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, da', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, da', ['en', 'en-GB'])).toBe('en'); + expect(Accept.language('da, en-GB', ['da-DK', 'en-GB'])).toBe('da-DK'); + expect(Accept.language('en, en-GB', ['en-US', 'en-GB', 'da-DK'])).toBe('en-US'); }); it('explicit preference overrides range match', () => { - - expect(Accept.language('da, en-GB', ['da-DK', 'en-GB', 'da'])).to.equal('en-GB'); + expect(Accept.language('da, en-GB', ['da-DK', 'en-GB', 'da'])).toBe('en-GB'); }); }); - describe('languages()', () => { - it('parses the header', () => { - const languages = Accept.languages('da, en-GB, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('orders by weight(q)', () => { - const languages = Accept.languages('da, en;q=0.7, en-GB;q=0.8'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('maintains case', () => { - const languages = Accept.languages('da, en-GB, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('drops zero weighted charsets', () => { - const languages = Accept.languages('da, en-GB, es;q=0, en'); - expect(languages).to.equal(['da', 'en-gb', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'en']); }); it('ignores invalid weights', () => { - const languages = Accept.languages('da, en-GB;q=1.1, es;q=a, en;q=0.0001'); - expect(languages).to.equal(['da', 'en-gb', 'es', 'en']); + expect(languages).toEqual(['da', 'en-gb', 'es', 'en']); }); it('return empty array when no header is present', () => { - const languages = Accept.languages(); - expect(languages).to.equal([]); + expect(languages).toEqual([]); }); it('return empty array when header is empty', () => { - const languages = Accept.languages(''); - expect(languages).to.equal([]); + expect(languages).toEqual([]); }); }); diff --git a/test/mediatype.js b/test/mediatype.js index 1102b7e..fd9849e 100755 --- a/test/mediatype.js +++ b/test/mediatype.js @@ -1,151 +1,121 @@ -'use strict'; - -const Accept = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import { describe, expect, it } from 'vitest'; +import * as Accept from '../src/index.js'; // Mediatypes describe('mediaType()', () => { - it('parses the header', () => { - const mediaType = Accept.mediaType(',text/html, text/plain,, application/json'); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('respects weights', () => { - const mediaType = Accept.mediaType('application/json;q=0.6, text/plain;q=0.8'); - expect(mediaType).to.equal('text/plain'); + expect(mediaType).toBe('text/plain'); }); it('requires the preferences parameter to be an array', () => { - expect(() => { - Accept.mediaType('application/json;q=0.6, text/plain;q=0.8', 'text/plain'); - }).to.throw('Preferences must be an array'); + }).toThrow('Preferences must be an array'); }); it('returns */* with header is empty', () => { - const mediaType = Accept.mediaType(''); - expect(mediaType).to.equal('*/*'); + expect(mediaType).toBe('*/*'); }); it('returns */* if header is missing', () => { - const mediaType = Accept.mediaType(); - expect(mediaType).to.equal('*/*'); + expect(mediaType).toBe('*/*'); }); it('ignores an empty preferences array', () => { - const mediaType = Accept.mediaType('text/plain, text/html, application/json', []); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('returns empty string if none of the preferences match', () => { - const mediaType = Accept.mediaType('text/csv, application/json, text/plain', ['text/html']); - expect(mediaType).to.equal(''); + expect(mediaType).toBe(''); }); it('returns first preference if header has */*', () => { - const mediaType = Accept.mediaType('text/html, application/json, text/plain, */*', ['text/csv']); - expect(mediaType).to.equal('text/csv'); + expect(mediaType).toBe('text/csv'); }); it('returns first found preference that header includes', () => { - const mediaType = Accept.mediaType('text/html, application/json, text/plain', ['text/csv', 'application/json']); - expect(mediaType).to.equal('application/json'); + expect(mediaType).toBe('application/json'); }); it('returns preference with highest specificity', () => { - const mediaType = Accept.mediaType('text/*, text/html, application/json', ['text/plain', 'text/html']); - expect(mediaType).to.equal('text/html'); + expect(mediaType).toBe('text/html'); }); it('return media type with highest weight', () => { - const mediaType = Accept.mediaType('text/html;q=0.5, text/plain;q=1', ['text/html', 'text/plain']); - expect(mediaType).to.equal('text/plain'); + expect(mediaType).toBe('text/plain'); }); it('prioritizes q value over wildcard', () => { - - expect(Accept.mediaType('text/plain;q=0.1, image/*;q=1', ['text/plain', 'image/jpeg'])).to.equal('image/jpeg'); + expect(Accept.mediaType('text/plain;q=0.1, image/*;q=1', ['text/plain', 'image/jpeg'])).toBe('image/jpeg'); }); it('removes extension parameters', () => { - - expect(Accept.mediaTypes('text/html;charset="ascii";q=0.5;ext=123, text/plain;other="xy z";Q=1')).to.equal([ + expect(Accept.mediaTypes('text/html;charset="ascii";q=0.5;ext=123, text/plain;other="xy z";Q=1')).toEqual([ 'text/plain;other="xy z"', - 'text/html;charset="ascii"' + 'text/html;charset="ascii"', ]); }); it('errors on invalid params', () => { - - expect(() => Accept.mediaType('text/plain;q')).to.throw(); - expect(() => Accept.mediaType('text/plain;q=')).to.throw(); + expect(() => Accept.mediaType('text/plain;q')).toThrow(); + expect(() => Accept.mediaType('text/plain;q=')).toThrow(); }); }); describe('mediaTypes()', () => { - it('returns */* when header is missing', () => { - - expect(Accept.mediaTypes()).to.equal(['*/*']); + expect(Accept.mediaTypes()).toEqual(['*/*']); }); it('parses header', () => { - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, */*;q=0.1, audio/*'); - expect(mediaTypes).to.equal(['audio/*', 'text/html', 'text/plain', 'application/json', '*/*']); + expect(mediaTypes).toEqual(['audio/*', 'text/html', 'text/plain', 'application/json', '*/*']); }); it('parses header with preferences', () => { - - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, audio/*', ['text/*', 'application/*']); - expect(mediaTypes).to.equal(['text/html', 'text/plain', 'application/json']); + const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, audio/*', [ + 'text/*', + 'application/*', + ]); + expect(mediaTypes).toEqual(['text/html', 'text/plain', 'application/json']); }); it('returns empty array when everything is disallowed', () => { - const mediaTypes = Accept.mediaTypes('*/*;q=0'); - expect(mediaTypes).to.equal([]); + expect(mediaTypes).toEqual([]); }); it('respects disallows', () => { - const mediaTypes = Accept.mediaTypes('text/plain, application/json;q=0.5, text/html, text/drop;q=0'); - expect(mediaTypes).to.equal(['text/html', 'text/plain', 'application/json']); + expect(mediaTypes).toEqual(['text/html', 'text/plain', 'application/json']); }); it('orders by weight', () => { - const mediaTypes = Accept.mediaTypes('application/json;q=0.2, text/html'); - expect(mediaTypes).to.equal(['text/html', 'application/json']); + expect(mediaTypes).toEqual(['text/html', 'application/json']); }); it('orders most specific to least specific', () => { - - const types = 'text/*, text/plain;format=flowed, text/plain, text/plain;level=1, text/html, text/plain;level=2, */*, image/*, text/rich'; + const types = + 'text/*, text/plain;format=flowed, text/plain, text/plain;level=1, text/html, text/plain;level=2, */*, image/*, text/rich'; const mediaTypes = Accept.mediaTypes(types); - expect(mediaTypes).to.equal([ + expect(mediaTypes).toEqual([ 'image/*', 'text/html', 'text/plain;format=flowed', @@ -154,47 +124,46 @@ describe('mediaTypes()', () => { 'text/plain', 'text/rich', 'text/*', - '*/*' + '*/*', ]); }); it('match subtype to preference', () => { - - expect(Accept.mediaTypes('text/html', ['TEXT/*'])).to.equal(['text/html']); + expect(Accept.mediaTypes('text/html', ['TEXT/*'])).toEqual(['text/html']); }); it('keeps wildcard behind more specific', () => { - - expect(Accept.mediaTypes('text/html, text/*')).to.equal(['text/html', 'text/*']); - expect(Accept.mediaTypes('text/*, text/html')).to.equal(['text/html', 'text/*']); + expect(Accept.mediaTypes('text/html, text/*')).toEqual(['text/html', 'text/*']); + expect(Accept.mediaTypes('text/*, text/html')).toEqual(['text/html', 'text/*']); }); it('matches */* preference to anything', () => { - - expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*'])).to.equal(['text/html', 'text/*', '*/*']); - expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*', 'TEXT/html'])).to.equal(['TEXT/html', 'text/*', '*/*']); + expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*'])).toEqual(['text/html', 'text/*', '*/*']); + expect(Accept.mediaTypes('text/html, text/*, */*', ['*/*', 'TEXT/html'])).toEqual([ + 'TEXT/html', + 'text/*', + '*/*', + ]); }); it('keeps the order of two media types with extensions', () => { - const mediaTypes = Accept.mediaTypes('text/html;level=1, text/html;level=2'); - expect(mediaTypes).to.equal(['text/html;level=1', 'text/html;level=2']); + expect(mediaTypes).toEqual(['text/html;level=1', 'text/html;level=2']); }); it('invalid header returns []', () => { - const mediaTypes = Accept.mediaTypes('ab'); - expect(mediaTypes).to.equal([]); + expect(mediaTypes).toEqual([]); }); it('invalid weight is ignored', () => { - const mediaTypes = Accept.mediaTypes('text/html;q=0.0001, text/plain;q=1.1, text/csv;q=a'); - expect(mediaTypes).to.equal(['text/csv', 'text/html', 'text/plain']); + expect(mediaTypes).toEqual(['text/csv', 'text/html', 'text/plain']); }); it('errors on invalid preference', () => { - - expect(() => Accept.mediaTypes('text/html', ['*/html'])).to.throw('Invalid media type preference contains wildcard type with a subtype'); + expect(() => Accept.mediaTypes('text/html', ['*/html'])).toThrow( + 'Invalid media type preference contains wildcard type with a subtype', + ); }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2420cb5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig;