type-is
Version:
Infer the content-type of a request.
170 lines • 5.89 kB
JavaScript
/*!
* type-is
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
import { parse, isTypeValid, isTokenValid } from "content-type";
/**
* Check if a request has a request body. A request with a body must either have
* `transfer-encoding` or `content-length` headers set.
*/
export function hasBody(req) {
return (req.headers["transfer-encoding"] !== undefined ||
!Number.isNaN(Number(req.headers["content-length"])));
}
/**
* The default behavior of `lookup` handles only a few common shorthands.
*/
export function DEFAULT_LOOKUP(value) {
switch (value) {
case "urlencoded":
return "application/x-www-form-urlencoded";
case "multipart":
return "multipart/*";
case "json":
return "application/json";
default:
return undefined;
}
}
/**
* Normalize MIME type by:
*
* - If the string contains a `/`, then it is returned as the type.
* - If the string starts with `+` (so it is a `+suffix` shorthand like `+json`), then it is expanded to contain the complete wildcard notation of `*\/*+suffix`.
* - Else the string is assumed to be a file extension and the mapped media type is returned, or the original input if there is no mapping.
*/
export function normalize(value, options) {
if (value.includes("/"))
return value;
if (value.startsWith("+"))
return `*/*${value}`;
const lookup = options?.lookup ?? DEFAULT_LOOKUP;
return lookup(value) ?? value;
}
/**
* Compile an expected mime type into a reusable matcher.
*/
export function match(expected) {
const expectedSlash = expected.indexOf("/");
if (expectedSlash === -1 || !isTypeValid(expected)) {
throw new TypeError(`Invalid mime type: ${expected}`);
}
const type = expected.slice(0, expectedSlash);
let subtype = expected.slice(expectedSlash + 1);
let suffix = "";
if (subtype.startsWith("*+")) {
suffix = subtype.slice(1);
subtype = "*";
}
if (type === "*" && subtype === "*") {
if (!suffix)
return (actual) => isTypeValid(actual);
return (actual) => {
return (actual.charAt(actual.length - suffix.length - 1) !== "/" &&
actual.endsWith(suffix) &&
isTypeValid(actual));
};
}
if (type === "*") {
return (actual) => {
return (actual.charAt(actual.length - subtype.length - 1) === "/" &&
actual.endsWith(subtype) &&
isTokenValid(actual, 0, actual.length - subtype.length - 1));
};
}
if (subtype === "*") {
return (actual) => {
return (actual.charAt(type.length) === "/" &&
actual.startsWith(type) &&
actual.endsWith(suffix) &&
isTokenValid(actual, type.length + 1, actual.length - suffix.length));
};
}
return (actual) => actual === expected;
}
/**
* Normalize a parameter value for comparison.
*/
export function DEFAULT_PARAMETER_VALUE(key, value) {
if (key === "charset")
return value.toLowerCase();
return value;
}
export class TypeIs {
hasParameters = false;
patterns = [];
parameterValue;
/**
* Compile a list of expected mime types into a reusable matcher.
*/
constructor(types, options) {
this.parameterValue = options?.parameterValue ?? DEFAULT_PARAMETER_VALUE;
for (const key of types) {
const contentType = parse(key);
const hasParameters = Object.keys(contentType.parameters).length > 0;
const type = normalize(contentType.type, options);
const parameters = contentType.parameters;
// Normalize parameter values before comparison.
for (const key of Object.keys(parameters)) {
parameters[key] = this.parameterValue(key, parameters[key]);
}
this.hasParameters ||= hasParameters;
if (Array.isArray(type)) {
for (const t of type) {
this.patterns.push({
key,
match: match(t),
parameters,
hasParameters,
});
}
}
else {
this.patterns.push({
key,
match: match(type),
parameters,
hasParameters,
});
}
}
}
/**
* Check whether a content type matches one of the configured types.
*/
is(value) {
const contentType = parse(value, { parameters: this.hasParameters });
return this.contentType(contentType);
}
/**
* Check whether a request body matches one of the configured types.
*/
request(req) {
if (!hasBody(req))
return;
const header = req.headers["content-type"];
if (!header)
return;
const value = Array.isArray(header) ? header[0] : header;
return this.is(value);
}
contentType(contentType) {
for (const pattern of this.patterns) {
if (pattern.match(contentType.type)) {
const parametersMatch = !pattern.hasParameters ||
Object.keys(pattern.parameters).every((key) => {
const actual = contentType.parameters[key];
if (actual === undefined)
return false;
const expected = pattern.parameters[key];
return expected === this.parameterValue(key, actual);
});
if (parametersMatch)
return pattern.key;
}
}
}
}
//# sourceMappingURL=index.js.map