-
-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(rulesets): use createRulesetFunction for all oas functions (#2491)
- Loading branch information
Showing
11 changed files
with
340 additions
and
297 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
packages/rulesets/src/oas/functions/__tests__/typedEnum.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
52
packages/rulesets/src/oas/functions/oasOpFormDataConsumeCheck.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.