Browse Source

Update content negotiation setup

Use more compact data structures for content negoatiation.
master
TheoryOfNekomata 7 months ago
parent
commit
c6dd337e50
8 changed files with 99 additions and 94 deletions
  1. +1
    -0
      examples/basic/serializers.ts
  2. +4
    -4
      examples/basic/server.ts
  3. +30
    -30
      src/core.ts
  4. +1
    -0
      src/encodings/index.ts
  5. +54
    -54
      src/handlers.ts
  6. +1
    -0
      src/serializers/index.ts
  7. +4
    -2
      src/utils.ts
  8. +4
    -4
      test/e2e/default.test.ts

+ 1
- 0
examples/basic/serializers.ts View File

@@ -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');


+ 4
- 4
examples/basic/server.ts View File

@@ -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);



+ 30
- 30
src/core.ts View File

@@ -239,8 +239,8 @@ interface HandlerState {
export interface ApplicationState {
resources: Set<ResourceWithDataSource<any>>;
languages: Set<Language>;
serializers: Map<string, SerializerPair>;
encodings: Map<string, EncodingPair>;
serializers: Set<SerializerPair>;
encodings: Set<EncodingPair>;
}

export interface BackendState {
@@ -267,11 +267,11 @@ interface MiddlewareArgs<T extends v.BaseSchema> {
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<T>;
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<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>): this;
createBackend(): Backend;
createClient(): Client;
@@ -316,21 +316,21 @@ export const application = (appParams: ApplicationParams): Application => {
const appState: ApplicationState = {
resources: new Set<ResourceWithDataSource<any>>(),
languages: new Set<Language>(),
serializers: new Map<string, SerializerPair>(),
encodings: new Map<string, EncodingPair>(),
serializers: new Set<SerializerPair>(),
encodings: new Set<EncodingPair>(),
};

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,
};



+ 1
- 0
src/encodings/index.ts View File

@@ -1,6 +1,7 @@
export * as utf8 from './utf-8';

export interface EncodingPair {
name: string;
encode: (str: string) => Buffer;
decode: (buf: Buffer) => string;
}

+ 54
- 54
src/handlers.ts View File

@@ -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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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') {


+ 1
- 0
src/serializers/index.ts View File

@@ -2,6 +2,7 @@ export * as applicationJson from './application/json';
export * as textJson from './application/json';

export interface SerializerPair {
name: string;
serialize: <T>(object: T) => string;
deserialize: <T>(s: string) => T;
}

+ 4
- 2
src/utils.ts View File

@@ -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,


+ 4
- 4
test/e2e/default.test.ts View File

@@ -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<string, string>[];
@@ -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


Loading…
Cancel
Save