diff --git a/src/includer/normalizer/normalize.ts b/src/includer/normalizer/normalize.ts new file mode 100644 index 0000000..fd72ecc --- /dev/null +++ b/src/includer/normalizer/normalize.ts @@ -0,0 +1,40 @@ +/* eslint-disable camelcase */ +import {OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {ISpecNormalizationStrategies, NormalizedSecurityDefinitions} from './strategies/defs'; +import {OASVersionGeneration, resolveOASVersionGeneration} from './resolveOASGeneration'; +import {oasV2NormalizationStrategies} from './strategies/2.0'; +import {oasV3NormalizationStrategies} from './strategies/3.0'; +import {oasV31NormalizationStrategies} from './strategies/3.1'; + +export type NormalizedSpec = { + securityDefinitions: NormalizedSecurityDefinitions; +}; + +type GenerationToDocumentType = { + [OASVersionGeneration.V2_0]: OpenAPIV2.Document; + [OASVersionGeneration.V3_0]: OpenAPIV3.Document; + [OASVersionGeneration.V3_1]: OpenAPIV3_1.Document; +}; + +type GenerationToStrategyMapping = { + [Gen in OASVersionGeneration]: ISpecNormalizationStrategies; +}; + +const generationToStrategyMap: GenerationToStrategyMapping = { + [OASVersionGeneration.V2_0]: oasV2NormalizationStrategies, + [OASVersionGeneration.V3_0]: oasV3NormalizationStrategies, + [OASVersionGeneration.V3_1]: oasV31NormalizationStrategies, +}; + +// This is a stub for now +// This should probably consist of all the custom schema types defined in `models.ts` +// The idea is to normalize `OpenAPI.Document` so that it provides a consistent +// way of rendering stuff we actually care about, regardless of the spec version +export const normalize = (spec: OpenAPI.Document): NormalizedSpec => { + const specGeneration = resolveOASVersionGeneration(spec); + const strategies = generationToStrategyMap[specGeneration] as ISpecNormalizationStrategies; + + return { + securityDefinitions: strategies.normalizeSecurityDefinitions(spec), + }; +}; diff --git a/src/includer/normalizer/resolveOASGeneration.ts b/src/includer/normalizer/resolveOASGeneration.ts new file mode 100644 index 0000000..3a20859 --- /dev/null +++ b/src/includer/normalizer/resolveOASGeneration.ts @@ -0,0 +1,32 @@ +import {OpenAPI, OpenAPIV2} from 'openapi-types'; + +export enum OASVersionGeneration { + V2_0, + V3_0, + V3_1, +} + +// this is probably a bit verbose, but `swagger-parser` we use has a similar bit of code +// https://github.com/APIDevTools/swagger-parser/blob/1d9776e2445c3dfc62cf2cd63a33f3449e5ed9fa/lib/index.js#L11 +const versionMap = { + '2.0': OASVersionGeneration.V2_0, + '3.0.0': OASVersionGeneration.V3_0, + '3.0.1': OASVersionGeneration.V3_0, + '3.0.2': OASVersionGeneration.V3_0, + '3.0.3': OASVersionGeneration.V3_0, + '3.1.0': OASVersionGeneration.V3_1, +} satisfies Record; + +const isLegacySwaggerSpec = (spec: OpenAPI.Document): spec is OpenAPIV2.Document => + Object.prototype.hasOwnProperty.call(spec, 'swagger'); + +export const resolveOASVersionGeneration = (spec: OpenAPI.Document): OASVersionGeneration => { + const resolvedVersion = isLegacySwaggerSpec(spec) ? spec.swagger : spec.openapi; + + if (resolvedVersion in versionMap) { + return versionMap[resolvedVersion as keyof typeof versionMap]; + } + + // technically, this throw is pointless, since `swagger-parser` already should have this checked beforehand + throw new TypeError(`Unsupported spec version: ${resolvedVersion}`); +}; diff --git a/src/includer/normalizer/shared/isOASV3XReferenceObject.ts b/src/includer/normalizer/shared/isOASV3XReferenceObject.ts new file mode 100644 index 0000000..175a836 --- /dev/null +++ b/src/includer/normalizer/shared/isOASV3XReferenceObject.ts @@ -0,0 +1,9 @@ +/* eslint-disable camelcase */ +import {OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; + +type OASV3XReferenceObject = OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject; + +export const isOASV3XReferenceObject = ( + maybeReferenceObject: object, +): maybeReferenceObject is OASV3XReferenceObject => + Object.prototype.hasOwnProperty.call(maybeReferenceObject, '$ref'); diff --git a/src/includer/normalizer/shared/normalizeOASV3XSecurityDefinitions.ts b/src/includer/normalizer/shared/normalizeOASV3XSecurityDefinitions.ts new file mode 100644 index 0000000..cb57164 --- /dev/null +++ b/src/includer/normalizer/shared/normalizeOASV3XSecurityDefinitions.ts @@ -0,0 +1,29 @@ +/* eslint-disable camelcase */ +import {OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {isOASV3XReferenceObject} from './isOASV3XReferenceObject'; +import {Security} from '../../models'; + +type OASV3XSpec = OpenAPIV3.Document | OpenAPIV3_1.Document; +type OASV3XSecurityScheme = OpenAPIV3.SecuritySchemeObject | OpenAPIV3_1.SecuritySchemeObject; + +const definitionIsSecurityScheme = ( + objectEntry: [string, object], +): objectEntry is [string, OASV3XSecurityScheme] => { + const [, maybeScheme] = objectEntry; + + return !isOASV3XReferenceObject(maybeScheme); +}; + +export const normalizeOASV3XSecurityDefinitions = (spec: OASV3XSpec) => + Object.fromEntries( + Object.entries(spec.components?.securitySchemes ?? {}) + .filter(definitionIsSecurityScheme) + .map(([schemeName, {type, description}]) => { + const normalizedScheme: Security = { + type, + description: description ?? '', + }; + + return [schemeName, normalizedScheme]; + }), + ); diff --git a/src/includer/normalizer/strategies/2.0/index.ts b/src/includer/normalizer/strategies/2.0/index.ts new file mode 100644 index 0000000..e8c1500 --- /dev/null +++ b/src/includer/normalizer/strategies/2.0/index.ts @@ -0,0 +1,19 @@ +import {OpenAPIV2} from 'openapi-types'; +import {ISpecNormalizationStrategies} from '../defs'; +import {Security} from '../../../models'; + +export const oasV2NormalizationStrategies: ISpecNormalizationStrategies = { + normalizeSecurityDefinitions: (spec) => + Object.fromEntries( + Object.entries(spec.securityDefinitions ?? {}).map( + ([schemeName, {type, description}]) => { + const normalizedScheme: Security = { + type, + description: description ?? '', + }; + + return [schemeName, normalizedScheme]; + }, + ), + ), +}; diff --git a/src/includer/normalizer/strategies/3.0/index.ts b/src/includer/normalizer/strategies/3.0/index.ts new file mode 100644 index 0000000..4445dfb --- /dev/null +++ b/src/includer/normalizer/strategies/3.0/index.ts @@ -0,0 +1,7 @@ +import {OpenAPIV3} from 'openapi-types'; +import {ISpecNormalizationStrategies} from '../defs'; +import {normalizeOASV3XSecurityDefinitions} from '../../shared/normalizeOASV3XSecurityDefinitions'; + +export const oasV3NormalizationStrategies: ISpecNormalizationStrategies = { + normalizeSecurityDefinitions: normalizeOASV3XSecurityDefinitions, +}; diff --git a/src/includer/normalizer/strategies/3.1/index.ts b/src/includer/normalizer/strategies/3.1/index.ts new file mode 100644 index 0000000..655d013 --- /dev/null +++ b/src/includer/normalizer/strategies/3.1/index.ts @@ -0,0 +1,8 @@ +/* eslint-disable camelcase */ +import {OpenAPIV3_1} from 'openapi-types'; +import {ISpecNormalizationStrategies} from '../defs'; +import {normalizeOASV3XSecurityDefinitions} from '../../shared/normalizeOASV3XSecurityDefinitions'; + +export const oasV31NormalizationStrategies: ISpecNormalizationStrategies = { + normalizeSecurityDefinitions: normalizeOASV3XSecurityDefinitions, +}; diff --git a/src/includer/normalizer/strategies/defs.ts b/src/includer/normalizer/strategies/defs.ts new file mode 100644 index 0000000..aad1215 --- /dev/null +++ b/src/includer/normalizer/strategies/defs.ts @@ -0,0 +1,9 @@ +import {OpenAPI} from 'openapi-types'; +import {Security} from '../../models'; + +export type NormalizedSecurityDefinitions = Record; + +export type ISpecNormalizationStrategies = + { + normalizeSecurityDefinitions: (spec: ConcreteSpec) => NormalizedSecurityDefinitions; + }; diff --git a/src/includer/parsers.ts b/src/includer/parsers.ts index 5a972e1..a280bfd 100644 --- a/src/includer/parsers.ts +++ b/src/includer/parsers.ts @@ -17,6 +17,8 @@ import { Specification, Tag, } from './models'; +import {normalize} from './normalizer/normalize'; +import {OpenAPI} from 'openapi-types'; function info(spec: OpenAPISpec): Info { const { @@ -115,8 +117,12 @@ function tagsFromSpec(spec: OpenAPISpec): Map { const opid = (path: string, method: string, id?: string) => slugify(id ?? [path, method].join('-')); function pathsFromSpec(spec: OpenAPISpec, tagsByID: Map): Specification { + // This conversion (OAPI.Doc -> OAS (with `any`) -> OAPI.Doc) is crude, but this whole file consists of crude code, so... + // (someday this will be a part of normalization routine I guess) + const prenormalizedSpec = normalize(spec as OpenAPI.Document); + const endpoints: Endpoints = []; - const {paths, servers, components = {}, security: globalSecurity = []} = spec; + const {paths, servers, security: globalSecurity = []} = spec; const visiter = ({path, method, endpoint}: VisiterParams) => { const { summary, @@ -129,16 +135,9 @@ function pathsFromSpec(spec: OpenAPISpec, tagsByID: Map): Specifica security = [], } = endpoint; - const parsedSecurity = [...security, ...globalSecurity].reduce((arr, item) => { - arr.push( - ...Object.keys(item).reduce((acc, key) => { - // @ts-ignore - acc.push(components.securitySchemes[key]); - return acc; - }, []), - ); - return arr; - }, []); + const parsedSecurity = [...security, ...globalSecurity].flatMap((item) => + Object.keys(item).map((key) => prenormalizedSpec.securityDefinitions[key]), + ); const parsedServers = (endpoint.servers || servers || [{url: '/'}]).map( (server: Server) => {