Skip to content

Commit

Permalink
feat: new typed-enum rule
Browse files Browse the repository at this point in the history
  • Loading branch information
nulltoken committed Jan 19, 2020
1 parent d8c3aba commit 1844f2b
Show file tree
Hide file tree
Showing 12 changed files with 421 additions and 2 deletions.
4 changes: 4 additions & 0 deletions docs/reference/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,7 @@ example-value-or-externalValue:
- externalValue
- value
```
## typedEnum
When both a `type` and `enum` are defined for a property, the enum values must respect the type.
38 changes: 38 additions & 0 deletions docs/reference/openapi-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,44 @@ tags:

**Recommended:** No

### typed-enum

Enum values should respect the `type` specifier.

**Recommended:** Yes

**Good Example**

```yaml
TheGoodModel:
type: object
properties:
number_of_connectors:
type: integer
description: The number of extension points.
enum:
- 1
- 2
- 4
- 8
```

**Bad Example**

```yaml
TheBadModel:
type: object
properties:
number_of_connectors:
type: integer
description: The number of extension points.
enum:
- 1
- 2
- 'a string!'
- 8
```

## OpenAPI v2.0-only

These rules will only apply to OpenAPI v2.0 documents.
Expand Down
114 changes: 114 additions & 0 deletions src/functions/__tests__/typedEnum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { typedEnum } from '../typedEnum';

const defaultReportingThreshold = 3;

function runTypedEnum(targetVal: any, reportingThreshold: any) {
return typedEnum(
targetVal,
{ reportingThreshold },
{ given: ['$'] },
{ given: null, original: null, resolved: {} as any },
);
}

describe('typedEnum', () => {
describe('parameters validation', () => {
test.each([1, { a: 1 }, 'nope', undefined])('is undefined when the enum is not an array (%s)', enumContent => {
const schema = {
type: 'integer',
enum: enumContent,
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
});
});

test('is undefined when the enum contains no value', () => {
const schema = {
type: 'integer',
enum: [],
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
});

describe('basic', () => {
test('is undefined when all enum values respect the type', () => {
const schema = {
type: 'integer',
enum: [123, 456],
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
});

test('is undefined when all enum values respect the type', () => {
const schema = {
type: 'integer',
enum: [123, 456],
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
});

test.each([undefined])('is undefined when type is "%s"', (typeValue: unknown) => {
const schema = {
type: typeValue,
enum: [123, 456],
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
});

test('identifies enum values which do not respect the type', () => {
const schema = {
type: 'integer',
enum: [123, 'a string!', 456, 'and another one!'],
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toEqual([
{
message: 'Enum value "a string!" do not respect the specified type "integer".',
},
{
message: 'Enum value "and another one!" do not respect the specified type "integer".',
},
]);
});
});

describe('types', () => {
const testCases: Array<[string, unknown[], unknown]> = [
['string', ['Hello', 'world!'], 12],
['number', [-2147483648, 17.13], 'Hello'],
['integer', [-2147483648, 17], 12.3],
['boolean', [true, false], 1],
];

test.each(testCases)(
'does not report anything when all the definitions are valid for type "%s"',
async (type: string, valids: unknown[], invalid: unknown) => {
const schema = {
type,
enum: valids,
};

expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
},
);

test.each(testCases)(
'identifies enum value which does not respect the type "%s"',
async (type: string, valids: unknown[], invalid: unknown) => {
const schema = {
type,
enum: [valids[0], invalid],
};

const results = runTypedEnum(schema, defaultReportingThreshold);

expect(results[0].message).toContain(`value "${invalid}"`);
},
);
});
});
1 change: 1 addition & 0 deletions src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const functions = {
undefined: require('./undefined').undefined,
xor: require('./xor').xor,
unreferencedReusableObject: require('./unreferencedReusableObject').unreferencedReusableObject,
typedEnum: require('./typedEnum').typedEnum,
};
40 changes: 40 additions & 0 deletions src/functions/typedEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isArray } from 'lodash';
import { IFunction, IFunctionResult, IRule, RuleFunction } from '../types';
import { schema } from './schema';

export type TypedEnumRule = IRule<RuleFunction.TYPED_ENUM>;

export const typedEnum: IFunction = (targetVal, opts, paths, otherValues): void | IFunctionResult[] => {
const { enum: enumValues, ...initialSchema } = targetVal;

if (!isArray(enumValues)) {
return;
}

const innerSchema = { type: initialSchema.type, enum: initialSchema.enum };
const schemaObject = { schema: innerSchema };

const incorrectValues: unknown[] = [];

for (const val of enumValues) {
const res = schema(val, schemaObject, paths, otherValues);

if (res === undefined || res.length === 0) {
continue;
}

incorrectValues.push(val);
}

if (incorrectValues.length === 0) {
return;
}

const { type } = initialSchema;

return incorrectValues.map(val => {
return {
message: `Enum value "${val}" do not respect the specified type "${type}".`,
};
});
};
168 changes: 168 additions & 0 deletions src/rulesets/oas/__tests__/typed-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { typedEnum } from '../../../functions/typedEnum';
import { RuleType, Spectral } from '../../../index';
import { rules } from '../index.json';

describe('typed-enum', () => {
const s = new Spectral();
s.setFunctions({ typedEnum });
s.setRules({
'typed-enum': Object.assign(rules['typed-enum'], {
recommended: true,
type: RuleType[rules['typed-enum'].type],
}),
});

describe('oas2', () => {
test('does not report anything for empty object', async () => {
const results = await s.run({
swagger: '2.0',
});

expect(results).toEqual([]);
});

test('does not report anything when the model valid', async () => {
const doc = {
swagger: '2.0',
definitions: {
Test: {
type: 'integer',
enum: [1, 2, 3],
},
},
};

const results = await s.run(doc);

expect(results).toEqual([]);
});

test('identifies enum values which do not respect the type', async () => {
const doc = {
swagger: '2.0',
definitions: {
Test: {
type: 'integer',
enum: [1, 'a string!', 3, 'and another one!'],
},
},
};

const results = await s.run(doc);

expect(results).toEqual([
{
code: 'typed-enum',
message: 'Enum value "a string!" do not respect the specified type "integer".',
path: ['definitions', 'Test'],
range: {
start: {
character: 11,
line: 3,
},
end: {
character: 26,
line: 9,
},
},
severity: 1,
},
{
code: 'typed-enum',
message: 'Enum value "and another one!" do not respect the specified type "integer".',
path: ['definitions', 'Test'],
range: {
start: {
character: 11,
line: 3,
},
end: {
character: 26,
line: 9,
},
},
severity: 1,
},
]);
});
});

describe('oas3', () => {
test('does not report anything for empty object', async () => {
const results = await s.run({
openapi: '3.0.0',
});

expect(results).toEqual([]);
});

test('does not report anything when the model is valid', async () => {
const doc = {
openapi: '3.0.0',
components: {
schemas: {
Test: {
type: 'integer',
enum: [1, 2, 3],
},
},
},
};

const results = await s.run(doc);

expect(results).toEqual([]);
});

test('identifies enum values which do not respect the type', async () => {
const doc = {
openapi: '3.0.0',
components: {
schemas: {
Test: {
type: 'integer',
enum: [1, 'a string!', 3, 'and another one!'],
},
},
},
};

const results = await s.run(doc);

expect(results).toEqual([
{
code: 'typed-enum',
message: 'Enum value "a string!" do not respect the specified type "integer".',
path: ['components', 'schemas', 'Test'],
range: {
start: {
character: 13,
line: 4,
},
end: {
character: 28,
line: 10,
},
},
severity: 1,
},
{
code: 'typed-enum',
message: 'Enum value "and another one!" do not respect the specified type "integer".',
path: ['components', 'schemas', 'Test'],
range: {
start: {
character: 13,
line: 4,
},
end: {
character: 28,
line: 10,
},
},
severity: 1,
},
]);
});
});
});
Loading

0 comments on commit 1844f2b

Please sign in to comment.