feat(http): added #getContentTypeEncoding utility function
MODIFY
packages/core/src/+internal/utils/any.util.ts
packages/core/src/+internal/utils/any.util.ts
-import { UnaryFunction } from 'rxjs';
+import { defer, Observable, of, UnaryFunction } from 'rxjs';
import { pipe, identity } from 'fp-ts/lib/function';
+import { IO } from 'fp-ts/lib/IO';
import * as O from 'fp-ts/lib/Option';
import * as E from 'fp-ts/lib/Either';
+import { Task } from 'fp-ts/lib/Task';
import { isString } from './string.util';
export const isNonNullable = <T>(value: T): value is NonNullable<T> =>
@@ -54,3 +56,14 @@ export const isEmpty = (obj: Record<string, unknown>): boolean =>
isNullable(obj)
? true
: Object.keys(obj).length === 0;
+
+const fromIO = <T>(fa: IO<T>): Observable<T> =>
+ defer(() => of(fa()));
+
+const fromTask = <T>(fa: Task<T>): Observable<T> =>
+ defer(fa);
+
+export const FpRx = {
+ fromIO,
+ fromTask,
+};

MODIFY
packages/http/src/+internal/contentType.util.spec.ts
packages/http/src/+internal/contentType.util.spec.ts
-import { getContentType } from './contentType.util';
+import * as O from 'fp-ts/lib/Option';
+import { getContentType, getContentTypeEncoding, getContentTypeUnsafe } from './contentType.util';
-test('#getContentType returns content type from headers object', () => {
- const contentType = 'application/json';
- expect(getContentType({ 'content-type': [contentType] })).toBe(contentType);
- expect(getContentType({ 'Content-Type': contentType })).toBe(contentType);
+describe('#getContentType', () => {
+ test('returns "some" if headers object contains "Content-Type" header', () => {
+ expect(getContentType({ 'content-type': ['application/json'] })).toEqual(O.some('application/json'));
+ expect(getContentType({ 'Content-Type': 'application/json' })).toEqual(O.some('application/json'));
});
-test('#getContentType returns empty string if headers object does not contain Content-Type header', () => {
- expect(getContentType({})).toBe('');
+ test('returns "none" if headers object does not contain "Content-Type" header', () => {
+ expect(getContentType({})).toEqual(O.none);
+ });
+});
+
+describe('#getContentTypeUnsafe', () => {
+ test('returns content type from headers object', () => {
+ expect(getContentTypeUnsafe({ 'content-type': ['application/json'] })).toEqual('application/json');
+ expect(getContentTypeUnsafe({ 'Content-Type': 'application/json' })).toEqual('application/json');
+ });
+
+ test('returns empty string if headers object does not contain "Content-Type" header', () => {
+ expect(getContentTypeUnsafe({})).toEqual('');
+ });
+});
+
+describe('#getContentTypeEncoding', () => {
+ test('returns "some" if headers object contains "Content-Type" header with "charset" part', () => {
+ expect(getContentTypeEncoding({ 'content-type': 'application/json; charset=utf8' })).toEqual(O.some('utf8'));
+ });
+
+ test('returns "none" if headers object doesn\'t contain "Content-Type" header', () => {
+ expect(getContentTypeEncoding({})).toEqual(O.none);
+ });
+
+ test('returns "none" if headers object doesn\'t contain "charset" part"', () => {
+ expect(getContentTypeEncoding({ 'content-type': 'application/json' })).toEqual(O.none);
+ expect(getContentTypeEncoding({ 'content-type': 'application/json;' })).toEqual(O.none);
+ expect(getContentTypeEncoding({ 'Content-Type': 'application/json; charset' })).toEqual(O.none);
+ expect(getContentTypeEncoding({ 'Content-Type': 'application/json; charset=' })).toEqual(O.none);
+ });
});

MODIFY
packages/http/src/+internal/contentType.util.ts
packages/http/src/+internal/contentType.util.ts
+import * as O from 'fp-ts/lib/Option';
+import * as A from 'fp-ts/lib/Array';
+import { constant, pipe } from 'fp-ts/lib/function';
import { HttpHeaders } from '../http.interface';
export enum ContentType {
@@ -36,12 +39,26 @@ export enum ContentType {
MULTIPART_FORM_DATA = 'multipart/form-data',
}
-export const getContentType = (headers: HttpHeaders): string => {
- const contentType = headers['content-type'] || headers['Content-Type'];
- return contentType
- ? Array.isArray(contentType) ? contentType[0] : String(contentType)
- : '';
-};
+export const getContentType = (headers: HttpHeaders): O.Option<string> =>
+ pipe(
+ O.fromNullable(headers['content-type'] ?? headers['Content-Type']),
+ O.chain(value => Array.isArray(value)
+ ? A.head(value)
+ : O.some(String(value))),
+ );
-export const isJsonContentType = (headerValue: string) =>
+export const getContentTypeUnsafe = (headers: HttpHeaders): string =>
+ pipe(
+ getContentType(headers),
+ O.getOrElse(constant('')),
+ );
+
+export const getContentTypeEncoding = (headers: HttpHeaders): O.Option<string> =>
+ pipe(
+ getContentType(headers),
+ O.chain(cType => O.fromNullable(/charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(cType))),
+ O.chain(res => O.fromNullable(res[1] || null)),
+ );
+
+export const isJsonContentType = (headerValue: string): boolean =>
headerValue.includes('json');

MODIFY
packages/http/src/response/http.responseBody.factory.ts
packages/http/src/response/http.responseBody.factory.ts
@@ -2,13 +2,13 @@ import { Stream } from 'stream';
import { pipe } from 'fp-ts/lib/function';
import { bufferFrom, isStream, stringifyJson } from '@marblejs/core/dist/+internal/utils';
import { HttpHeaders } from '../http.interface';
-import { ContentType, getContentType, isJsonContentType } from '../+internal/contentType.util';
+import { ContentType, getContentTypeUnsafe, isJsonContentType } from '../+internal/contentType.util';
import { transformUrlEncoded } from '../+internal/urlEncoded.util';
export type ResponseBodyFactory = (headers: HttpHeaders) => (body: any) => string | Stream | Buffer;
export const bodyFactory: ResponseBodyFactory = headers => body => {
- const contentType = getContentType(headers);
+ const contentType = getContentTypeUnsafe(headers);
if (isStream(body))
return body;

MODIFY
packages/middleware-body/src/body.util.ts
packages/middleware-body/src/body.util.ts
@@ -3,14 +3,14 @@ import { pipe } from 'fp-ts/lib/function';
import { Observable } from 'rxjs';
import { map, toArray } from 'rxjs/operators';
import { HttpRequest } from '@marblejs/http';
-import { getContentType } from '@marblejs/http/dist/+internal/contentType.util';
+import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
import { fromReadableStream } from '@marblejs/core/dist/+internal/observable';
export const matchType = (type: string[]) => (req: HttpRequest): boolean =>
- !!typeIs.is(getContentType(req.headers), type);
+ !!typeIs.is(getContentTypeUnsafe(req.headers), type);
export const isMultipart = (req: HttpRequest): boolean =>
- getContentType(req.headers).includes('multipart/');
+ getContentTypeUnsafe(req.headers).includes('multipart/');
export const hasBody = (req: HttpRequest): boolean =>
req.body !== undefined && req.body !== null;

MODIFY
packages/middleware-body/src/parsers/default.body.parser.ts
packages/middleware-body/src/parsers/default.body.parser.ts
import * as typeis from 'type-is';
-import { getContentType } from '@marblejs/http/dist/+internal/contentType.util';
+import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
import { RequestBodyParser } from '../body.model';
import { jsonParser } from './json.body.parser';
import { textParser } from './text.body.parser';
@@ -11,7 +11,7 @@ const SUPPORTED_CONTENT_TYPES = ['json', 'urlencoded', 'application/octet-stream
export const defaultParser: RequestBodyParser = reqOrContentType => body => {
const contentType = typeof reqOrContentType === 'string'
? reqOrContentType
- : getContentType(reqOrContentType.headers);
+ : getContentTypeUnsafe(reqOrContentType.headers);
switch (typeis.is(contentType, SUPPORTED_CONTENT_TYPES)) {
case 'json':

MODIFY
packages/middleware-body/test/bodyParser.integration.ts
packages/middleware-body/test/bodyParser.integration.ts
import { r, httpListener, combineRoutes } from '@marblejs/http';
-import { ContentType, getContentType } from '@marblejs/http/dist/+internal/contentType.util';
+import { ContentType, getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
import { map } from 'rxjs/operators';
import { bodyParser$, urlEncodedParser, jsonParser, textParser, rawParser } from '../src';
@@ -8,7 +8,7 @@ const effect$ = r.pipe(
r.matchType('POST'),
r.useEffect(req$ => req$.pipe(
map(req => {
- const incomingContentType = getContentType(req.headers) as ContentType;
+ const incomingContentType = getContentTypeUnsafe(req.headers) as ContentType;
const outgoingContentType = [ContentType.APPLICATION_OCTET_STREAM, ContentType.TEXT_PLAIN].includes(incomingContentType)
? incomingContentType
: ContentType.APPLICATION_JSON;

MODIFY
packages/middleware-multipart/src/multipart.util.ts
packages/middleware-multipart/src/multipart.util.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { of } from 'rxjs';
import { HttpRequest } from '@marblejs/http';
-import { getContentType } from '@marblejs/http/dist/+internal/contentType.util';
+import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
import { FileIncomingData, StreamHandler } from './multipart.interface';
type ComputedFileData = Partial<{ buffer: Buffer; destination: any; size: number }>;
@@ -11,7 +11,7 @@ const isProperMethod = (req: HttpRequest): boolean =>
['POST', 'PUT'].includes(req.method);
const isMultipart = (req: HttpRequest): boolean =>
- getContentType(req.headers).includes('multipart/');
+ getContentTypeUnsafe(req.headers).includes('multipart/');
export const shouldParseFieldname = (files: string[] | undefined) => (fieldname: string) =>
!!files ? files.includes(fieldname) : true;

MODIFY
packages/testing/src/testBed/http/http.testBed.response.ts
packages/testing/src/testBed/http/http.testBed.response.ts
@@ -2,7 +2,7 @@ import * as O from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/function';
import { HttpStatus } from '@marblejs/http';
import { parseJson } from '@marblejs/core/dist/+internal/utils';
-import { getContentType, ContentType } from '@marblejs/http/dist/+internal/contentType.util';
+import { getContentTypeUnsafe, ContentType } from '@marblejs/http/dist/+internal/contentType.util';
import { HttpTestBedResponseProps, HttpTestBedResponse } from './http.testBed.response.interface';
import { HttpTestBedRequest } from './http.testBed.request.interface';
@@ -10,7 +10,7 @@ const parseResponseBody = (props: HttpTestBedResponseProps): string | Array<any>
pipe(
O.fromNullable(props.body),
O.map(body => {
- switch (getContentType(props.headers)) {
+ switch (getContentTypeUnsafe(props.headers)) {
case ContentType.APPLICATION_JSON:
return pipe(body.toString(), parseJson);
case ContentType.TEXT_PLAIN:

feat(http): added #getContentTypeEncoding utility function