From 59b48d750ba9496fa9f43068de05b9ba6ff80e51 Mon Sep 17 00:00:00 2001 From: Matheus Robert Lichtnow Date: Tue, 28 Jan 2020 11:45:45 -0300 Subject: [PATCH] feat(handler): added new rule and its handler for resources in paths not using spinal case --- README.md | 4 + package-lock.json | 5 + package.json | 1 + .../data/with-spinal-case/swagger.yml | 59 ++++++++++++ .../swagger.yml | 95 +++++++++++++++++++ .../swagger.yml | 59 ++++++++++++ .../data/without-spinal-case/swagger.yml | 59 ++++++++++++ .../handlers/resource-spinal-case.spec.ts | 67 +++++++++++++ src/__tests__/validation/validate.spec.ts | 6 +- src/rules/handlers/index.ts | 4 +- src/rules/handlers/resource-spinal-case.ts | 59 ++++++++++++ src/rules/rule-handlers.ts | 6 +- src/rules/rules.ts | 4 + src/validation/validate.ts | 3 +- 14 files changed, 425 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/data/with-spinal-case/swagger.yml create mode 100644 src/__tests__/data/without-spinal-case-once-two-path/swagger.yml create mode 100644 src/__tests__/data/without-spinal-case-twice-one-path/swagger.yml create mode 100644 src/__tests__/data/without-spinal-case/swagger.yml create mode 100644 src/__tests__/rules/handlers/resource-spinal-case.spec.ts create mode 100644 src/rules/handlers/resource-spinal-case.ts diff --git a/README.md b/README.md index 2a060c0..92a01fa 100644 --- a/README.md +++ b/README.md @@ -78,5 +78,9 @@ These are the rules checked by the linter * Checks for missing `/domain/context` on server url, defaults to `true` */ "must-contain-domain-and-context": boolean; + /** + * Checks for resources not using spinal case + */ + "resource-spinal-case"?: boolean; } ``` diff --git a/package-lock.json b/package-lock.json index 618cfbc..fc5f306 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11711,6 +11711,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slugify": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz", + "integrity": "sha512-wA9XS475ZmGNlEnYYLPReSfuz/c3VQsEMoU43mi6OnKMCdbnFXd4/Yg7J0lBv8jkPolacMpOrWEaoYxuE1+hoQ==" + }, "smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", diff --git a/package.json b/package.json index 9d40306..17889e5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "npm": "^6.13.6", "openapi-types": "^1.3.5", "pluralize": "^8.0.0", + "slugify": "^1.3.6", "swagger-parser": "^8.0.4", "typescript": "^3.7.4", "url-parse": "^1.4.7", diff --git a/src/__tests__/data/with-spinal-case/swagger.yml b/src/__tests__/data/with-spinal-case/swagger.yml new file mode 100644 index 0000000..757a568 --- /dev/null +++ b/src/__tests__/data/with-spinal-case/swagger.yml @@ -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-with-spinal-case: + 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/json; charset=utf-8: + examples: + '0': + value: '{"country":"","region":"","city":"","latitude":"","longitude":""}' + schema: + type: string + description: Get response from longitude latitude lookup diff --git a/src/__tests__/data/without-spinal-case-once-two-path/swagger.yml b/src/__tests__/data/without-spinal-case-once-two-path/swagger.yml new file mode 100644 index 0000000..8c63ba5 --- /dev/null +++ b/src/__tests__/data/without-spinal-case-once-two-path/swagger.yml @@ -0,0 +1,95 @@ +openapi: 3.0.1 +servers: + - url: "https://api.geodatasource.com/v1/abc/123/test" +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: + /pathWithoutSpinalCase: + 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/json; charset=utf-8: + examples: + "0": + value: '{"country":"","region":"","city":"","latitude":"","longitude":""}' + schema: + type: string + description: Get response from longitude latitude lookup + /anotherPathWithoutSpinalCase: + 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/json; charset=utf-8: + examples: + "0": + value: '{"country":"","region":"","city":"","latitude":"","longitude":""}' + schema: + type: string + description: Get response from longitude latitude lookup diff --git a/src/__tests__/data/without-spinal-case-twice-one-path/swagger.yml b/src/__tests__/data/without-spinal-case-twice-one-path/swagger.yml new file mode 100644 index 0000000..913af2b --- /dev/null +++ b/src/__tests__/data/without-spinal-case-twice-one-path/swagger.yml @@ -0,0 +1,59 @@ +openapi: 3.0.1 +servers: + - url: 'https://api.geodatasource.com/v1/abc/123/test' +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: + /pathWithoutSpinalCase/anotherPathWithoutSpinalCase: + 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/json; charset=utf-8: + examples: + '0': + value: '{"country":"","region":"","city":"","latitude":"","longitude":""}' + schema: + type: string + description: Get response from longitude latitude lookup diff --git a/src/__tests__/data/without-spinal-case/swagger.yml b/src/__tests__/data/without-spinal-case/swagger.yml new file mode 100644 index 0000000..64fc301 --- /dev/null +++ b/src/__tests__/data/without-spinal-case/swagger.yml @@ -0,0 +1,59 @@ +openapi: 3.0.1 +servers: + - url: 'https://api.geodatasource.com/v1/abc/123/test' +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: + /pathWithoutSpinalCase: + 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/json; charset=utf-8: + examples: + '0': + value: '{"country":"","region":"","city":"","latitude":"","longitude":""}' + schema: + type: string + description: Get response from longitude latitude lookup diff --git a/src/__tests__/rules/handlers/resource-spinal-case.spec.ts b/src/__tests__/rules/handlers/resource-spinal-case.spec.ts new file mode 100644 index 0000000..f35a0b4 --- /dev/null +++ b/src/__tests__/rules/handlers/resource-spinal-case.spec.ts @@ -0,0 +1,67 @@ +import path from 'path'; +import { parse } from '../../../index'; +import { resourceSpinalCase } from '../../../rules/handlers/resource-spinal-case'; +import { RuleFault } from '../../../rules/rule-fault'; + +describe('resourceSpinalCase function', () => { + const apiWithoutErrors = path.join(__dirname, '..', '..', 'data', 'openapi-3.0', 'swagger.yml'); + const apiWithSpinalCase = path.join(__dirname, '..', '..', 'data', 'with-spinal-case', 'swagger.yml'); + const apiWithOneFaultAndOneError = path.join(__dirname, '..', '..', 'data', 'without-spinal-case', 'swagger.yml'); + const apiWithOneFaultAndTwoErrors = path.join(__dirname, '..', '..', 'data', 'without-spinal-case-twice-one-path', 'swagger.yml'); + const apiWithTwoFaultsOneErrorEach = path.join(__dirname, '..', '..', 'data', 'without-spinal-case-once-two-path', 'swagger.yml'); + + + it('should have no faults', async () => { + const faults: RuleFault[] = []; + + const api = await parse(apiWithoutErrors); + + resourceSpinalCase(api, faults); + + expect(faults.length).toBe(0); + }); + + it('should have no faults with spinal case', async () => { + const faults: RuleFault[] = []; + + const api = await parse(apiWithSpinalCase); + + resourceSpinalCase(api, faults); + + expect(faults.length).toBe(0); + }); + + it('should have one fault with one error', async () => { + const faults: RuleFault[] = []; + + const api = await parse(apiWithOneFaultAndOneError); + + resourceSpinalCase(api, faults); + + expect(faults.length).toBe(1); + expect(faults[0].errors.length).toBe(1); + }); + + it('should have one fault with two errors', async () => { + const faults: RuleFault[] = []; + + const api = await parse(apiWithOneFaultAndTwoErrors); + + resourceSpinalCase(api, faults); + + expect(faults.length).toBe(1); + expect(faults[0].errors.length).toBe(2); + }); + + it('should have two faults with one error each', async () => { + const faults: RuleFault[] = []; + + const api = await parse(apiWithTwoFaultsOneErrorEach); + + resourceSpinalCase(api, faults); + + expect(faults.length).toBe(2); + expect(faults[0].errors.length).toBe(1); + expect(faults[1].errors.length).toBe(1); + }); +}); diff --git a/src/__tests__/validation/validate.spec.ts b/src/__tests__/validation/validate.spec.ts index 1324d1a..b8222f6 100644 --- a/src/__tests__/validation/validate.spec.ts +++ b/src/__tests__/validation/validate.spec.ts @@ -13,7 +13,8 @@ describe('validate function', () => { "must-contain-port": true, "must-contain-version": true, "no-singular-resource": true, - "must-contain-server-url": true + "must-contain-server-url": true, + "resource-spinal-case": true }; it('should use use the provided rules', async () => { @@ -22,7 +23,8 @@ describe('validate function', () => { "must-contain-port": false, "must-contain-version": false, "no-singular-resource": false, - "must-contain-server-url": false + "must-contain-server-url": false, + "resource-spinal-case": false }; /** diff --git a/src/rules/handlers/index.ts b/src/rules/handlers/index.ts index 43ee479..69ad7ab 100644 --- a/src/rules/handlers/index.ts +++ b/src/rules/handlers/index.ts @@ -3,11 +3,13 @@ import { mustContainPort } from './must-contain-port'; 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'; export { mustContainServerURL, mustContainPort, noSingularResource, mustContainVersion, - mustContainDomainAndContext + mustContainDomainAndContext, + resourceSpinalCase }; diff --git a/src/rules/handlers/resource-spinal-case.ts b/src/rules/handlers/resource-spinal-case.ts new file mode 100644 index 0000000..cc71e66 --- /dev/null +++ b/src/rules/handlers/resource-spinal-case.ts @@ -0,0 +1,59 @@ +import { OpenAPI } from "openapi-types"; +import slugify from 'slugify'; +import { RuleFault, Severity, RuleFaultContent } from "../rule-fault"; +import { produceRuleFaultForPath, pushFault } from "./util"; + +const faults = { + resourceSpinalCase: 'Resource on path not on spinal-case:' +}; + +const produceResourceSpinalCaseFault = (path: string, resources: string[]): RuleFault => { + return { + value: produceRuleFaultForPath(path), + errors: resources.map(resource => { + return { + severity: Severity.warning, + message: `${faults.resourceSpinalCase} ${resource}` + } as RuleFaultContent; + }), + }; +}; + +export const resourceSpinalCase = (api: OpenAPI.Document, ruleFaults: RuleFault[]) => { + + const apiParsed: any = api; + + const slugifyOptions: any = { + lower: true, + remove: /_/ + }; + + /** + * There is no need for null safe checking on paths, since it is an obrigatory + * field in the OpenAPI Object Specification 3.0 + * https://swagger.io/specification/#oasDocument + */ + Object.entries(apiParsed.paths).forEach(([path, value]) => { + /** + * Gets every value inside the path and removes first empty value, e.g., '/a/b/c' turns intos ['a', 'b', 'c'] + */ + const splitPaths = path.split('/').slice(1); + + const notSpinalCaseResource: string[] = []; + + splitPaths.forEach(splitPath => { + /* istanbul ignore else */ + /** + * Tests for spinal case on the resource + */ + if (slugify(splitPath, slugifyOptions) !== splitPath) { + notSpinalCaseResource.push(splitPath); + } + }); + + /* istanbul ignore else */ + if (notSpinalCaseResource.length) { + pushFault(produceResourceSpinalCaseFault(path, notSpinalCaseResource), ruleFaults); + } + }); +}; diff --git a/src/rules/rule-handlers.ts b/src/rules/rule-handlers.ts index db26b08..2a4f728 100644 --- a/src/rules/rule-handlers.ts +++ b/src/rules/rule-handlers.ts @@ -6,7 +6,8 @@ import { mustContainPort, noSingularResource, mustContainVersion, - mustContainDomainAndContext + mustContainDomainAndContext, + resourceSpinalCase } from './handlers'; /** @@ -24,5 +25,6 @@ export const Handlers: RuleHandlers = { "must-contain-server-url": mustContainServerURL, "must-contain-port": mustContainPort, "must-contain-version": mustContainVersion, - "no-singular-resource": noSingularResource + "no-singular-resource": noSingularResource, + "resource-spinal-case": resourceSpinalCase }; diff --git a/src/rules/rules.ts b/src/rules/rules.ts index ac04ac1..1023431 100644 --- a/src/rules/rules.ts +++ b/src/rules/rules.ts @@ -22,4 +22,8 @@ export interface Rules { * Checks for missing `/domain/context` on server url, defaults to `true` */ "must-contain-domain-and-context"?: boolean; + /** + * Checks for resources not using spinal case + */ + "resource-spinal-case"?: boolean; } diff --git a/src/validation/validate.ts b/src/validation/validate.ts index 19d6886..5622466 100644 --- a/src/validation/validate.ts +++ b/src/validation/validate.ts @@ -11,7 +11,8 @@ export const DefaultRules: DefaultRules = { "must-contain-port": true, "must-contain-server-url": true, "must-contain-version": true, - "no-singular-resource": true + "no-singular-resource": true, + "resource-spinal-case": true }; /**