Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tools/spectral/ipa/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Instead of using the [Spectral overrides approach](https://docs.stoplight.io/doc
"xgen-IPA-104-resource-has-GET": "Legacy API, not used by infrastructure-as-code tooling",
}
```

Some rules allow an exception to be inherited from a parent component. For example, an exception for `xgen-IPA-112-boolean-field-names-avoid-is-prefix` can be set on a schema instead of on each of its fields, and applies to all fields defined directly on that schema. Fields in nested inline schemas or in `allOf`, `anyOf` or `oneOf` composition need their own exception. Inherited exceptions are still counted in the IPA metrics. See the rule's implementation details in the [rulesets README](https://github.com/mongodb/openapi/tree/main/tools/spectral/ipa/rulesets/README.md) to check whether it supports inherited exceptions.

---
## Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,193 @@ testRule('xgen-IPA-112-boolean-field-names-avoid-is-prefix', [
},
errors: [],
},
{
name: 'schema with schema-level exception - applies to all fields of the schema',
document: {
components: {
schemas: {
SchemaName: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
isEnabled: { type: 'boolean' },
isActive: { type: 'boolean' },
visible: { type: 'boolean' },
nested: {
type: 'object',
properties: {
isNested: { type: 'boolean' },
},
},
},
},
OtherSchema: {
type: 'object',
properties: {
isOther: { type: 'boolean' },
},
},
},
},
paths: {
'/users': {
post: {
requestBody: {
content: {
'application/vnd.atlas.2024-01-01+json': {
schema: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
isAvailable: { type: 'boolean' },
},
},
},
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message: 'Boolean field "isNested" should not use the "is" prefix. Use "nested" instead.',
path: ['components', 'schemas', 'SchemaName', 'properties', 'nested', 'properties', 'isNested'],
severity: DiagnosticSeverity.Error,
},
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message: 'Boolean field "isOther" should not use the "is" prefix. Use "other" instead.',
path: ['components', 'schemas', 'OtherSchema', 'properties', 'isOther'],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'schema with both schema-level and field-level exception - field-level exception is unnecessary',
document: {
components: {
schemas: {
SchemaName: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
isEnabled: {
type: 'boolean',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message:
'This component is covered by an exception on its containing schema and does not need its own exception. Please remove the exception.',
path: [
'components',
'schemas',
'SchemaName',
'properties',
'isEnabled',
'x-xgen-IPA-exception',
'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'schema with unnecessary schema-level exception - no violating fields',
document: {
components: {
schemas: {
SchemaName: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
enabled: { type: 'boolean' },
active: { type: 'boolean' },
name: { type: 'string' },
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message:
'This component does not need an exception because none of its fields violate the rule. Please remove the exception.',
path: [
'components',
'schemas',
'SchemaName',
'x-xgen-IPA-exception',
'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'schema with schema-level exception whose only violating field is nested - exception is unnecessary',
document: {
components: {
schemas: {
SchemaName: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
name: { type: 'string' },
nested: {
type: 'object',
properties: {
isNested: { type: 'boolean' },
},
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message:
'This component does not need an exception because none of its fields violate the rule. Please remove the exception.',
path: [
'components',
'schemas',
'SchemaName',
'x-xgen-IPA-exception',
'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
],
severity: DiagnosticSeverity.Error,
},
{
code: 'xgen-IPA-112-boolean-field-names-avoid-is-prefix',
message: 'Boolean field "isNested" should not use the "is" prefix. Use "nested" instead.',
path: ['components', 'schemas', 'SchemaName', 'properties', 'nested', 'properties', 'isNested'],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'schema with referenced property types',
document: {
Expand Down Expand Up @@ -320,4 +507,29 @@ testRule('xgen-IPA-112-boolean-field-names-avoid-is-prefix', [
},
],
},
{
name: 'non-boolean field with its own exception and a schema-level exception',
document: {
components: {
schemas: {
SchemaName: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
properties: {
isEnabled: { type: 'boolean' },
name: {
type: 'string',
'x-xgen-IPA-exception': {
'xgen-IPA-112-boolean-field-names-avoid-is-prefix': 'Reason',
},
},
},
},
},
},
},
errors: [],
},
]);
49 changes: 49 additions & 0 deletions tools/spectral/ipa/__tests__/utils/collectionUtils.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, jest } from '@jest/globals';
import {
evaluateAndCollectAdoptionStatus,
evaluateAndCollectAdoptionStatusWithInheritedException,
evaluateAndCollectAdoptionStatusWithoutExceptions,
} from '../../rulesets/functions/utils/collectionUtils.js';
import collector from '../../metrics/collector.js';
Expand Down Expand Up @@ -162,4 +163,52 @@ describe('tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js', () =>
expect(collector.add).toHaveBeenCalledWith(TEST_ENTRY_TYPE.ADOPTION, testPath, testRuleName);
});
});

describe('evaluateAndCollectAdoptionStatusWithInheritedException', () => {
const testRuleName = 'xgen-IPA-XXX-rule';
const testPath = ['components', 'schemas', 'Schema', 'properties', 'field'];
const testParentObject = {
properties: { field: {} },
'x-xgen-IPA-exception': {
'xgen-IPA-XXX-rule': 'reason',
},
};

afterEach(() => {
jest.clearAllMocks();
});

it('returns empty and collects exceptions for the object when there are errors', () => {
const testErrors = [
{
path: testPath,
message: TEST_ERROR_MESSAGE,
},
];

const result = evaluateAndCollectAdoptionStatusWithInheritedException(
testErrors,
testRuleName,
testParentObject,
testPath
);

expect(result).toEqual(undefined);
expect(collector.add).toHaveBeenCalledTimes(1);
expect(collector.add).toHaveBeenCalledWith(TEST_ENTRY_TYPE.EXCEPTION, testPath, testRuleName, 'reason');
});

it('returns empty and collects adoptions when there are no errors', () => {
const result = evaluateAndCollectAdoptionStatusWithInheritedException(
[],
testRuleName,
testParentObject,
testPath
);

expect(result).toEqual(undefined);
expect(collector.add).toHaveBeenCalledTimes(1);
expect(collector.add).toHaveBeenCalledWith(TEST_ENTRY_TYPE.ADOPTION, testPath, testRuleName);
});
});
});
55 changes: 54 additions & 1 deletion tools/spectral/ipa/__tests__/utils/exceptions.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { hasException } from '../../rulesets/functions/utils/exceptions';
import { findExceptionInParentSchema, hasException } from '../../rulesets/functions/utils/exceptions';

const TEST_RULE_NAME_100 = 'xgen-IPA-100';

Expand Down Expand Up @@ -52,4 +52,57 @@ describe('tools/spectral/ipa/rulesets/functions/utils/exceptions.js', () => {
expect(hasException(objectWithNestedIpa100Exception, TEST_RULE_NAME_100)).toBe(false);
});
});
describe('findExceptionInParentSchema', () => {
const oas = {
components: {
schemas: {
SchemaWithException: {
...objectWithIpa100Exception,
properties: {
field: {},
fieldWithException: { ...objectWithIpa100Exception },
},
},
SchemaWithoutException: {
properties: {
field: {},
},
},
},
},
};

it('returns the schema if the parent schema has an exception', () => {
const path = ['components', 'schemas', 'SchemaWithException', 'properties', 'field'];
expect(findExceptionInParentSchema(oas, {}, path, TEST_RULE_NAME_100)).toEqual({
schema: oas.components.schemas.SchemaWithException,
});
});
it('returns an error if both the property and the parent schema have an exception', () => {
const path = ['components', 'schemas', 'SchemaWithException', 'properties', 'fieldWithException'];
expect(findExceptionInParentSchema(oas, objectWithIpa100Exception, path, TEST_RULE_NAME_100)).toEqual({
error: [
{
path: [...path, 'x-xgen-IPA-exception', TEST_RULE_NAME_100],
message:
'This component is covered by an exception on its containing schema and does not need its own exception. Please remove the exception.',
},
],
});
});
it('returns the schema if the property is undefined and the parent schema has an exception', () => {
const path = ['components', 'schemas', 'SchemaWithException', 'properties', 'field'];
expect(findExceptionInParentSchema(oas, undefined, path, TEST_RULE_NAME_100)).toEqual({
schema: oas.components.schemas.SchemaWithException,
});
});
it('returns null if the parent schema does not have an exception', () => {
const path = ['components', 'schemas', 'SchemaWithoutException', 'properties', 'field'];
expect(findExceptionInParentSchema(oas, {}, path, TEST_RULE_NAME_100)).toBeNull();
expect(findExceptionInParentSchema(oas, {}, path, 'xgen-IPA-101')).toBeNull();
});
it('returns null if the path does not point to a schema property', () => {
expect(findExceptionInParentSchema(oas, {}, ['components', 'schemas'], TEST_RULE_NAME_100)).toBeNull();
});
});
});
4 changes: 4 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-112.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ rules:
- Applies only to properties with type 'boolean'
- Identifies property names that start with "is" followed by an uppercase letter
- Suggests using the direct adjective form instead (e.g., "disabled" instead of "isDisabled")
- Exceptions can be set on the field or on the schema containing the field. An exception on the schema applies to all fields defined directly on that schema and is counted in the IPA exception metrics
- A field-level exception is unnecessary when the schema containing the field already has one
- A schema-level exception is unnecessary when no field defined directly on the schema violates the rule
- Fields in nested inline schemas or in allOf, anyOf or oneOf composition need their own exception
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-112-boolean-field-names-avoid-is-prefix'
severity: error
resolved: false
Expand Down
4 changes: 4 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,10 @@ Rule checks for the following conditions:
- Applies only to properties with type 'boolean'
- Identifies property names that start with "is" followed by an uppercase letter
- Suggests using the direct adjective form instead (e.g., "disabled" instead of "isDisabled")
- Exceptions can be set on the field or on the schema containing the field. An exception on the schema applies to all fields defined directly on that schema and is counted in the IPA exception metrics
- A field-level exception is unnecessary when the schema containing the field already has one
- A schema-level exception is unnecessary when no field defined directly on the schema violates the rule
- Fields in nested inline schemas or in allOf, anyOf or oneOf composition need their own exception



Expand Down
Loading
Loading