Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Upgrade AJV from 6 to 8 #31

Merged
merged 4 commits into from
Jun 28, 2023
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
25 changes: 2 additions & 23 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3
Expand All @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3
Expand All @@ -40,24 +40,3 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: ./scripts/build.sh
shell: bash

# Fails
# [10:27:19] Error: Command failed: ./node_modules/.bin/conventional-changelog-lint --from=HEAD~20 --preset angular
# ['.' is not recognized as an internal or external command,
# [operable program or batch file.
# build-and-test-windows:
# runs-on: windows-latest
# strategy:
# matrix:
# node-version: [12.x, 14.x, 16.x]

# steps:
# - uses: actions/checkout@v3
# with:
# fetch-depth: 0
# - name: Use Node.js ${{ matrix.node-version }}
# uses: actions/setup-node@v3
# with:
# node-version: ${{ matrix.node-version }}
# - run: ./bin/build.sh
# shell: bash
2 changes: 1 addition & 1 deletion jasmine.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"../test/helpers/**/*.ts"
],
"stopSpecOnExpectationFailure": false,
"random": true
"random": false
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ParsedSpecJsonSchemaCore, ParsedSpecOperation, ParsedSpecParameter} from '../../parsed-spec';
import {Header, Parameter} from '../openapi3';
import { ParsedSpecJsonSchemaCore, ParsedSpecOperation, ParsedSpecParameter } from '../../parsed-spec';
import { Header, Parameter } from '../openapi3';

interface ToParsedSpecParameterOptions {
parameter: Header | Parameter;
Expand All @@ -8,26 +8,39 @@ interface ToParsedSpecParameterOptions {
location: string;
}

const isParameterSchemaUndefined = (schema?: ParsedSpecJsonSchemaCore): schema is undefined =>
schema === undefined;
const isParameterSchemaUndefined = (schema?: ParsedSpecJsonSchemaCore): schema is undefined => schema === undefined;

const isParameterSchemaUnsupported = (schema: ParsedSpecJsonSchemaCore): boolean =>
schema.type === 'object' || schema.type === 'array';

// draft-06 onwards converts exclusiveMinimum and exclusiveMaximum to numbers
const upgradeSchema = (schema: ParsedSpecJsonSchemaCore): ParsedSpecJsonSchemaCore => {
if (schema.exclusiveMaximum) {
schema.exclusiveMaximum = schema.maximum;
}
if (schema.exclusiveMinimum) {
schema.exclusiveMinimum = schema.minimum;
}
return schema;
};

const getParameterSchema = (parameter: Header | Parameter): ParsedSpecJsonSchemaCore =>
isParameterSchemaUndefined(parameter.schema) || isParameterSchemaUnsupported(parameter.schema)
? {}
: parameter.schema;
? {}
: upgradeSchema(parameter.schema);

export const toParsedSpecParameter = (
{parameter, name, parentOperation, location}: ToParsedSpecParameterOptions
): ParsedSpecParameter => {
export const toParsedSpecParameter = ({
parameter,
name,
parentOperation,
location,
}: ToParsedSpecParameterOptions): ParsedSpecParameter => {
return {
location,
name,
parentOperation,
required: parameter.required || false,
schema: getParameterSchema(parameter),
value: parameter
value: parameter,
};
};
4 changes: 2 additions & 2 deletions lib/swagger-mock-validator/spec-parser/parsed-spec.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ interface ParsedSpecJsonSchemaBooleanKeywords {
interface ParsedSpecJsonSchemaValue {
additionalProperties?: boolean | ParsedSpecJsonSchema;
enum?: any[];
exclusiveMaximum?: boolean;
exclusiveMinimum?: boolean;
exclusiveMaximum?: number;
exclusiveMinimum?: number;
Comment on lines +63 to +64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI defines these as booleans, as did AJV@6.

But since https://json-schema.org/draft-06/json-schema-release-notes.html, it has been converted to a number. AJV followed suite.

format?: ParsedSpecSchemaFormat;
items?: ParsedSpecJsonSchema;
maxItems?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ const toParsedParameter = (
required: (parameter.value as SwaggerHeaderPathOrQueryParameter).required || false,
schema: {
enum: parameter.value.enum,
exclusiveMaximum: parameter.value.exclusiveMaximum,
exclusiveMinimum: parameter.value.exclusiveMinimum,
exclusiveMaximum: parameter.value.exclusiveMaximum ? parameter.value.maximum : undefined,
exclusiveMinimum: parameter.value.exclusiveMinimum ? parameter.value.minimum : undefined,
format: parameter.value.format,
items: parameter.value.items,
maxItems: parameter.value.maxItems,
Expand Down
131 changes: 12 additions & 119 deletions lib/swagger-mock-validator/validate-spec-and-mock/validate-json.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,21 @@
import Ajv from 'ajv';
import _ from 'lodash';
import {traverseJsonSchema} from '../common/traverse-json-schema';
import {ParsedSpecJsonSchema, ParsedSpecJsonSchemaCore} from '../spec-parser/parsed-spec';
import {isBinary} from './validate-json/binary';
import {isByte} from './validate-json/byte';
import {doubleAjvKeyword, formatForDoubleNumbers, isDouble} from './validate-json/double';
import {floatAjvKeyword, formatForFloatNumbers, isFloat} from './validate-json/float';
import {formatForInt32Numbers, int32AjvKeyword, isInt32} from './validate-json/int32';
import {formatForInt64Numbers, int64AjvKeyword, isInt64} from './validate-json/int64';
import { formatForString, isString, stringAjvKeyword } from './validate-json/string';
import {isPassword} from './validate-json/password';
import draft4MetaSchema from 'ajv/lib/refs/json-schema-draft-04.json';

const removeLeadingDotIfPresent = (dataPath: string): string =>
dataPath.replace(/^\./, '');

const getRawValueFromJson = (rawJson: any, dataPath?: string): any =>
dataPath ? _.get(rawJson, removeLeadingDotIfPresent(dataPath)) : rawJson;

const addSwaggerFormatsAndKeywords = (ajv: Ajv.Ajv, rawJson: any) => {
ajv.addFormat('binary', isBinary);
ajv.addFormat('byte', isByte);
ajv.addFormat('password', isPassword);
ajv.addKeyword(doubleAjvKeyword, {
type: 'number',
validate: (_schema: any, _data: number, _parentSchema: any, dataPath?: string) => {
const rawValue = getRawValueFromJson(rawJson, dataPath);
return isDouble(rawValue);
}
});
ajv.addKeyword(floatAjvKeyword, {
type: 'number',
validate: (_schema: any, _data: number, _parentSchema: any, dataPath?: string) => {
const rawValue = getRawValueFromJson(rawJson, dataPath);
return isFloat(rawValue);
}
});
ajv.addKeyword(int32AjvKeyword, {
type: 'integer',
validate: (_schema: any, _data: number, _parentSchema: any, dataPath?: string) => {
const rawValue = getRawValueFromJson(rawJson, dataPath);
return isInt32(rawValue);
}
});
ajv.addKeyword(int64AjvKeyword, {
type: 'integer',
validate: (_schema: any, _data: number, _parentSchema: any, dataPath?: string) => {
const rawValue = getRawValueFromJson(rawJson, dataPath);
return isInt64(rawValue);
}
});
ajv.addKeyword(stringAjvKeyword, {
type: 'string',
validate: (_schema: any, _data: string, _parentSchema: any, dataPath?: string) => {
const rawValue = getRawValueFromJson(rawJson, dataPath);
return isString(rawValue);
}
});
};

const nonSwaggerAjvFormats = [
'email',
'hostname',
'ipv4',
'ipv6',
'json-pointer',
'regex',
'relative-json-pointer',
'time',
'uri',
'uuid',
'url',
'uri-template',
'uri-reference'
];

const alwaysTrue = () => true;

const removeNonSwaggerAjvFormats = (ajv: Ajv.Ajv) => {
nonSwaggerAjvFormats.forEach((formatName) => {
ajv.addFormat(formatName, alwaysTrue);
});
};

const updateSchemaPropertyToDraft4 = (schema: ParsedSpecJsonSchema) => {
(schema as any).$schema = 'http://json-schema.org/draft-04/schema';
};

const changeTypeToKeywordForCustomFormats = (schema?: ParsedSpecJsonSchema) => {
traverseJsonSchema(schema, (mutableSchema: ParsedSpecJsonSchemaCore) => {
formatForDoubleNumbers(mutableSchema);
formatForFloatNumbers(mutableSchema);
formatForInt32Numbers(mutableSchema);
formatForInt64Numbers(mutableSchema);
formatForString(mutableSchema);
});
};
const createAjvForDraft4 = (userOptions: Ajv.Options) => {
const optionsRequiredForDraft4 = {
logger: false,
schemaId: 'id'
};

const options: Ajv.Options = _.defaultsDeep({}, userOptions, optionsRequiredForDraft4);

const ajv = new Ajv(options);
ajv.addMetaSchema(draft4MetaSchema);

return ajv;
};
import Ajv from 'ajv';
import addFormats from "ajv-formats"
import {ParsedSpecJsonSchema} from '../spec-parser/parsed-spec';

export const validateJson = (jsonSchema: ParsedSpecJsonSchema, json: any, numbersSentAsStrings?: boolean) => {
const ajv = createAjvForDraft4({
const ajv = new Ajv({
allErrors: true,
coerceTypes: numbersSentAsStrings || false,
verbose: true
logger: false,
strictSchema: false,
});
addFormats(ajv);
ajv.addKeyword({
keyword: 'collectionFormat',
type: 'array',
});

addSwaggerFormatsAndKeywords(ajv, json);
removeNonSwaggerAjvFormats(ajv);
ajv.validate(jsonSchema, json);

const ajvCompatibleJsonSchema = _.cloneDeep(jsonSchema);
changeTypeToKeywordForCustomFormats(ajvCompatibleJsonSchema);
updateSchemaPropertyToDraft4(ajvCompatibleJsonSchema);
ajv.validate(ajvCompatibleJsonSchema, _.cloneDeep(json));
return ajv.errors || [];
};

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading