From c6dd337e50d7abd1e24f15128f771589b85e98fa Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Tue, 19 Mar 2024 14:15:39 +0800 Subject: [PATCH] Update content negotiation setup Use more compact data structures for content negoatiation. --- examples/basic/serializers.ts | 1 + examples/basic/server.ts | 8 +-- src/core.ts | 60 +++++++++---------- src/encodings/index.ts | 1 + src/handlers.ts | 108 +++++++++++++++++----------------- src/serializers/index.ts | 1 + src/utils.ts | 6 +- test/e2e/default.test.ts | 8 +-- 8 files changed, 99 insertions(+), 94 deletions(-) diff --git a/examples/basic/serializers.ts b/examples/basic/serializers.ts index a4b0da8..5fba021 100644 --- a/examples/basic/serializers.ts +++ b/examples/basic/serializers.ts @@ -1,4 +1,5 @@ export const TEXT_SERIALIZER_PAIR = { + name: 'text/plain', serialize(obj: unknown, level = 0): string { if (Array.isArray(obj)) { return obj.map((o) => this.serialize(o)).join('\n\n'); diff --git a/examples/basic/server.ts b/examples/basic/server.ts index 4e4cad6..a1826aa 100644 --- a/examples/basic/server.ts +++ b/examples/basic/server.ts @@ -51,10 +51,10 @@ const app = application({ name: 'piano-service', dataSource, }) - .contentType('application/json', serializers.applicationJson) - .contentType('text/json', serializers.textJson) - .contentType('text/plain', TEXT_SERIALIZER_PAIR) - .encoding('utf-8', encodings.utf8) + .contentType(serializers.applicationJson) + .contentType(serializers.textJson) + .contentType(TEXT_SERIALIZER_PAIR) + .encoding(encodings.utf8) .resource(Piano) .resource(User); diff --git a/src/core.ts b/src/core.ts index 85902a7..fee0ed6 100644 --- a/src/core.ts +++ b/src/core.ts @@ -239,8 +239,8 @@ interface HandlerState { export interface ApplicationState { resources: Set>; languages: Set; - serializers: Map; - encodings: Map; + serializers: Set; + encodings: Set; } export interface BackendState { @@ -267,11 +267,11 @@ interface MiddlewareArgs { appParams: ApplicationParams; serverParams: CreateServerParams; responseBodyLanguage: Language; - responseBodyEncoding: [string, EncodingPair]; - responseBodyMediaType: [string, SerializerPair]; + responseBodyEncoding: EncodingPair; + responseBodyMediaType: SerializerPair; errorResponseBodyLanguage: Language; - errorResponseBodyEncoding: [string, EncodingPair]; - errorResponseBodyMediaType: [string, SerializerPair]; + errorResponseBodyEncoding: EncodingPair; + errorResponseBodyMediaType: SerializerPair; resource: ResourceWithDataSource; resourceId: string; query: URLSearchParams; @@ -304,9 +304,9 @@ export interface Client { } export interface Application { - contentType(mimeTypePrefix: string, serializerPair: SerializerPair): this; + contentType(serializerPair: SerializerPair): this; language(language: Language): this; - encoding(encoding: string, encodingPair: EncodingPair): this; + encoding(encodingPair: EncodingPair): this; resource(resRaw: Partial>): this; createBackend(): Backend; createClient(): Client; @@ -316,21 +316,21 @@ export const application = (appParams: ApplicationParams): Application => { const appState: ApplicationState = { resources: new Set>(), languages: new Set(), - serializers: new Map(), - encodings: new Map(), + serializers: new Set(), + encodings: new Set(), }; appState.languages.add(en); - appState.encodings.set(utf8.name, utf8); - appState.serializers.set(applicationJson.name, applicationJson); + appState.encodings.add(utf8); + appState.serializers.add(applicationJson); return { - contentType(mimeTypePrefix: string, serializerPair: SerializerPair) { - appState.serializers.set(mimeTypePrefix, serializerPair); + contentType(serializerPair: SerializerPair) { + appState.serializers.add(serializerPair); return this; }, - encoding(encoding: string, encodingPair: EncodingPair) { - appState.encodings.set(encoding, encodingPair); + encoding(encodingPair: EncodingPair) { + appState.encodings.add(encodingPair); return this; }, language(language: Language) { @@ -424,8 +424,10 @@ export const application = (appParams: ApplicationParams): Application => { const negotiator = new Negotiator(req); const availableLanguages = Array.from(appState.languages); const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? backendState.fallback.language; - const encodingCandidate = negotiator.encoding(Array.from(appState.encodings.keys())) ?? backendState.fallback.encoding; - const contentTypeCandidate = negotiator.mediaType(Array.from(appState.serializers.keys())) ?? backendState.fallback.serializer; + const availableEncodings = Array.from(appState.encodings); + const encodingCandidate = negotiator.encoding(availableEncodings.map((l) => l.name)) ?? backendState.fallback.encoding; + const availableContentTypes = Array.from(appState.serializers); + const contentTypeCandidate = negotiator.mediaType(availableContentTypes.map((l) => l.name)) ?? backendState.fallback.serializer; const fallbackMessageCollection = en as Language; const fallbackSerializerPair = applicationJson as SerializerPair; @@ -435,10 +437,10 @@ export const application = (appParams: ApplicationParams): Application => { const errorMessageCollection = availableLanguages.find((l) => l.name === errorLanguageCode) ?? fallbackMessageCollection; const errorContentType = backendState.errorHeaders.serializer ?? backendState.fallback.serializer; - const errorSerializerPair = appState.serializers.get(errorContentType) ?? fallbackSerializerPair; + const errorSerializerPair = availableContentTypes.find((l) => l.name === errorContentType) ?? fallbackSerializerPair; const errorEncodingKey = backendState.errorHeaders.encoding ?? backendState.fallback.encoding; - const errorEncoding = appState.encodings.get(errorEncodingKey) ?? fallbackEncoding; + const errorEncoding = availableEncodings.find((l) => l.name === errorEncodingKey) ?? fallbackEncoding; // TODO refactor const currentLanguageMessages = availableLanguages.find((l) => l.name === languageCandidate); @@ -456,9 +458,8 @@ export const application = (appParams: ApplicationParams): Application => { return; } - const availableMediaTypes = Array.from(appState.serializers.entries()); - const [currentContentTypeMimeType, responseMediaTypeEntry] = availableMediaTypes.find(([key]) => key === contentTypeCandidate) ?? []; - if (typeof currentContentTypeMimeType === 'undefined' || typeof responseMediaTypeEntry === 'undefined') { + const currentMediaType = availableContentTypes.find((l) => l.name === contentTypeCandidate); + if (typeof currentMediaType === 'undefined') { const data = errorMessageCollection.bodies.languageNotAcceptable(); const responseRaw = errorSerializerPair.serialize(data); const response = errorEncoding.encode(responseRaw); @@ -472,9 +473,8 @@ export const application = (appParams: ApplicationParams): Application => { return; } - const availableEncodings = Array.from(appState.encodings.entries()); - const [currentEncoding, responseBodyEncodingEntry] = availableEncodings.find(([key]) => key === encodingCandidate) ?? []; - if (typeof currentEncoding === 'undefined' || typeof responseBodyEncodingEntry === 'undefined') { + const responseBodyEncodingEntry = availableEncodings.find((l) => l.name === encodingCandidate); + if (typeof responseBodyEncodingEntry === 'undefined') { const data = errorMessageCollection.bodies.languageNotAcceptable(); const responseRaw = errorSerializerPair.serialize(data); const response = errorEncoding.encode(responseRaw); @@ -497,11 +497,11 @@ export const application = (appParams: ApplicationParams): Application => { backendState, serverParams, query, - responseBodyEncoding: [currentEncoding, responseBodyEncodingEntry], - responseBodyMediaType: [currentContentTypeMimeType, responseMediaTypeEntry], + responseBodyEncoding: responseBodyEncodingEntry, + responseBodyMediaType: currentMediaType, responseBodyLanguage: currentLanguageMessages, - errorResponseBodyMediaType: [errorContentType, errorSerializerPair], - errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], + errorResponseBodyMediaType: errorSerializerPair, + errorResponseBodyEncoding: errorEncoding, errorResponseBodyLanguage: errorMessageCollection, }; diff --git a/src/encodings/index.ts b/src/encodings/index.ts index c5c3881..21fcf49 100644 --- a/src/encodings/index.ts +++ b/src/encodings/index.ts @@ -1,6 +1,7 @@ export * as utf8 from './utf-8'; export interface EncodingPair { + name: string; encode: (str: string) => Buffer; decode: (buf: Buffer) => string; } diff --git a/src/handlers.ts b/src/handlers.ts index 2f7ef17..5bc0c36 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -38,9 +38,9 @@ export const handleGetRoot: Middleware = ({ appState, appParams, serverParams, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, }) => (_req: IncomingMessage, res: ServerResponse) => { const data = { @@ -49,7 +49,7 @@ export const handleGetRoot: Middleware = ({ let serialized; try { - serialized = responseBodySerializerPair.serialize(data); + serialized = responseBodyMediaType.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -63,7 +63,7 @@ export const handleGetRoot: Middleware = ({ let encoded; try { - encoded = encoding.encode(serialized); + encoded = responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -76,9 +76,9 @@ export const handleGetRoot: Middleware = ({ } const theHeaders: Record = { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, }; const registeredResources = Array.from(appState.resources); @@ -107,9 +107,9 @@ export const handleGetRoot: Middleware = ({ export const handleGetCollection: Middleware = ({ resource, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, backendState, query, @@ -148,7 +148,7 @@ export const handleGetCollection: Middleware = ({ let serialized; try { - serialized = responseBodySerializerPair.serialize(data); + serialized = responseBodyMediaType.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -162,7 +162,7 @@ export const handleGetCollection: Middleware = ({ let encoded; try { - encoded = encoding.encode(serialized); + encoded = responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -175,9 +175,9 @@ export const handleGetCollection: Middleware = ({ } const headers: Record = { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, }; if (typeof totalItemCount !== 'undefined') { @@ -195,9 +195,9 @@ export const handleGetCollection: Middleware = ({ export const handleGetItem: Middleware = ({ resourceId, resource, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, }) => async (_req: IncomingMessage, res: ServerResponse) => { try { @@ -229,7 +229,7 @@ export const handleGetItem: Middleware = ({ let serialized: string | null; try { - serialized = data === null ? null : responseBodySerializerPair.serialize(data); + serialized = data === null ? null : responseBodyMediaType.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -243,7 +243,7 @@ export const handleGetItem: Middleware = ({ let encoded; try { - encoded = serialized === null ? null : encoding.encode(serialized); + encoded = serialized === null ? null : responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -257,9 +257,9 @@ export const handleGetItem: Middleware = ({ if (encoded) { res.writeHead(constants.HTTP_STATUS_OK, { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, }); res.statusMessage = responseBodyLanguage.statusMessages.resourceFetched(resource) res.end(encoded); @@ -353,12 +353,12 @@ export const handlePatchItem: Middleware = ({ appState, resource, resourceId, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, - errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], - errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], + errorResponseBodyMediaType, + errorResponseBodyEncoding, }) => async (req: IncomingMessage, res: ServerResponse) => { const { deserializerPair: requestBodyDeserializerPair, encodingPair: requestBodyEncodingPair } = getDeserializerObjects(appState, req); if (typeof requestBodyDeserializerPair === 'undefined' || typeof requestBodyEncodingPair === 'undefined') { @@ -440,16 +440,16 @@ export const handlePatchItem: Middleware = ({ } // TODO better error reporting, localizable messages // TODO handle error handlers' errors - const serialized = errorSerializerPair.serialize( + const serialized = errorResponseBodyMediaType.serialize( err.issues.map((i) => ( `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` )), ); - const encoded = errorEncoding.encode(serialized); + const encoded = errorResponseBodyEncoding.encode(serialized); res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { ...headers, - 'Content-Type': errorMediaType, - 'Content-Encoding': errorEncodingKey, + 'Content-Type': errorResponseBodyMediaType.name, + 'Content-Encoding': errorResponseBodyEncoding.name, }) res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResourcePatch(resource); res.end(encoded); @@ -475,7 +475,7 @@ export const handlePatchItem: Middleware = ({ let serialized; try { - serialized = responseBodySerializerPair.serialize(newObject); + serialized = responseBodyMediaType.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -489,7 +489,7 @@ export const handlePatchItem: Middleware = ({ let encoded; try { - encoded = encoding.encode(serialized); + encoded = responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -502,9 +502,9 @@ export const handlePatchItem: Middleware = ({ } res.writeHead(constants.HTTP_STATUS_OK, { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, }); res.statusMessage = responseBodyLanguage.statusMessages.resourcePatched(resource); res.end(encoded); @@ -519,12 +519,12 @@ export const handleCreateItem: Middleware = ({ appState, serverParams, backendState, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, - errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], - errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], + errorResponseBodyMediaType, + errorResponseBodyEncoding, resource, }) => async (req: IncomingMessage, res: ServerResponse) => { const { deserializerPair: requestBodyDeserializerPair, encodingPair: requestBodyEncodingPair } = getDeserializerObjects(appState, req); @@ -562,16 +562,16 @@ export const handleCreateItem: Middleware = ({ } // TODO better error reporting, localizable messages // TODO handle error handlers' errors - const serialized = errorSerializerPair.serialize( + const serialized = errorResponseBodyMediaType.serialize( err.issues.map((i) => ( `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` )), ); - const encoded = errorEncoding.encode(serialized); + const encoded = errorResponseBodyEncoding.encode(serialized); res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { ...headers, - 'Content-Type': errorMediaType, - 'Content-Encoding': errorEncodingKey, + 'Content-Type': errorResponseBodyMediaType.name, + 'Content-Encoding': errorResponseBodyEncoding.name, }) res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(encoded); @@ -628,7 +628,7 @@ export const handleCreateItem: Middleware = ({ let serialized; try { - serialized = responseBodySerializerPair.serialize(newObject); + serialized = responseBodyMediaType.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -642,7 +642,7 @@ export const handleCreateItem: Middleware = ({ let encoded; try { - encoded = encoding.encode(serialized); + encoded = responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -655,9 +655,9 @@ export const handleCreateItem: Middleware = ({ } const headers: Record = { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, 'Location': `${serverParams.baseUrl}/${resource.state.routeName}/${newId}` }; if (typeof totalItemCount !== 'undefined') { @@ -674,12 +674,12 @@ export const handleCreateItem: Middleware = ({ export const handleEmplaceItem: Middleware = ({ appState, serverParams, - responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], + responseBodyMediaType, responseBodyLanguage, - responseBodyEncoding: [encodingKey, encoding], + responseBodyEncoding, errorResponseBodyLanguage, - errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], - errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], + errorResponseBodyMediaType, + errorResponseBodyEncoding, resource, resourceId, backendState, @@ -731,16 +731,16 @@ export const handleEmplaceItem: Middleware = ({ } // TODO better error reporting, localizable messages // TODO handle error handlers' errors - const serialized = errorSerializerPair.serialize( + const serialized = errorResponseBodyMediaType.serialize( err.issues.map((i) => ( `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` )), ); - const encoded = errorEncoding.encode(serialized); + const encoded = errorResponseBodyEncoding.encode(serialized); res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { ...headers, - 'Content-Type': errorMediaType, - 'Content-Encoding': errorEncodingKey, + 'Content-Type': errorResponseBodyMediaType.name, + 'Content-Encoding': errorResponseBodyEncoding.name, }) res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(encoded); @@ -781,7 +781,7 @@ export const handleEmplaceItem: Middleware = ({ let serialized; try { - serialized = responseBodySerializerPair.serialize(newObject); + serialized = responseBodyMediaType.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -795,7 +795,7 @@ export const handleEmplaceItem: Middleware = ({ let encoded; try { - encoded = encoding.encode(serialized); + encoded = responseBodyEncoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { 'Content-Language': errorResponseBodyLanguage.name, @@ -808,9 +808,9 @@ export const handleEmplaceItem: Middleware = ({ } const headers: Record = { - 'Content-Type': responseBodyMediaType, + 'Content-Type': responseBodyMediaType.name, 'Content-Language': responseBodyLanguage.name, - 'Content-Encoding': encodingKey, + 'Content-Encoding': responseBodyEncoding.name, }; let totalItemCount: number | undefined; if (backendState.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { diff --git a/src/serializers/index.ts b/src/serializers/index.ts index e6c71b7..151723a 100644 --- a/src/serializers/index.ts +++ b/src/serializers/index.ts @@ -2,6 +2,7 @@ export * as applicationJson from './application/json'; export * as textJson from './application/json'; export interface SerializerPair { + name: string; serialize: (object: T) => string; deserialize: (s: string) => T; } diff --git a/src/utils.ts b/src/utils.ts index b8841ed..0db81af 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,8 +6,10 @@ import {EncodingPair} from './encodings'; import {ApplicationState} from './core'; export const getDeserializerObjects = (appState: ApplicationState, req: IncomingMessage) => { - const deserializerPair = appState.serializers.get(req.headers['content-type'] ?? 'application/octet-stream'); - const encodingPair = appState.encodings.get(req.headers['content-encoding'] ?? 'utf-8'); + const availableSerializers = Array.from(appState.serializers); + const availableEncodings = Array.from(appState.encodings); + const deserializerPair = availableSerializers.find((l) => l.name === (req.headers['content-type'] ?? 'application/octet-stream')); + const encodingPair = availableEncodings.find((l) => l.name === (req.headers['content-encoding'] ?? 'utf-8')); return { deserializerPair, encodingPair, diff --git a/test/e2e/default.test.ts b/test/e2e/default.test.ts index b3eccfa..7a13d1e 100644 --- a/test/e2e/default.test.ts +++ b/test/e2e/default.test.ts @@ -34,8 +34,8 @@ import {constants} from 'http2'; const PORT = 3000; const HOST = 'localhost'; -const ACCEPT_ENCODING = 'utf-8'; -const ACCEPT = 'application/json'; +const ACCEPT_ENCODING = encodings.utf8.name; +const ACCEPT = serializers.applicationJson.name; const autoIncrement = async (dataSource: DataSource) => { const data = await dataSource.getMultiple() as Record[]; @@ -94,8 +94,8 @@ describe('yasumi', () => { name: 'piano-service', dataSource: (resource) => new dataSources.jsonlFile.DataSource(resource, baseDir), }) - .contentType(ACCEPT, serializers.applicationJson) - .encoding(ACCEPT_ENCODING, encodings.utf8) + .contentType(serializers.applicationJson) + .encoding(encodings.utf8) .resource(Piano); const backend = app