Skip to content

Commit

Permalink
feat(handler): added new handler for no custome media types
Browse files Browse the repository at this point in the history
  • Loading branch information
robertLichtnow committed Jan 29, 2020
1 parent 3c58380 commit 3bb48ad
Show file tree
Hide file tree
Showing 16 changed files with 8,778 additions and 14 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ const swaggerFile = path.join('path', 'to', 'swagger', 'file.yml');
* Optional rules object for disabling/enabling rules
*/
const options = {
"must-contain-port": true;
"must-contain-server-url": true;
"no-singular-resource": true;
"must-contain-version": true;
"must-contain-domain-and-context": true;
"must-contain-port": true,
"must-contain-server-url": true,
"no-singular-resource": true,
"must-contain-version": true,
"must-contain-domain-and-context": true,
"resource-spinal-case": true,
"no-custom-media-type": true
};

const promise = validate(swaggerFile, options);
Expand Down Expand Up @@ -82,5 +84,9 @@ These are the rules checked by the linter
* Checks for resources not using spinal case
*/
"resource-spinal-case"?: boolean;
/**
* Checks for main media types defined in RFC 6838, defaults to `true`
*/
"no-custom-media-type"?: boolean;
}
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/url-parse": "^1.4.3",
"lint": "^0.7.0",
"marked": "^0.7.0",
"mime-types": "^2.1.26",
"npm": "^6.13.6",
"openapi-types": "^1.3.5",
"pluralize": "^8.0.0",
Expand All @@ -48,6 +49,7 @@
},
"devDependencies": {
"@types/jest": "^24.9.0",
"@types/mime-types": "^2.1.0",
"@types/pluralize": "0.0.29",
"@types/yargs": "^15.0.1",
"commitizen": "^4.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/data/openapi-3.0/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ paths:
responses:
'200':
content:
application/json; charset=utf-8:
application/pdf:
examples:
'0':
value: '{"country":"","region":"","city":"","latitude":"","longitude":""}'
Expand Down
59 changes: 59 additions & 0 deletions src/__tests__/data/with-custom-media-type/swagger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
openapi: 3.0.1
servers:
- url: 'https://api.geodatasource.com'
info:
contact:
x-twitter: _geodatasource
description: 'GeoDataSourceâ„¢ Web Service is a REST API enable user to lookup for a city by using latitude and longitude coordinate. It will return the result in either JSON or XML containing the information of country, region, city, latitude and longitude. Visit https://www.geodatasource.com/web-service for further information.'
title: GeoDataSource Location Search
version: '1.0'
x-apisguru-categories:
- location
x-logo:
url: 'https://api.apis.guru/v2/cache/logo/https_twitter.com__geodatasource_profile_image.png'
x-origin:
- converter:
url: 'https://github.com/lucybot/api-spec-converter'
version: 2.7.31
format: openapi
url: 'https://app.swaggerhub.com/apiproxy/schema/file/geodatasource/geodatasource-location-search/1.0/swagger.yaml'
version: '3.0'
x-preferred: true
x-providerName: geodatasource.com
paths:
/city:
get:
description: Get City name by using latitude and longitude
parameters:
- in: query
name: key
required: true
schema:
type: string
- in: query
name: lng
required: true
schema:
type: number
- in: query
name: lat
required: true
schema:
type: number
- in: query
name: format
schema:
enum:
- json
- xml
type: string
responses:
'200':
content:
custom-media-type:
examples:
'0':
value: '{"country":"","region":"","city":"","latitude":"","longitude":""}'
schema:
type: string
description: Get response from longitude latitude lookup
59 changes: 59 additions & 0 deletions src/__tests__/data/with-vendor-media-type/swagger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
openapi: 3.0.1
servers:
- url: 'https://api.geodatasource.com'
info:
contact:
x-twitter: _geodatasource
description: 'GeoDataSourceâ„¢ Web Service is a REST API enable user to lookup for a city by using latitude and longitude coordinate. It will return the result in either JSON or XML containing the information of country, region, city, latitude and longitude. Visit https://www.geodatasource.com/web-service for further information.'
title: GeoDataSource Location Search
version: '1.0'
x-apisguru-categories:
- location
x-logo:
url: 'https://api.apis.guru/v2/cache/logo/https_twitter.com__geodatasource_profile_image.png'
x-origin:
- converter:
url: 'https://github.com/lucybot/api-spec-converter'
version: 2.7.31
format: openapi
url: 'https://app.swaggerhub.com/apiproxy/schema/file/geodatasource/geodatasource-location-search/1.0/swagger.yaml'
version: '3.0'
x-preferred: true
x-providerName: geodatasource.com
paths:
/city:
get:
description: Get City name by using latitude and longitude
parameters:
- in: query
name: key
required: true
schema:
type: string
- in: query
name: lng
required: true
schema:
type: number
- in: query
name: lat
required: true
schema:
type: number
- in: query
name: format
schema:
enum:
- json
- xml
type: string
responses:
'200':
content:
application/vnd.teste.123:
examples:
'0':
value: '{"country":"","region":"","city":"","latitude":"","longitude":""}'
schema:
type: string
description: Get response from longitude latitude lookup
45 changes: 45 additions & 0 deletions src/__tests__/rules/handlers/no-custom-media-type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import path from 'path';
import { parse } from '../../../index';
import { noCustomMediaType } from '../../../rules/handlers';
import { RuleFault, Severity } from '../../../rules/rule-fault';

describe('noCustomMediaType function', () => {
const apiFileWithoutErrors = path.join(__dirname, '..', '..', 'data', 'openapi-3.0', 'swagger.yml');
const apiFileWithCustomMediaType = path.join(__dirname, '..', '..', 'data', 'with-custom-media-type', 'swagger.yml');
const apiFileWithVendorMediaType = path.join(__dirname, '..', '..', 'data', 'with-vendor-media-type', 'swagger.yml');

test('should have no faults', async () => {
const api = await parse(apiFileWithoutErrors);

const faults: RuleFault[] = [];

noCustomMediaType(api, faults);

expect(faults.length).toBe(0);
});

test('should have one fault with error', async () => {
const api = await parse(apiFileWithCustomMediaType);

const faults: RuleFault[] = [];

noCustomMediaType(api, faults);

expect(faults.length).toBe(1);
expect(faults[0].errors.length).toBe(1);
expect(faults[0].errors[0].severity).toBe(Severity.error);
});

test('should have one fault with warning', async () => {
const api = await parse(apiFileWithVendorMediaType);

const faults: RuleFault[] = [];

noCustomMediaType(api, faults);

expect(faults.length).toBe(1);
expect(faults[0].errors.length).toBe(1);
expect(faults[0].errors[0].severity).toBe(Severity.warning);
});

});
6 changes: 4 additions & 2 deletions src/__tests__/validation/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ describe('validate function', () => {
"must-contain-version": true,
"no-singular-resource": true,
"must-contain-server-url": true,
"resource-spinal-case": true
"resource-spinal-case": true,
"no-custom-media-type": true
};

it('should use use the provided rules', async () => {
Expand All @@ -24,7 +25,8 @@ describe('validate function', () => {
"must-contain-version": false,
"no-singular-resource": false,
"must-contain-server-url": false,
"resource-spinal-case": false
"resource-spinal-case": false,
"no-custom-media-type": false
};

/**
Expand Down
3 changes: 3 additions & 0 deletions src/declaration.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'media-typer' {
function test(mediaType: string): boolean;
}
4 changes: 3 additions & 1 deletion src/rules/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { noSingularResource } from './no-singular-resource';
import { mustContainVersion } from './must-contain-version';
import { mustContainDomainAndContext } from './must-contain-domain-and-context';
import { resourceSpinalCase } from './resource-spinal-case';
import { noCustomMediaType } from './no-custom-media-type';

export {
mustContainServerURL,
mustContainPort,
noSingularResource,
mustContainVersion,
mustContainDomainAndContext,
resourceSpinalCase
resourceSpinalCase,
noCustomMediaType
};
66 changes: 66 additions & 0 deletions src/rules/handlers/no-custom-media-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { OpenAPI } from "openapi-types";
import mime from 'mime-types';
import { RuleFault, Severity, RuleFaultContent } from "../rule-fault";
import { produceRuleFaultForPathMethod, pushFault } from "./util";

const faults = {
nonStandardMediaType: `This content media-type doesn't follow RFC 6838 standard media-types`,
vndMediaType: `This content media-type uses custom vendor definitions in RFC 6838 and it should be replaced as a standard one`
};

const produceNonStandardMediaType = (path: string, method: string, httpStatus: string): RuleFault => {
return {
value: produceRuleFaultForPathMethod(path, method, httpStatus),
errors: [
{
severity: Severity.error,
message: faults.nonStandardMediaType
} as RuleFaultContent
]
};
};

const produceVendorMediaType = (path: string, method: string, httpStatus: string): RuleFault => {
return {
value: produceRuleFaultForPathMethod(path, method, httpStatus),
errors: [
{
severity: Severity.warning,
message: faults.vndMediaType
} as RuleFaultContent
]
};
};

const isStandardMediaType = (mediaType: string) => {
return !!mime.extension(mediaType);
};

const containsVendorInMediaType = (mediaType: string) => {
const vendorPattern = /^\w+\/vnd(\.\w+)+$/;
return vendorPattern.test(mediaType);
};

export const noCustomMediaType = (api: OpenAPI.Document, ruleFaults: RuleFault[]) => {
const apiParsed: any = api;

Object.entries(apiParsed.paths).forEach(([path, pathValue]) => {
Object.entries(pathValue as any).forEach(([method, methodValue]) => {
const responses = Object.keys((methodValue as any).responses);

responses.forEach(response => {
const mediaTypes = Object.keys((methodValue as any).responses[response].content);

mediaTypes.forEach(mediaType => {
const mainMediaType = mediaType.split(';')[0];

if(containsVendorInMediaType(mainMediaType)) {
pushFault(produceVendorMediaType(path, method, response), ruleFaults);
} else if(!isStandardMediaType(mainMediaType)) {
pushFault(produceNonStandardMediaType(path, method, response), ruleFaults);
}
});
});
});
});
};
8 changes: 8 additions & 0 deletions src/rules/handlers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const produceRuleFaultValueForUrl = (url: string) => `Server url: ${url}`
*/
export const produceRuleFaultForPath = (path: string) => `Path: ${path}`;

/**
* Outputs a uniform string for path method errors
* @param path The path to use on the output
* @param method The method to use on the output
* @param httpStatus The http status code to use on the output
*/
export const produceRuleFaultForPathMethod = (path: string, method: string, httpStatus: string) => `Path: ${path} - Method: ${method} - HTTPStatus: ${httpStatus}`;

/**
* Pushes the `fault` into the `ruleFaults` array without overlapping values
* @param fault The RuleFault to push
Expand Down
6 changes: 4 additions & 2 deletions src/rules/rule-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
noSingularResource,
mustContainVersion,
mustContainDomainAndContext,
resourceSpinalCase
resourceSpinalCase,
noCustomMediaType
} from './handlers';

/**
Expand All @@ -26,5 +27,6 @@ export const Handlers: RuleHandlers = {
"must-contain-port": mustContainPort,
"must-contain-version": mustContainVersion,
"no-singular-resource": noSingularResource,
"resource-spinal-case": resourceSpinalCase
"resource-spinal-case": resourceSpinalCase,
"no-custom-media-type": noCustomMediaType
};
6 changes: 5 additions & 1 deletion src/rules/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export interface Rules {
*/
"must-contain-domain-and-context"?: boolean;
/**
* Checks for resources not using spinal case
* Checks for resources not using spinal case, defaults to `true`
*/
"resource-spinal-case"?: boolean;
/**
* Checks for main media types defined in RFC 6838, defaults to `true`
*/
"no-custom-media-type"?: boolean;
}
3 changes: 2 additions & 1 deletion src/validation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export const DefaultRules: DefaultRules = {
"must-contain-server-url": true,
"must-contain-version": true,
"no-singular-resource": true,
"resource-spinal-case": true
"resource-spinal-case": true,
"no-custom-media-type": true
};

/**
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"files": [
"src/index.ts",
"src/cli.ts"
"src/cli.ts",
"src/declaration.d.ts"
]
}
Loading

0 comments on commit 3bb48ad

Please sign in to comment.