diff --git a/README.md b/README.md index a34b0fa..e656d0e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ constructor(updateInterval?: number = 86400000) ```typescript // Getters -list(): { [extension: string]: string[] } // List of all extensions with their media types +list(): { [extension: string]: MIMEType[] } // List of all extensions with their media types updateInterval(): number versions(): { apache: string, debian: string, nginx: string } ``` @@ -42,7 +42,7 @@ versions(): { apache: string, debian: string, nginx: string } * @throws {TypeError} Invalid updateInterval * @see https://developer.mozilla.org/en-US/docs/Web/API/setInterval#delay */ -updateInterval(updateInterval?: number = 86400000) +updateInterval(updateInterval?: number = 86400000): void ``` ```typescript @@ -56,7 +56,7 @@ updateInterval(updateInterval?: number = 86400000) delete( extension: string mediaType: string, // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#structure_of_a_mime_type -) boolean +): boolean /** * @method @@ -65,7 +65,7 @@ delete( */ get( path: string // https://nodejs.org/api/path.html#pathparsepath -): string[] // Media type list +): MIMEType[] // https://nodejs.org/api/util.html#class-utilmimetype /** * @method @@ -77,18 +77,18 @@ get( set( extension: string mediaType: string, // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#structure_of_a_mime_type -) boolean +): boolean /** * @method * @fires MediaTypes#update */ -update(force?: boolean = false): Promise // List of new inserted media types +update(force?: boolean = false): Promise // List of new inserted media types ``` ```typescript // Events -on('update', callback: (list: { [extension: string]: string[] }) => void): void +on('update', callback: (list: { [extension: string]: MIMEType[] }) => void): void on('error', callback: (error: Error) => void): void ``` diff --git a/package.json b/package.json index dc99cda..415881d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "This is a comprehensive compilation of media types that is periodically updated through the following projects: Apache, NGINX and Debian", "main": "./src/MediaTypes.js", "engines": { - "node": ">=18" + "node": ">=18.13" }, "scripts": { "test": "jest ./test", diff --git a/src/MediaTypes.js b/src/MediaTypes.js index 730c994..76b25f0 100644 --- a/src/MediaTypes.js +++ b/src/MediaTypes.js @@ -1,6 +1,13 @@ +'use strict' + const fs = require('node:fs') const { parse, join } = require('node:path') const { EventEmitter } = require('node:events') +const { MIMEType } = require('node:util') + +function removeDuplicates (array) { + return array.filter((v, i, a) => a.findIndex(t => t.essence === v.essence) === i) +} /** * @class @@ -19,8 +26,6 @@ class MediaTypes { #updateInterval #updateLoop - // https://www.rfc-editor.org/rfc/rfc6838#section-4.2 - #formatMediaType = /^(?(x-[a-z0-9]{1,62}|[a-z0-9]{1,64}))\/(?[a-z0-9!#$&\-^_.+]{1,64})$/i #formatExtension = /^[a-z0-9!#$&\-^_+]+$/i /** @@ -40,7 +45,11 @@ class MediaTypes { try { const { mediaTypes, versions } = JSON.parse(fs.readFileSync(join(__dirname, 'DB.json')).toString('utf8')) - this.#mediaTypes = mediaTypes + this.#mediaTypes = Object.keys(mediaTypes).reduce((acc, key) => { + acc[key] = mediaTypes[key].map(mediaType => new MIMEType(mediaType)) + + return acc + }, {}) this.#versions = versions } catch (err) { this.#mediaTypes = {} @@ -54,6 +63,14 @@ class MediaTypes { this.updateInterval = updateInterval } + #isMediaType = mediaType => { + try { + return new MIMEType(mediaType) + } catch (err) { + return false + } + } + #updateList = content => { const list = {} @@ -62,16 +79,16 @@ class MediaTypes { if (extension in this.#mediaTypes) { content[extension].forEach(mediaType => { - mediaType = mediaType.trim().toLowerCase() + mediaType = new MIMEType(mediaType) - if (!this.#mediaTypes[extension].includes(mediaType)) { + if (!this.#mediaTypes[extension].some(MT => MT.essence === mediaType.essence)) { this.#mediaTypes[extension] = this.#mediaTypes[extension].concat(mediaType).sort() list[extension] = (list[extension] || []).concat(mediaType) } }) } else { - list[extension] = this.#mediaTypes[extension] = content[extension] + list[extension] = this.#mediaTypes[extension] = removeDuplicates(content[extension].map(mediaType => new MIMEType(mediaType))) } } @@ -93,9 +110,9 @@ class MediaTypes { line = line.match(/^(?[^\s]+)\s+(?.*)$/) - const mediaType = line?.groups?.mediaType?.trim()?.toLowerCase() + const mediaType = line?.groups?.mediaType - if (this.#formatMediaType.test(mediaType)) { + if (this.#isMediaType(mediaType)) { line?.groups?.extensions?.split(/\s+/)?.forEach(extension => { extension = extension?.trim()?.toLowerCase() @@ -116,7 +133,7 @@ class MediaTypes { * * @fires MediaTypes#update * - * @return {Promise>} List of all extensions with their media types + * @return {Promise>} List of all extensions with their media types */ update = (force = false) => { return Promise.allSettled([ @@ -215,7 +232,7 @@ class MediaTypes { list = Object.keys(list).reduce((acc, cur) => { Object.keys(list[cur].content).forEach(key => { - acc[key] = [...new Set((acc[key] || []).concat(list[cur].content[key]))] + acc[key] = removeDuplicates((acc[key] || []).concat(list[cur].content[key])) }) return acc @@ -225,7 +242,7 @@ class MediaTypes { * Update event * * @event MediaTypes#update - * @type {Object.} + * @type {Object.} */ this.#eventEmitter.emit('update', list) @@ -234,7 +251,7 @@ class MediaTypes { } /** - * @return {Object.} + * @return {Object.} */ get list () { return this.#mediaTypes @@ -301,7 +318,7 @@ class MediaTypes { * @throws {TypeError} Invalid path * @throws {SyntaxError} Invalid extension * - * @return {string[]} + * @return {MIMEType[]} */ get = path => { if (typeof path !== 'string') { @@ -315,7 +332,7 @@ class MediaTypes { throw new SyntaxError('Invalid extension') } - return this.#mediaTypes[extension] || [] + return (this.#mediaTypes[extension] || []) } /** @@ -339,7 +356,7 @@ class MediaTypes { if (typeof mediaType !== 'string') { throw new TypeError('Invalid mediaType') - } else if (!this.#formatMediaType.test(mediaType)) { + } else if (!this.#isMediaType(mediaType)) { throw new SyntaxError('Invalid mediaType') } @@ -383,7 +400,7 @@ class MediaTypes { if (typeof mediaType !== 'string') { throw new TypeError('Invalid mediaType') - } else if (!this.#formatMediaType.test(mediaType)) { + } else if (!this.#isMediaType(mediaType)) { throw new SyntaxError('Invalid mediaType') } @@ -391,7 +408,7 @@ class MediaTypes { return false } - const i = this.#mediaTypes[extension].indexOf(mediaType.trim().toLowerCase()) + const i = this.#mediaTypes[extension].findIndex(MT => MT.essence === new MIMEType(mediaType).essence) if (i < 0) { return false diff --git a/test/MediaTypes.spec.js b/test/MediaTypes.spec.js index 89311c0..a3ec26e 100644 --- a/test/MediaTypes.spec.js +++ b/test/MediaTypes.spec.js @@ -1,5 +1,8 @@ +'use strict' + const fs = require('node:fs') const { EventEmitter, errorMonitor } = require('node:events') +const { MIMEType } = require('node:util') jest.useFakeTimers() jest.spyOn(global, 'setInterval') @@ -131,7 +134,7 @@ describe('Attributes', () => { const mediaType = new MediaTypes() expect(mediaType.list).toMatchObject({ - txt: ['text/plain'] + txt: [new MIMEType('text/plain')] }) expect(mediaType.versions).toMatchObject({ @@ -197,7 +200,7 @@ describe('Methods', () => { const mediaType = new MediaTypes(-1); ['path/to/fileName.txt', 'fileName.txt'].forEach(path => { - expect(mediaType.get(path)).toContain('text/plain') + expect(mediaType.get(path)).toContainEqual(new MIMEType('text/plain')) }) }) }) @@ -233,17 +236,19 @@ describe('Methods', () => { const extension = 'test' const contentType = ['application/x-test', 'application/octet-stream'] - expect(mediaType.set(extension, contentType[0])).toBeTruthy() - expect(mediaType.get(`fileName.${extension}`)).toContain(contentType[0]) + expect(mediaType.set(extension, `${contentType[0]};key=value`)).toBeTruthy() + expect(mediaType.set(extension, contentType[0])).toBeFalsy() + expect(mediaType.get(`fileName.${extension}`)).toContainEqual(new MIMEType(`${contentType[0]};key=value`)) expect(mediaType.set(extension, contentType[1])).toBeTruthy() - expect(mediaType.get(`fileName.${extension}`)).toContain(contentType[1]) + expect(mediaType.get(`fileName.${extension}`)).toContainEqual(new MIMEType(contentType[1])) }) test('Given that one wants to set an already existing media type into the module list', () => { const mediaType = new MediaTypes(-1) expect(mediaType.set('txt', 'text/plain')).toBeFalsy() + expect(mediaType.set('txt', 'text/plain;key=value')).toBeFalsy() }) }) @@ -280,7 +285,7 @@ describe('Methods', () => { expect(mediaType.delete(extension, contentType)).toBeTruthy() expect(extension in mediaType.list).toBeFalsy() - expect(mediaType.get(`fileName.${extension}`)).not.toContain(contentType) + expect(mediaType.get(`fileName.${extension}`)).not.toContainEqual(new MIMEType(contentType)) }) test('Given that one wants to delete a media type that does not exist in the module list', () => { @@ -297,7 +302,7 @@ describe('Methods', () => { expect(mediaType.set(extension, 'application/octet-stream')).toBeTruthy() expect(mediaType.delete(extension, 'text/plain')).toBeTruthy() - expect(mediaType.get(`fileName.${extension}`)).toContain('application/octet-stream') + expect(mediaType.get(`fileName.${extension}`)).toContainEqual(new MIMEType('application/octet-stream')) }) }) @@ -315,7 +320,7 @@ describe('Methods', () => { await expect(mediaType.update().then(res => { return Object.keys(res) })).resolves.toContain(extension) - expect(mediaType.get(`fileName.${extension}`)).toContain(contentType) + expect(mediaType.get(`fileName.${extension}`)).toContainEqual(new MIMEType(contentType)) }) test('Given that one wants to try to force update the list of media types at some point', async () => { @@ -336,7 +341,7 @@ describe('Methods', () => { return Object.keys(res) })).resolves.toContain(extension) - expect(mediaType.get(`fileName.${extension}`)).toContain(contentType) + expect(mediaType.get(`fileName.${extension}`)).toContainEqual(new MIMEType(contentType)) }) }) })