Skip to content

Commit

Permalink
chore(rulesets): use createRulesetFunction for all oas functions (#2491)
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip authored Sep 13, 2023
1 parent a64ca92 commit 3cbf047
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 297 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { typedEnum } from '../typedEnum';
import typedEnum from '../typedEnum';
import { Document } from '@stoplight/spectral-core';
import * as Parsers from '@stoplight/spectral-parsers';

Expand Down
79 changes: 46 additions & 33 deletions packages/rulesets/src/oas/functions/oasDiscriminator.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core';
import { isObject } from './utils/isObject';

export const oasDiscriminator: IFunction = (schema, _opts, { path }) => {
/**
* This function verifies:
*
* 1. The discriminator property name is defined at this schema.
* 2. The discriminator property is in the required property list.
*/

if (!isObject(schema)) return;

if (typeof schema.discriminator !== 'string') return;

const discriminatorName = schema.discriminator;

const results: IFunctionResult[] = [];

if (!isObject(schema.properties) || !Object.keys(schema.properties).some(k => k === discriminatorName)) {
results.push({
message: `The discriminator property must be defined in this schema.`,
path: [...path, 'properties'],
});
}

if (!Array.isArray(schema.required) || !schema.required.some(n => n === discriminatorName)) {
results.push({
message: `The discriminator property must be in the required property list.`,
path: [...path, 'required'],
});
}

return results;
type Input = {
discriminator: string;
[key: string]: unknown;
};

export default oasDiscriminator;
export default createRulesetFunction<Input, null>(
{
input: {
type: 'object',
properties: {
discriminator: {
type: 'string',
},
},
required: ['discriminator'],
},
options: null,
},
function oasDiscriminator(schema, _opts, { path }) {
/**
* This function verifies:
*
* 1. The discriminator property name is defined at this schema.
* 2. The discriminator property is in the required property list.
*/

const discriminatorName = schema.discriminator;

const results: IFunctionResult[] = [];

if (!isObject(schema.properties) || !Object.keys(schema.properties).some(k => k === discriminatorName)) {
results.push({
message: `The discriminator property must be defined in this schema.`,
path: [...path, 'properties'],
});
}

if (!Array.isArray(schema.required) || !schema.required.some(n => n === discriminatorName)) {
results.push({
message: `The discriminator property must be in the required property list.`,
path: [...path, 'required'],
});
}

return results;
},
);
52 changes: 32 additions & 20 deletions packages/rulesets/src/oas/functions/oasOpFormDataConsumeCheck.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import type { IFunction } from '@stoplight/spectral-core';
import { createRulesetFunction } from '@stoplight/spectral-core';
import { isObject } from './utils/isObject';

const validConsumeValue = /(application\/x-www-form-urlencoded|multipart\/form-data)/;

export const oasOpFormDataConsumeCheck: IFunction = targetVal => {
if (!isObject(targetVal)) return;

const parameters: unknown = targetVal.parameters;
const consumes: unknown = targetVal.consumes;

if (!Array.isArray(parameters) || !Array.isArray(consumes)) {
return;
}
type Input = {
consumes: unknown[];
parameters: unknown[];
};

if (parameters.some(p => isObject(p) && p.in === 'formData') && !validConsumeValue.test(consumes?.join(','))) {
return [
{
message: 'Consumes must include urlencoded, multipart, or form-data media type when using formData parameter.',
export default createRulesetFunction<Input, null>(
{
input: {
type: 'object',
properties: {
consumes: {
type: 'array',
},
parameters: {
type: 'array',
},
},
];
}
required: ['consumes', 'parameters'],
},
options: null,
},
function oasOpFormDataConsumeCheck({ parameters, consumes }) {
if (parameters.some(p => isObject(p) && p.in === 'formData') && !validConsumeValue.test(consumes?.join(','))) {
return [
{
message:
'Consumes must include urlencoded, multipart, or form-data media type when using formData parameter.',
},
];
}

return;
};

export default oasOpFormDataConsumeCheck;
return;
},
);
59 changes: 31 additions & 28 deletions packages/rulesets/src/oas/functions/oasOpIdUnique.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import type { IFunctionResult } from '@stoplight/spectral-core';
import { createRulesetFunction } from '@stoplight/spectral-core';
import { getAllOperations } from './utils/getAllOperations';
import { isObject } from './utils/isObject';

export const oasOpIdUnique: IFunction = targetVal => {
if (!isObject(targetVal) || !isObject(targetVal.paths)) return;
export default createRulesetFunction<Record<string, unknown>, null>(
{
input: {
type: 'object',
},
options: null,
},
function oasOpIdUnique(paths) {
const results: IFunctionResult[] = [];

const results: IFunctionResult[] = [];
const seenIds: unknown[] = [];

const { paths } = targetVal;
for (const { path, operation } of getAllOperations(paths)) {
const pathValue = paths[path];

const seenIds: unknown[] = [];
if (!isObject(pathValue)) continue;

for (const { path, operation } of getAllOperations(paths)) {
const pathValue = paths[path];
const operationValue = pathValue[operation];

if (!isObject(pathValue)) continue;
if (!isObject(operationValue) || !('operationId' in operationValue)) {
continue;
}

const operationValue = pathValue[operation];
const { operationId } = operationValue;

if (!isObject(operationValue) || !('operationId' in operationValue)) {
continue;
if (seenIds.includes(operationId)) {
results.push({
message: 'operationId must be unique.',
path: ['paths', path, operation, 'operationId'],
});
} else {
seenIds.push(operationId);
}
}

const { operationId } = operationValue;

if (seenIds.includes(operationId)) {
results.push({
message: 'operationId must be unique.',
path: ['paths', path, operation, 'operationId'],
});
} else {
seenIds.push(operationId);
}
}

return results;
};

export default oasOpIdUnique;
return results;
},
);
136 changes: 71 additions & 65 deletions packages/rulesets/src/oas/functions/oasOpParams.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,87 @@
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import type { Dictionary } from '@stoplight/types';
import type { IFunctionResult } from '@stoplight/spectral-core';
import { isObject } from './utils/isObject';
import { createRulesetFunction } from '@stoplight/spectral-core';

function computeFingerprint(param: Record<string, unknown>): string {
return `${String(param.in)}-${String(param.name)}`;
}

export const oasOpParams: IFunction = (params, _opts, { path }) => {
/**
* This function verifies:
*
* 1. Operations must have unique `name` + `in` parameters.
* 2. Operation cannot have both `in:body` and `in:formData` parameters
* 3. Operation must have only one `in:body` parameter.
*/

if (!Array.isArray(params)) return;

if (params.length < 2) return;

const results: IFunctionResult[] = [];

const count: Dictionary<number[]> = {
body: [],
formData: [],
};
const list: string[] = [];
const duplicates: number[] = [];

let index = -1;

for (const param of params) {
index++;

if (!isObject(param)) continue;

// skip params that are refs
if ('$ref' in param) continue;

// Operations must have unique `name` + `in` parameters.
const fingerprint = computeFingerprint(param);
if (list.includes(fingerprint)) {
duplicates.push(index);
} else {
list.push(fingerprint);
export default createRulesetFunction<unknown[], null>(
{
input: {
type: 'array',
},
options: null,
},
function oasOpParams(params, _opts, { path }) {
/**
* This function verifies:
*
* 1. Operations must have unique `name` + `in` parameters.
* 2. Operation cannot have both `in:body` and `in:formData` parameters
* 3. Operation must have only one `in:body` parameter.
*/

if (!Array.isArray(params)) return;

if (params.length < 2) return;

const results: IFunctionResult[] = [];

const count: Record<string, number[]> = {
body: [],
formData: [],
};
const list: string[] = [];
const duplicates: number[] = [];

let index = -1;

for (const param of params) {
index++;

if (!isObject(param)) continue;

// skip params that are refs
if ('$ref' in param) continue;

// Operations must have unique `name` + `in` parameters.
const fingerprint = computeFingerprint(param);
if (list.includes(fingerprint)) {
duplicates.push(index);
} else {
list.push(fingerprint);
}

if (typeof param.in === 'string' && param.in in count) {
count[param.in].push(index);
}
}

if (typeof param.in === 'string' && param.in in count) {
count[param.in].push(index);
if (duplicates.length > 0) {
for (const i of duplicates) {
results.push({
message: 'A parameter in this operation already exposes the same combination of "name" and "in" values.',
path: [...path, i],
});
}
}
}

if (duplicates.length > 0) {
for (const i of duplicates) {
if (count.body.length > 0 && count.formData.length > 0) {
results.push({
message: 'A parameter in this operation already exposes the same combination of "name" and "in" values.',
path: [...path, i],
message: 'Operation must not have both "in:body" and "in:formData" parameters.',
});
}
}

if (count.body.length > 0 && count.formData.length > 0) {
results.push({
message: 'Operation must not have both "in:body" and "in:formData" parameters.',
});
}

if (count.body.length > 1) {
for (let i = 1; i < count.body.length; i++) {
results.push({
message: 'Operation must not have more than a single instance of the "in:body" parameter.',
path: [...path, count.body[i]],
});
if (count.body.length > 1) {
for (let i = 1; i < count.body.length; i++) {
results.push({
message: 'Operation must not have more than a single instance of the "in:body" parameter.',
path: [...path, count.body[i]],
});
}
}
}

return results;
};

export default oasOpParams;
return results;
},
);
6 changes: 2 additions & 4 deletions packages/rulesets/src/oas/functions/oasOpSuccessResponse.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createRulesetFunction } from '@stoplight/spectral-core';
import { oas3 } from '@stoplight/spectral-formats';

export const oasOpSuccessResponse = createRulesetFunction<Record<string, unknown>, null>(
export default createRulesetFunction<Record<string, unknown>, null>(
{
input: {
type: 'object',
},
options: null,
},
(input, opts, context) => {
function oasOpSuccessResponse(input, opts, context) {
const isOAS3X = context.document.formats?.has(oas3) === true;

for (const response of Object.keys(input)) {
Expand All @@ -28,5 +28,3 @@ export const oasOpSuccessResponse = createRulesetFunction<Record<string, unknown
];
},
);

export default oasOpSuccessResponse;
Loading

0 comments on commit 3cbf047

Please sign in to comment.