-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmedia_types.ts
233 lines (199 loc) · 6.36 KB
/
media_types.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright 2018-2024 the oak authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-irregular-whitespace
/**
* Several APIs designed for processing of media types in request bodies.
*
* `MediaType`, `parse()` and `format()` are inspired media-typer at
* https://github.com/jshttp/media-typer/ which is licensed as follows:
*
* Copyright(c) 2014-2017 Douglas Christopher Wilson
*
* MIT License
*
* `matches()` is inspired by type-is at https://github.com/jshttp/type-is/
* which is licensed as follows:
*
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2015 Douglas Christopher Wilson
*
* MIT License
*
* @module
*/
import { typeByExtension } from "jsr:@std/media-types@^1.0/type-by-extension";
const SUBTYPE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/;
const TYPE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/;
const TYPE_RE =
/^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/;
function mediaTypeMatch(expected: string | undefined, actual: string): boolean {
if (!expected) {
return false;
}
const actualParts = actual.split("/");
const expectedParts = expected.split("/");
if (actualParts.length !== 2 || expectedParts.length !== 2) {
return false;
}
const [actualType, actualSubtype] = actualParts;
const [expectedType, expectedSubtype] = expectedParts;
if (expectedType !== "*" && expectedType !== actualType) {
return false;
}
if (expectedSubtype.substring(0, 2) === "*+") {
return expectedSubtype.length <= actualSubtype.length + 1 &&
expectedSubtype.substring(1) ===
actualSubtype.substring(
actualSubtype.length + 1 - expectedSubtype.length,
);
}
if (expectedSubtype !== "*" && expectedSubtype !== actualSubtype) {
return false;
}
return true;
}
function normalize(mediaType: string): string | undefined {
if (mediaType === "urlencoded") {
return "application/x-www-form-urlencoded";
}
if (mediaType === "multipart") {
return "multipart/*";
}
if (mediaType.startsWith("+")) {
return `*/*${mediaType}`;
}
return mediaType.includes("/") ? mediaType : typeByExtension(mediaType);
}
function normalizeType(value: string): string | undefined {
try {
const [type] = value.split(/\s*;/);
const mediaType = MediaType.parse(type);
return mediaType.toString();
} catch {
return undefined;
}
}
/** A class which encapsulates the information in a media type, allowing
* inspecting of modifying individual parts of the media type. */
export class MediaType {
#subtype!: string;
#suffix?: string;
#type!: string;
/** Create an instance of {@linkcode MediaType} by providing the components
* of `type`, `subtype` and optionally a `suffix`. */
constructor(type: string, subtype: string, suffix?: string) {
this.type = type;
this.subtype = subtype;
if (suffix) {
this.suffix = suffix;
}
}
/** The subtype of the media type. */
set subtype(value: string) {
if (!SUBTYPE_NAME_RE.test(value)) {
throw new TypeError("Invalid subtype.");
}
this.#subtype = value;
}
/** The subtype of the media type. */
get subtype(): string {
return this.#subtype;
}
/** The optional suffix of the media type. */
set suffix(value: string | undefined) {
if (value && !TYPE_NAME_RE.test(value)) {
throw new TypeError("Invalid suffix.");
}
this.#suffix = value;
}
/** The optional suffix of the media type. */
get suffix(): string | undefined {
return this.#suffix;
}
/** The type of the media type. */
set type(value: string) {
if (!TYPE_NAME_RE.test(value)) {
throw new TypeError("Invalid type.");
}
this.#type = value;
}
/** The type of the media type. */
get type(): string {
return this.#type;
}
/** Return the parsed media type in its valid string format. */
toString(): string {
return this.#suffix
? `${this.#type}/${this.#subtype}+${this.#suffix}`
: `${this.#type}/${this.#subtype}`;
}
/** Take a string and attempt to parse it into a {@linkcode MediaType}
* object. */
static parse(value: string): MediaType {
const match = TYPE_RE.exec(value.toLowerCase());
if (!match) {
throw new TypeError("Invalid media type.");
}
let [, type, subtype] = match;
let suffix: string | undefined;
const idx = subtype.lastIndexOf("+");
if (idx >= 0) {
suffix = subtype.substring(idx + 1);
subtype = subtype.substring(0, idx);
}
return new this(type, subtype, suffix);
}
}
/** Determines if the provided media type matches one of the supplied media
* types. If there is a match, the matched media type is returned, otherwise
* `undefined` is returned.
*
* Each type in the media types array can be one of the following:
*
* - A file extension name such as `json`. This name will be returned if
* matched.
* - A media type such as `application/json`.
* - A media type with a wildcard such as `*/*` or `*/json` or `application/*`.
* The full media type will be returned if matched.
* - A suffix such as `+json`. This can be combined with a wildcard such as
* `*/vnd+json` or `application/*+json`. The full mime type will be returned
* if matched.
* - Special cases of `urlencoded` and `multipart` which get normalized to
* `application/x-www-form-urlencoded` and `multipart/*` respectively.
*/
export function matches(
value: string,
mediaTypes: string[],
): string | undefined {
const normalized = normalizeType(value);
if (!normalized) {
return undefined;
}
if (!mediaTypes.length) {
return normalized;
}
for (const mediaType of mediaTypes) {
if (mediaTypeMatch(normalize(mediaType), normalized)) {
return mediaType.startsWith("+") || mediaType.includes("*")
? normalized
: mediaType;
}
}
return undefined;
}
/**
* Convert a type, subtype and optional suffix of a media type into its valid
* string form.
*/
export function format(
value: { type: string; subtype: string; suffix?: string },
): string {
const mediaType = value instanceof MediaType
? value
: new MediaType(value.type, value.subtype, value.suffix);
return mediaType.toString();
}
/** Parses a media type into a {@linkcode MediaType} object which provides
* parts of the media type as individual properties. */
export function parse(value: string): MediaType {
return MediaType.parse(value);
}