Skip to content

Commit

Permalink
extract headerToMap
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikzogg committed Jun 5, 2022
1 parent 2748e3b commit 12d9797
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 90 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A simple negotiation library.
## Requirements

* node: 14
* [@chubbyts/chubbyts-http-types][2]: ^2.0.0
* [@chubbyts/chubbyts-http-types][2]: ^2.0.1

## Installation

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chubbyts/chubbyts-negotiation",
"version": "2.0.0",
"version": "2.0.1",
"description": "A simple negotiation library.",
"keywords": [
"chubbyts",
Expand Down
43 changes: 8 additions & 35 deletions src/accept-language-negotiator.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
import { ServerRequest } from '@chubbyts/chubbyts-http-types/dist/message';
import { NegotiatedValue, Negotiator } from './negotiation';

const resolveAcceptLanguages = (header: string): Map<string, Record<string, string>> => {
return new Map(
header
.split(',')
.map((headerValue): [string, Record<string, string>] => {
const headerValueParts = headerValue.split(';');
const locale = (headerValueParts.shift() as string).trim();
const attributes: Record<string, string> = Object.fromEntries(
headerValueParts
.filter((attribute) => -1 !== attribute.search(/=/))
.map((attribute): [string, string] => {
const [attributeKey, attributeValue] = attribute.split('=');

return [attributeKey.trim(), attributeValue.trim()];
}),
);

if (!attributes['q']) {
attributes['q'] = '1.0';
}

return [locale, attributes];
})
.sort((entryA, entryB) => entryB[1]['q'].localeCompare(entryA[1]['q'])),
);
};
import { resolveHeaderToMap, NegotiatedValue, Negotiator } from './negotiation';

const compareLanguage = (
locale: string,
Expand All @@ -50,24 +23,24 @@ const compareLanguage = (

const compareAcceptLanguages = (
supportedValues: Array<string>,
acceptLanguages: Map<string, Record<string, string>>,
headerToMap: Map<string, Record<string, string>>,
): NegotiatedValue | undefined => {
for (const [locale, attributes] of acceptLanguages.entries()) {
for (const [locale, attributes] of headerToMap.entries()) {
if (-1 !== supportedValues.indexOf(locale)) {
return { value: locale, attributes };
}
}

for (const [locale, attributes] of acceptLanguages.entries()) {
for (const [locale, attributes] of headerToMap.entries()) {
const negotiatedValue = compareLanguage(locale, supportedValues, attributes);

if (undefined !== negotiatedValue) {
return negotiatedValue;
}
}

if (acceptLanguages.has('*')) {
return { value: supportedValues[0], attributes: acceptLanguages.get('*') as Record<string, string> };
if (headerToMap.has('*')) {
return { value: supportedValues[0], attributes: headerToMap.get('*') as Record<string, string> };
}

return undefined;
Expand All @@ -82,9 +55,9 @@ export const createAcceptLanguageNegotiator = (supportedValues: Array<string>):
return undefined;
}

const acceptLanguages = resolveAcceptLanguages(acceptLanguage.join(','));
const headerToMap = resolveHeaderToMap(acceptLanguage.join(','));

return compareAcceptLanguages(supportedValues, acceptLanguages);
return compareAcceptLanguages(supportedValues, headerToMap);
},
supportedValues,
};
Expand Down
45 changes: 9 additions & 36 deletions src/accept-negotiator.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
import { ServerRequest } from '@chubbyts/chubbyts-http-types/dist/message';
import { NegotiatedValue, Negotiator } from './negotiation';

const resolveMediaTypes = (header: string): Map<string, Record<string, string>> => {
return new Map(
header
.split(',')
.map((headerValue): [string, Record<string, string>] => {
const headerValueParts = headerValue.split(';');
const mediaType = (headerValueParts.shift() as string).trim();
const attributes: Record<string, string> = Object.fromEntries(
headerValueParts
.filter((attribute) => -1 !== attribute.search(/=/))
.map((attribute): [string, string] => {
const [attributeKey, attributeValue] = attribute.split('=');

return [attributeKey.trim(), attributeValue.trim()];
}),
);

if (!attributes['q']) {
attributes['q'] = '1.0';
}

return [mediaType, attributes];
})
.sort((entryA, entryB) => entryB[1]['q'].localeCompare(entryA[1]['q'])),
);
};
import { resolveHeaderToMap, NegotiatedValue, Negotiator } from './negotiation';

const escapeStringRegexp = (regex: string): string => {
return regex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
Expand Down Expand Up @@ -55,30 +28,30 @@ const compareMediaTypeWithTypeOnly = (
const compareMediaTypes = (
supportedValues: Array<string>,
suffixSupportedValues: Map<string | undefined, string>,
mediaTypes: Map<string, Record<string, string>>,
headerToMap: Map<string, Record<string, string>>,
): NegotiatedValue | undefined => {
for (const [mediaType, attributes] of mediaTypes.entries()) {
for (const [mediaType, attributes] of headerToMap.entries()) {
if (-1 !== supportedValues.indexOf(mediaType)) {
return { value: mediaType, attributes };
}
}

for (const [mediaType, attributes] of mediaTypes.entries()) {
for (const [mediaType, attributes] of headerToMap.entries()) {
if (suffixSupportedValues.has(mediaType)) {
return { value: suffixSupportedValues.get(mediaType) as string, attributes };
}
}

for (const [mediaType, attributes] of mediaTypes.entries()) {
for (const [mediaType, attributes] of headerToMap.entries()) {
const negotiatedValue = compareMediaTypeWithTypeOnly(supportedValues, mediaType, attributes);

if (undefined !== negotiatedValue) {
return negotiatedValue;
}
}

if (mediaTypes.has('*/*')) {
return { value: supportedValues[0], attributes: mediaTypes.get('*/*') as Record<string, string> };
if (headerToMap.has('*/*')) {
return { value: supportedValues[0], attributes: headerToMap.get('*/*') as Record<string, string> };
}

return undefined;
Expand All @@ -104,9 +77,9 @@ export const createAcceptNegotiator = (supportedValues: Array<string>): Negotiat
return undefined;
}

const mediaTypes = resolveMediaTypes(accept.join(','));
const headerToMap = resolveHeaderToMap(accept.join(','));

return compareMediaTypes(supportedValues, suffixSupportedValues, mediaTypes);
return compareMediaTypes(supportedValues, suffixSupportedValues, headerToMap);
},
supportedValues,
};
Expand Down
30 changes: 15 additions & 15 deletions src/content-type-negotiator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ServerRequest } from '@chubbyts/chubbyts-http-types/dist/message';
import { NegotiatedValue, Negotiator } from './negotiation';
import { resolveHeaderToMap, NegotiatedValue, Negotiator } from './negotiation';

const compareMediaTypeWithSuffix = (
supportedValues: Array<string>,
Expand All @@ -19,37 +19,37 @@ const compareMediaTypeWithSuffix = (
}
};

const compareMediaTypes = (supportedValues: Array<string>, header: string): NegotiatedValue | undefined => {
if (-1 !== header.search(/,/)) {
const compareMediaTypes = (
supportedValues: Array<string>,
headerToMap: Map<string, Record<string, string>>,
): NegotiatedValue | undefined => {
const entries = Array.from(headerToMap.entries());
if (entries.length !== 1) {
return undefined;
}

const headerValueParts = header.split(';');
const mediaType = (headerValueParts.shift() as string).trim();
const attributes: Record<string, string> = Object.fromEntries(
headerValueParts.map((attribute: string) => {
const [attributeKey, attributeValue] = attribute.split('=');
return [attributeKey.trim(), attributeValue.trim()];
}),
);
const [mediaType, attributes] = entries[0];
const { q, ...otherAttriutes } = attributes;

if (-1 !== supportedValues.indexOf(mediaType)) {
return { value: mediaType, attributes };
return { value: mediaType, attributes: otherAttriutes };
}

return compareMediaTypeWithSuffix(supportedValues, mediaType, attributes);
return compareMediaTypeWithSuffix(supportedValues, mediaType, otherAttriutes);
};

export const createContentTypeNegotiator = (supportedValues: Array<string>): Negotiator => {
return {
negotiate: (request: ServerRequest) => {
const contentType = request.headers['content-type'];

if (!contentType || contentType.length > 1) {
if (!contentType) {
return undefined;
}

return compareMediaTypes(supportedValues, contentType[0]);
const headerToMap = resolveHeaderToMap(contentType.join(','));

return compareMediaTypes(supportedValues, headerToMap);
},
supportedValues,
};
Expand Down
28 changes: 28 additions & 0 deletions src/negotiation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,31 @@ export type Negotiator = {
negotiate: (request: ServerRequest) => NegotiatedValue | undefined;
supportedValues: Array<string>;
};

export const resolveHeaderToMap = (header: string): Map<string, Record<string, string>> => {
return new Map(
header
.split(',')
.map((headerValue): [string, Record<string, string>] => {
const headerValueParts = headerValue.split(';');
const locale = (headerValueParts.shift() as string).trim();
const attributes: Record<string, string> = Object.fromEntries(
headerValueParts
.filter((attribute) => -1 !== attribute.search(/=/))
.map((attribute): [string, string] => {
const [attributeKey, attributeValue] = attribute.split('=');

return [attributeKey.trim(), attributeValue.trim()];
}),
);

if (!attributes['q']) {
attributes['q'] = '1.0';
}

return [locale, attributes];
})
.filter(([locale]) => locale !== '')
.sort((a, b) => b[1]['q'].localeCompare(a[1]['q'])),
);
};
9 changes: 7 additions & 2 deletions tests/content-type-negotiator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('content-type-negotiator', () => {
{
contentType: ['application/xml; charset=UTF-8,'],
supportedMediaTypes: ['application/json', 'application/xml', 'application/x-yaml'],
expectedContentType: undefined,
expectedContentType: { value: 'application/xml', attributes: { charset: 'UTF-8' } },
},
{
contentType: ['xml; charset=UTF-8'],
Expand All @@ -50,8 +50,13 @@ describe('content-type-negotiator', () => {
expectedContentType: { value: 'application/xml', attributes: { charset: 'UTF-8' } },
},
{
contentType: ['application/jsonx+xml; charset=UTF-8', 'application/jsonx+xml; charset=UTF-8'],
contentType: ['application/jsonx+xml; charset=LATIN-1', 'application/jsonx+xml; charset=UTF-8'],
supportedMediaTypes: ['application/xml'],
expectedContentType: { value: 'application/xml', attributes: { charset: 'UTF-8' } },
},
{
contentType: ['application/json; charset=UTF-8', 'application/jsonx+xml; charset=UTF-8'],
supportedMediaTypes: ['application/json', 'application/xml'],
expectedContentType: undefined,
},
{
Expand Down
Loading

0 comments on commit 12d9797

Please sign in to comment.