<commit_before>// @ts-check
'use strict';

const fs = require('fs');
const url = require('url');
const URL = url.URL;
const util = require('util');

const yaml = require('js-yaml');
const should = require('should');
let ajv = require('ajv')({
    allErrors: true,
    verbose: true,
    jsonPointers: true,
    patternGroups: true,
    extendRefs: true // optional, current default is to 'fail', spec behaviour is to 'ignore'
});
//meta: false, // optional, to prevent adding draft-06 meta-schema

let ajvFormats = require('ajv/lib/compile/formats.js');
ajv.addFormat('uriref', ajvFormats.full['uri-reference']);
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
ajv._refs['http://json-schema.org/schema'] = 'http://json-schema.org/draft-04/schema'; // optional, using unversioned URI is out of spec
let metaSchema = require('ajv/lib/refs/json-schema-v5.json');
ajv.addMetaSchema(metaSchema);
ajv._opts.defaultMeta = metaSchema.id;

const common = require('openapi-kit-common');
const jptr = require('reftools/lib/jptr.js');
const resolveInternal = jptr.jptr;
const clone = require('reftools/lib/clone.js').clone;
const recurse = require('reftools/lib/recurse.js').recurse;
const isRef = require('reftools/lib/isref.js').isRef;
const sw = require('openapi-schema-walker');
const linter = require('openapi-linter');
const resolver = require('openapi-resolver');

const jsonSchema = require('./schemas/json_v5.json');
const validateMetaSchema = ajv.compile(jsonSchema);
let openapi3Schema = require('./schemas/openapi-3.0.json');
let validateOpenAPI3 = ajv.compile(openapi3Schema);

const dummySchema = { anyOf: {} };
const emptySchema = {};
const urlRegexStr = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
const urlRegex = new RegExp(urlRegexStr, 'i');

function contextAppend(options, s) {
    options.context.push((options.context[options.context.length - 1] + '/' + s).split('//').join('/'));
}

function validateUrl(s, contextServers, context, options) {
    should(s).be.a.String();
    s.should.not.be.Null();
    if (!options.laxurls) s.should.not.be.exactly('', 'Invalid empty URL ' + context);
    let base = options.origin || 'http://localhost/';
    if (contextServers && contextServers.length) {
        let servers = contextServers[0];
        if (servers && servers.length) {
            base = servers[0].url;
        }
    }
    if (s.indexOf('://') > 0) { // FIXME HACK
        base = undefined;
    }
    //s.should.match(urlRegex); // doesn't allow for templated urls
    let u = (URL && options.whatwg) ? new URL(s, base) : url.parse(s);
    return true; // if we haven't thrown
}

function validateComponentName(name) {
    return /^[a-zA-Z0-9\.\-_]+$/.test(name);
}

function validateHeaderName(name) {
    return /^[A-Za-z0-9!#\-\$%&'\*\+\\\.\^_`\|~]+$/.test(name);
}

function validateSchema(schema, openapi, options) {
    validateMetaSchema(schema);
    let errors = validateSchema.errors;
    if (errors && errors.length) {
        throw (new Error('Schema invalid: ' + util.inspect(errors)));
    }
    return !(errors && errors.length);
}

function checkSubSchema(schema, parent, state) {
    let prop = state.property;
    if (prop) contextAppend(state.options, prop);
    if (state.options.lint) state.options.linter('schema',schema,'schema',state.options);
    schema.should.be.an.Object();
    schema.should.not.be.an.Array();

    if (typeof schema.$ref !== 'undefined') {
        schema.$ref.should.be.a.String();
        if (state.options.lint) state.options.linter('reference',schema,'$ref',state.options);
        if (prop) state.options.context.pop();
        return; // all other properties SHALL be ignored
    }

    for (let k in schema) {
        if (!k.startsWith('x-')) {
            should(['type','items','format','properties','required','minimum','maximum',
            'exclusiveMinimum','exclusiveMaximum','enum','default','description','title',
            'readOnly','writeOnly','anyOf','allOf','oneOf','not','discriminator','maxItems',
            'minItems','additionalItems','additionalProperties','example','maxLength',
            'minLength','pattern','uniqueItems','xml','externalDocs','nullable','deprecated',
            'minProperties','maxProperties','multipleOf'].indexOf(k)).
            be.greaterThan(-1,'Schema object cannot have additionalProperty: '+k);
        }
    }

    if (schema.multipleOf) {
        schema.multipleOf.should.be.a.Number();
        schema.multipleOf.should.be.greaterThan(0);
    }
    if (schema.maximum) {
        schema.maximum.should.be.a.Number();
    }
    if (schema.exclusiveMaximum) {
        schema.exclusiveMaximum.should.be.a.Boolean();
    }
    if (schema.minimum) {
        schema.minimum.should.be.a.Number();
    }
    if (schema.exclusiveMinimum) {
        schema.exclusiveMinimum.should.be.a.Boolean();
    }
    if (schema.maxLength) {
        schema.maxLength.should.be.a.Number();
        schema.maxLength.should.be.greaterThan(-1);
    }
    if (schema.minLength) {
        schema.minLength.should.be.a.Number();
        schema.minLength.should.be.greaterThan(-1);
    }
    if (schema.pattern) {
        try {
            let regex = new RegExp(schema.pattern);
        }
        catch (ex) {
            should.fail(false,true,'pattern does not conform to ECMA-262');
        }
    }
    if (typeof schema.items !== 'undefined') {
        schema.items.should.be.an.Object();
        schema.items.should.not.be.an.Array();
    }
    if (schema.additionalItems) {
        if (typeof schema.additionalItems === 'boolean') {
        }
        else if (typeof schema.additionalItems === 'object') {
            schema.additionalItems.should.not.be.an.Array();
        }
        else should.fail(false,true,'additionalItems must be a boolean or schema');
    }
    if (schema.additionalProperties) {
        if (typeof schema.additionalProperties === 'boolean') {
        }
        else if (typeof schema.additionalProperties === 'object') {
            schema.additionalProperties.should.not.be.an.Array();
        }
        else should.fail(false,true,'additionalProperties must be a boolean or schema');
    }
    if (schema.maxItems) {
        schema.maxItems.should.be.a.Number();
        schema.maxItems.should.be.greaterThan(-1);
    }
    if (schema.minItems) {
        schema.minItems.should.be.a.Number();
        schema.minItems.should.be.greaterThan(-1);
    }
    if (typeof schema.uniqueItems !== 'undefined') {
        schema.uniqueItems.should.be.a.Boolean();
    }
    if (schema.maxProperties) {
        schema.maxProperties.should.be.a.Number();
        schema.maxProperties.should.be.greaterThan(-1);
    }
    if (schema.minProperties) {
        schema.minProperties.should.be.a.Number();
        schema.minProperties.should.be.greaterThan(-1);
    }
    if (typeof schema.required !== 'undefined') {
        schema.required.should.be.an.Array();
        schema.required.should.not.be.empty();
        common.hasDuplicates(schema.required).should.be.exactly(false,'required items must be unique');
    }
    if (schema.properties) {
        schema.properties.should.be.an.Object();
        schema.properties.should.not.be.an.Array();
    }
    schema.should.not.have.property('patternProperties');
    /*if (schema.patternProperties) {
        schema.patternProperties.should.be.an.Object();
        for (let prop in schema.patternProperties) {
            try {
                let regex = new RegExp(prop);
            }
            catch (ex) {
                should.fail(false,true,'patternProperty '+prop+' does not conform to ECMA-262');
            }
        }
    }*/
    if (typeof schema.enum !== 'undefined') {
        schema.enum.should.be.an.Array();
        schema.enum.should.not.be.empty();
        // items only SHOULD be unique
    }
    if (typeof schema.type !== 'undefined') {
        schema.type.should.be.a.String(); // not an array
        schema.type.should.equalOneOf('integer','number','string','boolean','object','array'); // not null
        if (schema.type === 'array') {
            schema.should.have.property('items');
        }
    }
    if (schema.allOf) {
        schema.allOf.should.be.an.Array();
        schema.allOf.should.not.be.empty();
    }
    if (schema.anyOf) {
        schema.anyOf.should.be.an.Array();
        schema.anyOf.should.not.be.empty();
    }
    if (schema.oneOf) {
        schema.oneOf.should.be.an.Array();
        schema.oneOf.should.not.be.empty();
    }
    if (schema.not) {
        schema.not.should.be.an.Object();
        schema.oneOf.should.not.be.an.Array();
    }
    if (typeof schema.title !== 'undefined') {
        schema.title.should.be.a.String(); //?
    }
    if (typeof schema.description !== 'undefined') {
        schema.description.should.be.a.String();
    }
    if (typeof schema.default !== 'undefined') {
        schema.should.have.property('type');
        let realType = typeof schema.default;
        let schemaType = schema.type;
        if (Array.isArray(schema.default)) realType = 'array';
        if (schemaType === 'integer') schemaType = 'number';
        schemaType.should.equal(realType);
    }
    if (typeof schema.format !== 'undefined') {
        schema.format.should.be.a.String();
        if (schema.type && ['date-time','email','hostname','ipv4','ipv6','uri','uriref',
            'byte','binary','date','password'].indexOf(schema.format) >= 0) {
            schema.type.should.equal('string');
        }
        if (schema.type && ['int32','int64'].indexOf(schema.format) >= 0) {
            if (schema.type !== 'string' && schema.type !== 'number') { // common case - googleapis
               schema.type.should.equal('integer');
            }
        }
        if (schema.type && ['float','double'].indexOf(schema.format) >= 0) {
            if (schema.type !== 'string') { // occasionally seen
                schema.type.should.equal('number');
            }
        }
    }

    if (typeof schema.nullable !== 'undefined') {
        schema.nullable.should.be.a.Boolean();
    }
    if (typeof schema.readOnly !== 'undefined') {
        schema.readOnly.should.be.a.Boolean();
        schema.should.not.have.property('writeOnly');
    }
    if (typeof schema.writeOnly !== 'undefined') {
        schema.writeOnly.should.be.a.Boolean();
        schema.should.not.have.property('readOnly');
    }
    if (typeof schema.deprecated !== 'undefined') {
        schema.deprecated.should.be.a.Boolean();
    }
    if (typeof schema.discriminator !== 'undefined') {
        schema.discriminator.should.be.an.Object();
        schema.discriminator.should.not.be.an.Array();
        schema.discriminator.should.have.property('propertyName');
        //"To avoid redundancy, the discriminator MAY be added to a parent schema definition..."
        //if ((Object.keys(parent).length>0) && !(parent.oneOf || parent.anyOf || parent.allOf)) {
        //    should.fail(false,true,'discriminator requires oneOf, anyOf or allOf in parent schema');
        //}
    }
    if (typeof schema.xml !== 'undefined') {
        schema.xml.should.be.an.Object();
        schema.xml.should.not.be.an.Array();
    }
    // example can be any type

    if (schema.externalDocs) {
        schema.externalDocs.should.have.key('url');
        should.doesNotThrow(function() { validateUrl(schema.externalDocs.url, [state.openapi.servers], 'externalDocs', state.options)}, 'Invalid externalDocs.url');
    }
    if (prop) state.options.context.pop();
    if (!prop || prop === 'schema') validateSchema(schema, state.openapi, state.options); // top level only
}

function checkSchema(schema,parent,prop,openapi,options) {
    let state = sw.getDefaultState();
    state.openapi = openapi;
    state.options = options;
    state.property = prop;
    sw.walkSchema(schema,parent,state,checkSubSchema);
}

function checkExample(ex, contextServers, openapi, options) {
    ex.should.be.an.Object();
    ex.should.not.be.an.Array();
    if (typeof ex.summary !== 'undefined') {
        ex.summary.should.have.type('string');
    }
    if (typeof ex.description !== 'undefined') {
        ex.description.should.have.type('string');
    }
    if (typeof ex.value !== 'undefined') {
        ex.should.not.have.property('externalValue');
    }
    //else { // not mandated by the spec. moved to linter rule
    //    ex.should.have.property('externalValue');
    //}
    if (typeof ex.externalValue !== 'undefined') {
        ex.should.not.have.property('value');
        should.doesNotThrow(function () { validateUrl(ex.externalValue, contextServers, 'examples..externalValue', options) },'Invalid examples..externalValue');
    }
    //else { // not mandated by the spec. moved to linter rule
    //    ex.should.have.property('value');
    //}
    for (let k in ex) {
        if (!k.startsWith('x-')) {
            should(['summary','description','value','externalValue'].indexOf(k)).be.greaterThan(-1,'Example object cannot have additionalProperty: '+k);
        }
    }
    if (options.lint) options.linter('example',ex,'example',options);
}

function checkContent(content, contextServers, openapi, options) {
    contextAppend(options, 'content');
    content.should.be.an.Object();
    content.should.not.be.an.Array();
    for (let ct in content) {
        contextAppend(options, jptr.jpescape(ct));
        // validate ct against https://tools.ietf.org/html/rfc6838#section-4.2
        should(/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+\/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+/.test(ct)).be.exactly(true,'media-type should match RFC6838 format'); // this is a SHOULD not MUST
        let contentType = content[ct];
        should(contentType).be.an.Object();
        should(contentType).not.be.an.Array();

        if (typeof contentType.schema !== 'undefined') {
            checkSchema(contentType.schema,emptySchema,'schema',openapi,options);
        }
        if (contentType.example) {
            contentType.should.not.have.property('examples');
        }
        if (contentType.examples) {
            contextAppend(options, 'examples');
            contentType.should.not.have.property('example');
            contentType.examples.should.be.an.Object();
            contentType.examples.should.not.be.an.Array();
            for (let e in contentType.examples) {
                let ex = contentType.examples[e];
                if (typeof ex.$ref !== 'undefined') {
                    if (options.lint) options.linter('reference',ex,'$ref',options);
                }
                else {
                    checkExample(ex, contextServers, openapi, options);
                }
            }
            options.context.pop();
        }
        options.context.pop();
    }
    options.context.pop();
}

function checkServer(server, options) {
    server.should.have.property('url');
    should.doesNotThrow(function () { validateUrl(server.url, [], 'server.url', options) },'Invalid server.url');
    if (typeof server.description !== 'undefined') {
        should(server.description).be.a.String();
    }
    let srvVars = 0;
    server.url.replace(/\{(.+?)\}/g, function (match, group1) {
        srvVars++;
        server.should.have.key('variables');
        server.variables.should.have.key(group1);
    });
    if (typeof server.variables !== 'undefined') {
        contextAppend(options, 'variables');
        should(server.variables).be.an.Object();
        server.variables.should.not.be.an.Array();
        for (let v in server.variables) {
            contextAppend(options, v);
            should(server.variables[v]).be.an.Object();
            server.variables[v].should.have.key('default');
            server.variables[v].default.should.be.type('string');
            if (typeof server.variables[v].enum !== 'undefined') {
                contextAppend(options, 'enum');
                should(server.variables[v].enum).be.an.Array();
                should(server.variables[v].enum.length).not.be.exactly(0, 'Server variables enum should not be empty');
                for (let e in server.variables[v].enum) {
                    contextAppend(options, e);
                    should(server.variables[v].enum[e]).be.type('string');
                    options.context.pop();
                }
                options.context.pop();
            }
            if (options.lint) options.linter('serverVariable',server.variables[v],v,options);
            options.context.pop();
        }
        should(Object.keys(server.variables).length).be.exactly(srvVars);
        options.context.pop();
    }
    if (options.lint) options.linter('server',server,'server',options);
}

function checkServers(servers, options) {
    servers.should.be.an.Array();
    //common.distinctArray(servers).should.be.exactly(true,'servers array must be distinct'); // TODO move to linter
    for (let s in servers) {
        contextAppend(options, s);
        let server = servers[s];
        checkServer(server, options);
        options.context.pop();
    }
}

function checkLink(link, openapi, options) {
    if (typeof link.$ref !== 'undefined') {
        let ref = link.$ref;
        should(link.$ref).be.type('string');
        if (options.lint) options.linter('reference',link,'$ref',options);
        link = resolveInternal(openapi, ref);
        should(link).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    link.should.be.type('object');
    if (typeof link.operationRef === 'undefined') {
        link.should.have.property('operationId');
    }
    else {
        link.operationRef.should.be.type('string');
        link.should.not.have.property('operationId');
    }
    if (typeof link.operationId === 'undefined') {
        link.should.have.property('operationRef');
    }
    else {
        link.operationId.should.be.type('string');
        link.should.not.have.property('operationRef');
        // validate operationId exists (external refs?)
    }
    if (typeof link.parameters != 'undefined') {
        link.parameters.should.be.type('object');
        link.parameters.should.not.be.an.Array();
    }
    if (typeof link.description !== 'undefined') {
        link.description.should.have.type('string');
    }
    if (typeof link.server !== 'undefined') {
        checkServer(link.server, options);
    }
    if (options.lint) options.linter('link',link,'link',options);
}

function checkHeader(header, contextServers, openapi, options) {
    if (typeof header.$ref !== 'undefined') {
        let ref = header.$ref;
        should(header.$ref).be.type('string');
        if (options.lint) options.linter('reference',header,'$ref',options);
        header = resolveInternal(openapi, ref);
        should(header).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    header.should.not.have.property('name');
    header.should.not.have.property('in');
    header.should.not.have.property('type');
    for (let prop of common.parameterTypeProperties) {
        header.should.not.have.property(prop);
    }
    if (typeof header.schema !== 'undefined') {
        header.should.not.have.property('content');
        if (typeof header.style !== 'undefined') {
            header.style.should.be.type('string');
            header.style.should.be.exactly('simple');
        }
        if (typeof header.explode !== 'undefined') {
            header.explode.should.be.type('boolean');
        }
        if (typeof header.allowReserved !== 'undefined') {
            header.allowReserved.should.be.type('boolean');
        }
        checkSchema(header.schema, emptySchema, 'schema', openapi, options);
    }
    if (header.content) {
        header.should.not.have.property('schema');
        header.should.not.have.property('style');
        header.should.not.have.property('explode');
        header.should.not.have.property('allowReserved');
        header.should.not.have.property('example');
        header.should.not.have.property('examples');
        checkContent(header.content, contextServers, openapi, options);
    }
    if (!header.schema && !header.content) {
        header.should.have.property('schema', 'Header should have schema or content');
    }
    if (options.lint) options.linter('header',header,'header',options);
}

function checkResponse(response, contextServers, openapi, options) {
    if (typeof response.$ref !== 'undefined') {
        let ref = response.$ref;
        should(response.$ref).be.type('string');
        if (options.lint) options.linter('reference',response,'$ref',options);
        response = resolveInternal(openapi, ref);
        should(response).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    response.should.have.property('description');
    should(response.description).have.type('string', 'response description should be of type string');
    response.should.not.have.property('examples');
    response.should.not.have.property('schema');
    if (response.headers) {
        contextAppend(options, 'headers');
        for (let h in response.headers) {
            contextAppend(options, h);
            validateHeaderName(h).should.be.equal(true, 'Header doesn\'t match RFC7230 pattern');
            checkHeader(response.headers[h], contextServers, openapi, options);
            options.context.pop();
        }
        options.context.pop();
    }

    if (response.content) {
        checkContent(response.content, contextServers, openapi, options);
    }

    if (typeof response.links !== 'undefined') {
        contextAppend(options, 'links');
        for (let l in response.links) {
            contextAppend(options, l);
            checkLink(response.links[l], openapi, options);
            options.context.pop();
        }
        options.context.pop();
    }
    if (options.lint) options.linter('response',response,'response',options);
}

function checkParam(param, index, path, contextServers, openapi, options) {
    contextAppend(options, index);
    if (typeof param.$ref !== 'undefined') {
        should(param.$ref).be.type('string');
        if (options.lint) options.linter('reference',param,'$ref',options);
        let ref = param.$ref;
        param = resolveInternal(openapi, ref);
        should(param).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    param.should.have.property('name');
    param.name.should.have.type('string');
    param.should.have.property('in');
    param.in.should.have.type('string');
    param.in.should.equalOneOf('query', 'header', 'path', 'cookie');
    if (param.in === 'path') {
        param.should.have.property('required');
        param.required.should.be.exactly(true, 'Path parameters must have an explicit required:true');
        if (path) { // if we're not looking at a param from #/components (checked when used)
            should(path.indexOf('{'+param.name+'}')).be.greaterThan(-1,'path parameters must appear in the path');
        }
    }
    if (typeof param.required !== 'undefined') should(param.required).have.type('boolean');
    param.should.not.have.property('items');
    param.should.not.have.property('collectionFormat');
    param.should.not.have.property('type');
    for (let prop of common.parameterTypeProperties) {
        param.should.not.have.property(prop);
    }
    param.in.should.not.be.exactly('body', 'Parameter type body is no-longer valid');
    param.in.should.not.be.exactly('formData', 'Parameter type formData is no-longer valid');
    if (param.description) {
        param.description.should.have.type('string');
    }
    if (typeof param.deprecated !== 'undefined') {
        param.deprecated.should.be.a.Boolean();
    }
    if (typeof param.schema !== 'undefined') {
        param.should.not.have.property('content');
        if (typeof param.style !== 'undefined') {
            param.style.should.be.type('string');
            if (param.in === 'path') {
                param.style.should.not.be.exactly('form');
                param.style.should.not.be.exactly('spaceDelimited');
                param.style.should.not.be.exactly('pipeDelimited');
                param.style.should.not.be.exactly('deepObject');
            }
            if (param.in === 'query') {
                param.style.should.not.be.exactly('matrix');
                param.style.should.not.be.exactly('label');
                param.style.should.not.be.exactly('simple');
            }
            if (param.in === 'header') {
                param.style.should.be.exactly('simple');
            }
            if (param.in === 'cookie') {
                param.style.should.be.exactly('form');
            }
        }
        if (typeof param.explode !== 'undefined') {
            param.explode.should.be.type('boolean');
        }
        if (typeof param.allowReserved !== 'undefined') {
            param.allowReserved.should.be.type('boolean');
        }
        if (typeof param.example !== 'undefined') {
            param.should.not.have.key('examples');
        }
        if (typeof param.examples !== 'undefined') {
            contextAppend(options, 'examples');
            param.should.not.have.key('example');
            param.examples.should.be.an.Object();
            param.examples.should.not.be.an.Array();
            for (let e in param.examples) {
                contextAppend(options, e);
                let example = param.examples[e];
                checkExample(example, contextServers, openapi, options);
                options.context.pop();
            }
            options.context.pop();
        }
        checkSchema(param.schema, emptySchema, 'schema', openapi, options);
    }
    if (param.content) {
        param.should.not.have.property('schema');
        param.should.not.have.property('style');
        param.should.not.have.property('explode');
        param.should.not.have.property('allowReserved');
        param.should.not.have.property('example');
        param.should.not.have.property('examples');
        should(Object.keys(param.content).length).be.exactly(1, 'Parameter content must have only one entry');
        checkContent(param.content, contextServers, openapi, options);
    }
    if (!param.schema && !param.content) {
        param.should.have.property('schema', 'Parameter should have schema or content');
    }
    if (options.lint) options.linter('parameter',param,index,options);
    options.context.pop();
    return param;
}

function checkPathItem(pathItem, path, openapi, options) {

    let contextServers = [];
    contextServers.push(openapi.servers);
    if (pathItem.servers) contextServers.push(pathItem.servers);

    let pathParameters = {};
    if (typeof pathItem.parameters !== 'undefined') should(pathItem.parameters).be.an.Array();
    for (let p in pathItem.parameters) {
        contextAppend(options, 'parameters');
        let param = checkParam(pathItem.parameters[p], p, path, contextServers, openapi, options);
        if (pathParameters[param.in+':'+param.name]) {
            should.fail(false,true,'Duplicate path-level parameter '+param.name);
        }
        else {
            pathParameters[param.in+':'+param.name] = param;
        }
        options.context.pop();
    }

    for (let o in pathItem) {
        contextAppend(options, o);
        let op = pathItem[o];
        if (o === '$ref') {
            should(op).be.ok();
            op.should.have.type('string');
            should(op.startsWith('#/')).equal(false,'PathItem $refs must be external ('+op+')');
            if (options.lint) options.linter('reference',pathItem,'$ref',options);
        }
        else if (o === 'parameters') {
            // checked above
        }
        else if (o === 'servers') {
            contextAppend(options, 'servers');
            checkServers(op, options); // won't be here in converted definitions
            options.context.pop();
        }
        else if (o === 'summary') {
            pathItem.summary.should.have.type('string');
        }
        else if (o === 'description') {
            pathItem.description.should.have.type('string');
        }
        else if (common.httpMethods.indexOf(o) >= 0) {
            op.should.not.be.empty();
            op.should.not.have.property('consumes');
            op.should.not.have.property('produces');
            op.should.not.have.property('schemes');
            op.should.have.property('responses');
            op.responses.should.not.be.empty();
            if (op.summary) op.summary.should.have.type('string');
            if (op.description) op.description.should.have.type('string');
            if (typeof op.operationId !== 'undefined') {
                op.operationId.should.have.type('string');
                should(options.operationIds.indexOf(op.operationId)).be.exactly(-1,'operationIds must be unique');
                options.operationIds.push(op.operationId);
            }

            if (op.servers) {
                contextAppend(options, 'servers');
                checkServers(op.servers, options); // won't be here in converted definitions
                options.context.pop();
                contextServers.push(op.servers);
            }

            if (op.tags) {
                contextAppend(options, 'tags');
                op.tags.should.be.an.Array();
                for (let tag of op.tags) {
                    tag.should.be.a.String();
                }
                options.context.pop();
            }

            if (op.requestBody && op.requestBody.content) {
                contextAppend(options, 'requestBody');
                op.requestBody.should.have.property('content');
                if (typeof op.requestBody.description !== 'undefined') should(op.requestBody.description).have.type('string');
                if (typeof op.requestBody.required !== 'undefined') op.requestBody.required.should.have.type('boolean');
                checkContent(op.requestBody.content, contextServers, openapi, options);
                options.context.pop();
            }

            contextAppend(options, 'responses');
            for (let r in op.responses) {
                if (!r.startsWith('x-')) {
                    contextAppend(options, r);
                    let response = op.responses[r];
                    checkResponse(response, contextServers, openapi, options);
                    options.context.pop();
                }
            }
            options.context.pop();

            if (typeof op.parameters !== 'undefined') {
                should(op.parameters).be.an.Array();
                let localPathParameters = clone(pathParameters);
                let opParameters = {};
                contextAppend(options, 'parameters');
                for (let p in op.parameters) {
                    let param = checkParam(op.parameters[p], p, path, contextServers, openapi, options);
                    if (opParameters[param.in+':'+param.name]) {
                        should.fail(false,true,'Duplicate operation-level parameter '+param.name);
                    }
                    else {
                        opParameters[param.in+':'+param.name] = param;
                        delete localPathParameters[param.in+':'+param.name];
                    }
                }

                let contextParameters = Object.assign({},localPathParameters,opParameters);
                path.replace(/\{(.+?)\}/g, function (match, group1) {
                    if (!contextParameters['path:'+group1]) {
                        should.fail(false,true,'Templated parameter '+group1+' not found');
                    }
                });

                options.context.pop();
            }
            if (typeof op.deprecated !== 'undefined') {
                op.deprecated.should.be.a.Boolean();
            }
            if (op.externalDocs) {
                contextAppend(options, 'externalDocs');
                op.externalDocs.should.have.key('url');
                should.doesNotThrow(function () { validateUrl(op.externalDocs.url, contextServers, 'externalDocs', options) },'Invalid externalDocs.url');
                options.context.pop();
            }
            if (op.callbacks) {
                contextAppend(options, 'callbacks');
                for (let c in op.callbacks) {
                    let callback = op.callbacks[c];
                    if (callback && typeof callback.$ref !== 'undefined') {
                        if (options.lint) options.linter('reference',callback,'$ref',options);
                    }
                    else {
                        contextAppend(options, c);
                        for (let p in callback) {
                            let cbPi = callback[p];
                            options.isCallback = true;
                            checkPathItem(cbPi, p, openapi, options);
                            options.isCallBack = false;
                        }
                        options.context.pop();
                    }
                }
                options.context.pop();
            }
            if (op.security) {
                checkSecurity(op.security,openapi,options);
            }
            if (options.lint) options.linter('operation',op,o,options);
        }
        else if (!o.startsWith('x-')) {
            should.fail(false,true,'PathItem should not have additional property '+o);
        }
        options.context.pop();
    }
    if (options.lint) options.linter('pathItem',pathItem,path,options);
    return true;
}

function checkSecurity(security,openapi,options) {
    contextAppend(options, 'security');
    security.should.be.an.Array();
    //common.distinctArray(security).should.be.exactly(true,'security array should be distinct'); // TODO move to linter
    for (let sr of security) {
        sr.should.be.an.Object();
        sr.should.not.be.an.Array();
        for (let i in sr) {
            sr[i].should.be.an.Array();
            let sec = jptr.jptr(openapi,'#/components/securitySchemes/'+i);
            sec.should.not.be.exactly(false,'Could not dereference securityScheme '+i);
            if (sec.type !== 'oauth2') {
                sr[i].should.be.empty();
            }
            else if (sr[i].length) {
                if (!options.allScopes[i]) {
                    options.allScopes[i] = {};
                    if (sec.flows.password) Object.assign(options.allScopes[i],sec.flows.password.scopes);
                    if (sec.flows.implicit) Object.assign(options.allScopes[i],sec.flows.implicit.scopes);
                    if (sec.flows.authorizationCode) Object.assign(options.allScopes[i],sec.flows.authorizationCode.scopes);
                    if (sec.flows.clientCredentials) Object.assign(options.allScopes[i],sec.flows.clientCredentials.scopes);
                }
                for (let scope of sr[i]) {
                    options.allScopes[i].should.have.property(scope);
                }
            }
        }
    }
    if (options.lint) options.linter('security',security,'security',options);
    options.context.pop();
}

function validateSync(openapi, options, callback) {
    setupOptions(options,openapi);
    let contextServers = [];

    if (options.jsonschema) {
        let schemaStr = fs.readFileSync(options.jsonschema, 'utf8');
        openapi3Schema = yaml.safeLoad(schemaStr, { json: true });
        validateOpenAPI3 = ajv.compile(openapi3Schema);
    }

    should(openapi).be.an.Object();
    openapi.should.not.have.key('swagger');
    openapi.should.have.key('openapi');
    openapi.openapi.should.have.type('string');
    should.ok(openapi.openapi.startsWith('3.0.'), 'Must be an OpenAPI 3.0.x document');
    openapi.should.not.have.key('host');
    openapi.should.not.have.key('basePath');
    openapi.should.not.have.key('schemes');
    openapi.should.have.key('paths');
    should(openapi.paths).be.an.Object();
    openapi.should.not.have.key('definitions');
    openapi.should.not.have.key('parameters');
    openapi.should.not.have.key('responses');
    openapi.should.not.have.key('securityDefinitions');
    openapi.should.not.have.key('produces');
    openapi.should.not.have.key('consumes');

    for (let k in openapi) {
        if (!k.startsWith('x-')) {
            should(['openapi','info','servers','security','externalDocs','tags','paths','components'].indexOf(k)).be.greaterThan(-1,'OpenAPI object cannot have additionalProperty: '+k);
        }
    }

    openapi.should.have.key('info');
    should(openapi.info).be.an.Object();
    openapi.info.should.not.be.an.Array();
    contextAppend(options, 'info');
    openapi.info.should.have.key('title');
    should(openapi.info.title).be.type('string', 'title should be of type string');
    openapi.info.should.have.key('version');
    should(openapi.info.version).be.type('string', 'version should be of type string');
    if (typeof openapi.servers !== 'undefined') {
        should(openapi.servers).be.an.Object();
        contextAppend(options, 'servers');
        checkServers(openapi.servers, options);
        options.context.pop();
        contextServers.push(openapi.servers);
    }
    if (typeof openapi.info.license !== 'undefined') {
        should(openapi.info.license).be.an.Object();
        openapi.info.license.should.not.be.an.Array();
        contextAppend(options, 'license');
        openapi.info.license.should.have.key('name');
        should(openapi.info.license.name).have.type('string');
        if (typeof openapi.info.license.url !== 'undefined') {
            should.doesNotThrow(function () { validateUrl(openapi.info.license.url, contextServers, 'license.url', options) },'Invalid license.url');
        }
        if (options.lint) options.linter('license',openapi.info.license,'license',options);
        options.context.pop();
    }
    if (typeof openapi.info.termsOfService !== 'undefined') {
        should.doesNotThrow(function () { validateUrl(openapi.info.termsOfService, contextServers, 'termsOfService', options) },'Invalid termsOfService.url');
    }
    if (typeof openapi.info.contact !== 'undefined') {
        contextAppend(options, 'contact');
        should(openapi.info.contact).be.type('object');
        should(openapi.info.contact).not.be.an.Array();
        should(openapi.info.contact).not.be.Null();
        if (typeof openapi.info.contact.name !== 'undefined') {
            should(openapi.info.contact.name).be.a.String();
        }
        if (typeof openapi.info.contact.url !== 'undefined') {
            should.doesNotThrow(function () { validateUrl(openapi.info.contact.url, contextServers, 'url', options) },'Invalid contact.url');
        }
        if (typeof openapi.info.contact.email !== 'undefined') {
            openapi.info.contact.email.should.have.type('string');
            should(openapi.info.contact.email.indexOf('@')).be.greaterThan(-1,'Contact email must be a valid email address');
        }
        if (options.lint) options.linter('contact',openapi.info.contact,'contact',options);
        for (let k in openapi.info.contact) {
            if (!k.startsWith('x-')) {
                should(['name','url','email'].indexOf(k)).be.greaterThan(-1,'info object cannot have additionalProperty: '+k);
            }
        }
        options.context.pop();
    }
    if (typeof openapi.info.description !== 'undefined') {
        should(openapi.info.description).be.a.String();
    }
    if (options.lint) options.linter('info',openapi.info,'info',options);
    options.context.pop();

    if (typeof openapi.externalDocs !== 'undefined') {
        should(openapi.externalDocs).be.an.Object();
        contextAppend(options, 'externalDocs');
        openapi.externalDocs.should.have.key('url');
        if (typeof openapi.externalDocs.description !== 'undefined') {
            should(openapi.externalDocs.description).be.a.String();
        }
        should.doesNotThrow(function () { validateUrl(openapi.externalDocs.url, contextServers, 'externalDocs', options) },'Invalid externalDocs.url');
        options.context.pop();
    }

    if (typeof openapi.tags !== 'undefined') {
        should(openapi.tags).be.an.Array();
        contextAppend(options, 'tags');
        let tagsSeen = new Map();
        for (let tag of openapi.tags) {
            tag.should.have.property('name');
            contextAppend(options, tag.name);
            tag.name.should.have.type('string');
            tagsSeen.has(tag.name).should.be.exactly(false,'Tag names must be unique');
            tagsSeen.set(tag.name,true);
            if (typeof tag.externalDocs !== 'undefined') {
                contextAppend(options, 'externalDocs');
                should(tag.externalDocs).be.an.Object();
                tag.externalDocs.should.not.be.an.Array();
                if (typeof tag.externalDocs.description) {
                    should(tag.externalDocs.description).be.a.String();
                }
                tag.externalDocs.should.have.key('url');
                should.doesNotThrow(function () { validateUrl(tag.externalDocs.url, contextServers, 'tag.externalDocs', options) },'Invalid externalDocs.url');
                options.context.pop();
            }
            if (options.lint) options.linter('tag',tag,tag.name,options); // should be index
            options.context.pop();
        }
        options.context.pop();
    }

    if (openapi.security) {
        checkSecurity(openapi.security,openapi,options);
    }

    if (typeof openapi.components !== 'undefined') {
        options.context.push('#/components');
        should(openapi.components).be.an.Object();
        openapi.components.should.not.be.an.Array();
        if (options.lint) options.linter('components',openapi.components,'components',options);
        options.context.pop();
    }

    if (openapi.components && (typeof openapi.components.securitySchemes !== 'undefined')) {
        options.context.push('#/components/securitySchemes');
        should(openapi.components.securitySchemes).be.an.Object();
        openapi.components.securitySchemes.should.not.be.an.Array();
        for (let s in openapi.components.securitySchemes) {
            options.context.push('#/components/securitySchemes/' + s);<commit_msg>parser; fix tag externalDocs description checking<commit_after>// @ts-check
'use strict';

const fs = require('fs');
const url = require('url');
const URL = url.URL;
const util = require('util');

const yaml = require('js-yaml');
const should = require('should');
let ajv = require('ajv')({
    allErrors: true,
    verbose: true,
    jsonPointers: true,
    patternGroups: true,
    extendRefs: true // optional, current default is to 'fail', spec behaviour is to 'ignore'
});
//meta: false, // optional, to prevent adding draft-06 meta-schema

let ajvFormats = require('ajv/lib/compile/formats.js');
ajv.addFormat('uriref', ajvFormats.full['uri-reference']);
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
ajv._refs['http://json-schema.org/schema'] = 'http://json-schema.org/draft-04/schema'; // optional, using unversioned URI is out of spec
let metaSchema = require('ajv/lib/refs/json-schema-v5.json');
ajv.addMetaSchema(metaSchema);
ajv._opts.defaultMeta = metaSchema.id;

const common = require('openapi-kit-common');
const jptr = require('reftools/lib/jptr.js');
const resolveInternal = jptr.jptr;
const clone = require('reftools/lib/clone.js').clone;
const recurse = require('reftools/lib/recurse.js').recurse;
const isRef = require('reftools/lib/isref.js').isRef;
const sw = require('openapi-schema-walker');
const linter = require('openapi-linter');
const resolver = require('openapi-resolver');

const jsonSchema = require('./schemas/json_v5.json');
const validateMetaSchema = ajv.compile(jsonSchema);
let openapi3Schema = require('./schemas/openapi-3.0.json');
let validateOpenAPI3 = ajv.compile(openapi3Schema);

const dummySchema = { anyOf: {} };
const emptySchema = {};
const urlRegexStr = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
const urlRegex = new RegExp(urlRegexStr, 'i');

function contextAppend(options, s) {
    options.context.push((options.context[options.context.length - 1] + '/' + s).split('//').join('/'));
}

function validateUrl(s, contextServers, context, options) {
    should(s).be.a.String();
    s.should.not.be.Null();
    if (!options.laxurls) s.should.not.be.exactly('', 'Invalid empty URL ' + context);
    let base = options.origin || 'http://localhost/';
    if (contextServers && contextServers.length) {
        let servers = contextServers[0];
        if (servers && servers.length) {
            base = servers[0].url;
        }
    }
    if (s.indexOf('://') > 0) { // FIXME HACK
        base = undefined;
    }
    //s.should.match(urlRegex); // doesn't allow for templated urls
    let u = (URL && options.whatwg) ? new URL(s, base) : url.parse(s);
    return true; // if we haven't thrown
}

function validateComponentName(name) {
    return /^[a-zA-Z0-9\.\-_]+$/.test(name);
}

function validateHeaderName(name) {
    return /^[A-Za-z0-9!#\-\$%&'\*\+\\\.\^_`\|~]+$/.test(name);
}

function validateSchema(schema, openapi, options) {
    validateMetaSchema(schema);
    let errors = validateSchema.errors;
    if (errors && errors.length) {
        throw (new Error('Schema invalid: ' + util.inspect(errors)));
    }
    return !(errors && errors.length);
}

function checkSubSchema(schema, parent, state) {
    let prop = state.property;
    if (prop) contextAppend(state.options, prop);
    if (state.options.lint) state.options.linter('schema',schema,'schema',state.options);
    schema.should.be.an.Object();
    schema.should.not.be.an.Array();

    if (typeof schema.$ref !== 'undefined') {
        schema.$ref.should.be.a.String();
        if (state.options.lint) state.options.linter('reference',schema,'$ref',state.options);
        if (prop) state.options.context.pop();
        return; // all other properties SHALL be ignored
    }

    for (let k in schema) {
        if (!k.startsWith('x-')) {
            should(['type','items','format','properties','required','minimum','maximum',
            'exclusiveMinimum','exclusiveMaximum','enum','default','description','title',
            'readOnly','writeOnly','anyOf','allOf','oneOf','not','discriminator','maxItems',
            'minItems','additionalItems','additionalProperties','example','maxLength',
            'minLength','pattern','uniqueItems','xml','externalDocs','nullable','deprecated',
            'minProperties','maxProperties','multipleOf'].indexOf(k)).
            be.greaterThan(-1,'Schema object cannot have additionalProperty: '+k);
        }
    }

    if (schema.multipleOf) {
        schema.multipleOf.should.be.a.Number();
        schema.multipleOf.should.be.greaterThan(0);
    }
    if (schema.maximum) {
        schema.maximum.should.be.a.Number();
    }
    if (schema.exclusiveMaximum) {
        schema.exclusiveMaximum.should.be.a.Boolean();
    }
    if (schema.minimum) {
        schema.minimum.should.be.a.Number();
    }
    if (schema.exclusiveMinimum) {
        schema.exclusiveMinimum.should.be.a.Boolean();
    }
    if (schema.maxLength) {
        schema.maxLength.should.be.a.Number();
        schema.maxLength.should.be.greaterThan(-1);
    }
    if (schema.minLength) {
        schema.minLength.should.be.a.Number();
        schema.minLength.should.be.greaterThan(-1);
    }
    if (schema.pattern) {
        try {
            let regex = new RegExp(schema.pattern);
        }
        catch (ex) {
            should.fail(false,true,'pattern does not conform to ECMA-262');
        }
    }
    if (typeof schema.items !== 'undefined') {
        schema.items.should.be.an.Object();
        schema.items.should.not.be.an.Array();
    }
    if (schema.additionalItems) {
        if (typeof schema.additionalItems === 'boolean') {
        }
        else if (typeof schema.additionalItems === 'object') {
            schema.additionalItems.should.not.be.an.Array();
        }
        else should.fail(false,true,'additionalItems must be a boolean or schema');
    }
    if (schema.additionalProperties) {
        if (typeof schema.additionalProperties === 'boolean') {
        }
        else if (typeof schema.additionalProperties === 'object') {
            schema.additionalProperties.should.not.be.an.Array();
        }
        else should.fail(false,true,'additionalProperties must be a boolean or schema');
    }
    if (schema.maxItems) {
        schema.maxItems.should.be.a.Number();
        schema.maxItems.should.be.greaterThan(-1);
    }
    if (schema.minItems) {
        schema.minItems.should.be.a.Number();
        schema.minItems.should.be.greaterThan(-1);
    }
    if (typeof schema.uniqueItems !== 'undefined') {
        schema.uniqueItems.should.be.a.Boolean();
    }
    if (schema.maxProperties) {
        schema.maxProperties.should.be.a.Number();
        schema.maxProperties.should.be.greaterThan(-1);
    }
    if (schema.minProperties) {
        schema.minProperties.should.be.a.Number();
        schema.minProperties.should.be.greaterThan(-1);
    }
    if (typeof schema.required !== 'undefined') {
        schema.required.should.be.an.Array();
        schema.required.should.not.be.empty();
        common.hasDuplicates(schema.required).should.be.exactly(false,'required items must be unique');
    }
    if (schema.properties) {
        schema.properties.should.be.an.Object();
        schema.properties.should.not.be.an.Array();
    }
    schema.should.not.have.property('patternProperties');
    /*if (schema.patternProperties) {
        schema.patternProperties.should.be.an.Object();
        for (let prop in schema.patternProperties) {
            try {
                let regex = new RegExp(prop);
            }
            catch (ex) {
                should.fail(false,true,'patternProperty '+prop+' does not conform to ECMA-262');
            }
        }
    }*/
    if (typeof schema.enum !== 'undefined') {
        schema.enum.should.be.an.Array();
        schema.enum.should.not.be.empty();
        // items only SHOULD be unique
    }
    if (typeof schema.type !== 'undefined') {
        schema.type.should.be.a.String(); // not an array
        schema.type.should.equalOneOf('integer','number','string','boolean','object','array'); // not null
        if (schema.type === 'array') {
            schema.should.have.property('items');
        }
    }
    if (schema.allOf) {
        schema.allOf.should.be.an.Array();
        schema.allOf.should.not.be.empty();
    }
    if (schema.anyOf) {
        schema.anyOf.should.be.an.Array();
        schema.anyOf.should.not.be.empty();
    }
    if (schema.oneOf) {
        schema.oneOf.should.be.an.Array();
        schema.oneOf.should.not.be.empty();
    }
    if (schema.not) {
        schema.not.should.be.an.Object();
        schema.oneOf.should.not.be.an.Array();
    }
    if (typeof schema.title !== 'undefined') {
        schema.title.should.be.a.String(); //?
    }
    if (typeof schema.description !== 'undefined') {
        schema.description.should.be.a.String();
    }
    if (typeof schema.default !== 'undefined') {
        schema.should.have.property('type');
        let realType = typeof schema.default;
        let schemaType = schema.type;
        if (Array.isArray(schema.default)) realType = 'array';
        if (schemaType === 'integer') schemaType = 'number';
        schemaType.should.equal(realType);
    }
    if (typeof schema.format !== 'undefined') {
        schema.format.should.be.a.String();
        if (schema.type && ['date-time','email','hostname','ipv4','ipv6','uri','uriref',
            'byte','binary','date','password'].indexOf(schema.format) >= 0) {
            schema.type.should.equal('string');
        }
        if (schema.type && ['int32','int64'].indexOf(schema.format) >= 0) {
            if (schema.type !== 'string' && schema.type !== 'number') { // common case - googleapis
               schema.type.should.equal('integer');
            }
        }
        if (schema.type && ['float','double'].indexOf(schema.format) >= 0) {
            if (schema.type !== 'string') { // occasionally seen
                schema.type.should.equal('number');
            }
        }
    }

    if (typeof schema.nullable !== 'undefined') {
        schema.nullable.should.be.a.Boolean();
    }
    if (typeof schema.readOnly !== 'undefined') {
        schema.readOnly.should.be.a.Boolean();
        schema.should.not.have.property('writeOnly');
    }
    if (typeof schema.writeOnly !== 'undefined') {
        schema.writeOnly.should.be.a.Boolean();
        schema.should.not.have.property('readOnly');
    }
    if (typeof schema.deprecated !== 'undefined') {
        schema.deprecated.should.be.a.Boolean();
    }
    if (typeof schema.discriminator !== 'undefined') {
        schema.discriminator.should.be.an.Object();
        schema.discriminator.should.not.be.an.Array();
        schema.discriminator.should.have.property('propertyName');
        //"To avoid redundancy, the discriminator MAY be added to a parent schema definition..."
        //if ((Object.keys(parent).length>0) && !(parent.oneOf || parent.anyOf || parent.allOf)) {
        //    should.fail(false,true,'discriminator requires oneOf, anyOf or allOf in parent schema');
        //}
    }
    if (typeof schema.xml !== 'undefined') {
        schema.xml.should.be.an.Object();
        schema.xml.should.not.be.an.Array();
    }
    // example can be any type

    if (schema.externalDocs) {
        schema.externalDocs.should.have.key('url');
        should.doesNotThrow(function() { validateUrl(schema.externalDocs.url, [state.openapi.servers], 'externalDocs', state.options)}, 'Invalid externalDocs.url');
    }
    if (prop) state.options.context.pop();
    if (!prop || prop === 'schema') validateSchema(schema, state.openapi, state.options); // top level only
}

function checkSchema(schema,parent,prop,openapi,options) {
    let state = sw.getDefaultState();
    state.openapi = openapi;
    state.options = options;
    state.property = prop;
    sw.walkSchema(schema,parent,state,checkSubSchema);
}

function checkExample(ex, contextServers, openapi, options) {
    ex.should.be.an.Object();
    ex.should.not.be.an.Array();
    if (typeof ex.summary !== 'undefined') {
        ex.summary.should.have.type('string');
    }
    if (typeof ex.description !== 'undefined') {
        ex.description.should.have.type('string');
    }
    if (typeof ex.value !== 'undefined') {
        ex.should.not.have.property('externalValue');
    }
    //else { // not mandated by the spec. moved to linter rule
    //    ex.should.have.property('externalValue');
    //}
    if (typeof ex.externalValue !== 'undefined') {
        ex.should.not.have.property('value');
        should.doesNotThrow(function () { validateUrl(ex.externalValue, contextServers, 'examples..externalValue', options) },'Invalid examples..externalValue');
    }
    //else { // not mandated by the spec. moved to linter rule
    //    ex.should.have.property('value');
    //}
    for (let k in ex) {
        if (!k.startsWith('x-')) {
            should(['summary','description','value','externalValue'].indexOf(k)).be.greaterThan(-1,'Example object cannot have additionalProperty: '+k);
        }
    }
    if (options.lint) options.linter('example',ex,'example',options);
}

function checkContent(content, contextServers, openapi, options) {
    contextAppend(options, 'content');
    content.should.be.an.Object();
    content.should.not.be.an.Array();
    for (let ct in content) {
        contextAppend(options, jptr.jpescape(ct));
        // validate ct against https://tools.ietf.org/html/rfc6838#section-4.2
        should(/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+\/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+/.test(ct)).be.exactly(true,'media-type should match RFC6838 format'); // this is a SHOULD not MUST
        let contentType = content[ct];
        should(contentType).be.an.Object();
        should(contentType).not.be.an.Array();

        if (typeof contentType.schema !== 'undefined') {
            checkSchema(contentType.schema,emptySchema,'schema',openapi,options);
        }
        if (contentType.example) {
            contentType.should.not.have.property('examples');
        }
        if (contentType.examples) {
            contextAppend(options, 'examples');
            contentType.should.not.have.property('example');
            contentType.examples.should.be.an.Object();
            contentType.examples.should.not.be.an.Array();
            for (let e in contentType.examples) {
                let ex = contentType.examples[e];
                if (typeof ex.$ref !== 'undefined') {
                    if (options.lint) options.linter('reference',ex,'$ref',options);
                }
                else {
                    checkExample(ex, contextServers, openapi, options);
                }
            }
            options.context.pop();
        }
        options.context.pop();
    }
    options.context.pop();
}

function checkServer(server, options) {
    server.should.have.property('url');
    should.doesNotThrow(function () { validateUrl(server.url, [], 'server.url', options) },'Invalid server.url');
    if (typeof server.description !== 'undefined') {
        should(server.description).be.a.String();
    }
    let srvVars = 0;
    server.url.replace(/\{(.+?)\}/g, function (match, group1) {
        srvVars++;
        server.should.have.key('variables');
        server.variables.should.have.key(group1);
    });
    if (typeof server.variables !== 'undefined') {
        contextAppend(options, 'variables');
        should(server.variables).be.an.Object();
        server.variables.should.not.be.an.Array();
        for (let v in server.variables) {
            contextAppend(options, v);
            should(server.variables[v]).be.an.Object();
            server.variables[v].should.have.key('default');
            server.variables[v].default.should.be.type('string');
            if (typeof server.variables[v].enum !== 'undefined') {
                contextAppend(options, 'enum');
                should(server.variables[v].enum).be.an.Array();
                should(server.variables[v].enum.length).not.be.exactly(0, 'Server variables enum should not be empty');
                for (let e in server.variables[v].enum) {
                    contextAppend(options, e);
                    should(server.variables[v].enum[e]).be.type('string');
                    options.context.pop();
                }
                options.context.pop();
            }
            if (options.lint) options.linter('serverVariable',server.variables[v],v,options);
            options.context.pop();
        }
        should(Object.keys(server.variables).length).be.exactly(srvVars);
        options.context.pop();
    }
    if (options.lint) options.linter('server',server,'server',options);
}

function checkServers(servers, options) {
    servers.should.be.an.Array();
    //common.distinctArray(servers).should.be.exactly(true,'servers array must be distinct'); // TODO move to linter
    for (let s in servers) {
        contextAppend(options, s);
        let server = servers[s];
        checkServer(server, options);
        options.context.pop();
    }
}

function checkLink(link, openapi, options) {
    if (typeof link.$ref !== 'undefined') {
        let ref = link.$ref;
        should(link.$ref).be.type('string');
        if (options.lint) options.linter('reference',link,'$ref',options);
        link = resolveInternal(openapi, ref);
        should(link).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    link.should.be.type('object');
    if (typeof link.operationRef === 'undefined') {
        link.should.have.property('operationId');
    }
    else {
        link.operationRef.should.be.type('string');
        link.should.not.have.property('operationId');
    }
    if (typeof link.operationId === 'undefined') {
        link.should.have.property('operationRef');
    }
    else {
        link.operationId.should.be.type('string');
        link.should.not.have.property('operationRef');
        // validate operationId exists (external refs?)
    }
    if (typeof link.parameters != 'undefined') {
        link.parameters.should.be.type('object');
        link.parameters.should.not.be.an.Array();
    }
    if (typeof link.description !== 'undefined') {
        link.description.should.have.type('string');
    }
    if (typeof link.server !== 'undefined') {
        checkServer(link.server, options);
    }
    if (options.lint) options.linter('link',link,'link',options);
}

function checkHeader(header, contextServers, openapi, options) {
    if (typeof header.$ref !== 'undefined') {
        let ref = header.$ref;
        should(header.$ref).be.type('string');
        if (options.lint) options.linter('reference',header,'$ref',options);
        header = resolveInternal(openapi, ref);
        should(header).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    header.should.not.have.property('name');
    header.should.not.have.property('in');
    header.should.not.have.property('type');
    for (let prop of common.parameterTypeProperties) {
        header.should.not.have.property(prop);
    }
    if (typeof header.schema !== 'undefined') {
        header.should.not.have.property('content');
        if (typeof header.style !== 'undefined') {
            header.style.should.be.type('string');
            header.style.should.be.exactly('simple');
        }
        if (typeof header.explode !== 'undefined') {
            header.explode.should.be.type('boolean');
        }
        if (typeof header.allowReserved !== 'undefined') {
            header.allowReserved.should.be.type('boolean');
        }
        checkSchema(header.schema, emptySchema, 'schema', openapi, options);
    }
    if (header.content) {
        header.should.not.have.property('schema');
        header.should.not.have.property('style');
        header.should.not.have.property('explode');
        header.should.not.have.property('allowReserved');
        header.should.not.have.property('example');
        header.should.not.have.property('examples');
        checkContent(header.content, contextServers, openapi, options);
    }
    if (!header.schema && !header.content) {
        header.should.have.property('schema', 'Header should have schema or content');
    }
    if (options.lint) options.linter('header',header,'header',options);
}

function checkResponse(response, contextServers, openapi, options) {
    if (typeof response.$ref !== 'undefined') {
        let ref = response.$ref;
        should(response.$ref).be.type('string');
        if (options.lint) options.linter('reference',response,'$ref',options);
        response = resolveInternal(openapi, ref);
        should(response).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    response.should.have.property('description');
    should(response.description).have.type('string', 'response description should be of type string');
    response.should.not.have.property('examples');
    response.should.not.have.property('schema');
    if (response.headers) {
        contextAppend(options, 'headers');
        for (let h in response.headers) {
            contextAppend(options, h);
            validateHeaderName(h).should.be.equal(true, 'Header doesn\'t match RFC7230 pattern');
            checkHeader(response.headers[h], contextServers, openapi, options);
            options.context.pop();
        }
        options.context.pop();
    }

    if (response.content) {
        checkContent(response.content, contextServers, openapi, options);
    }

    if (typeof response.links !== 'undefined') {
        contextAppend(options, 'links');
        for (let l in response.links) {
            contextAppend(options, l);
            checkLink(response.links[l], openapi, options);
            options.context.pop();
        }
        options.context.pop();
    }
    if (options.lint) options.linter('response',response,'response',options);
}

function checkParam(param, index, path, contextServers, openapi, options) {
    contextAppend(options, index);
    if (typeof param.$ref !== 'undefined') {
        should(param.$ref).be.type('string');
        if (options.lint) options.linter('reference',param,'$ref',options);
        let ref = param.$ref;
        param = resolveInternal(openapi, ref);
        should(param).not.be.exactly(false, 'Cannot resolve reference: ' + ref);
    }
    param.should.have.property('name');
    param.name.should.have.type('string');
    param.should.have.property('in');
    param.in.should.have.type('string');
    param.in.should.equalOneOf('query', 'header', 'path', 'cookie');
    if (param.in === 'path') {
        param.should.have.property('required');
        param.required.should.be.exactly(true, 'Path parameters must have an explicit required:true');
        if (path) { // if we're not looking at a param from #/components (checked when used)
            should(path.indexOf('{'+param.name+'}')).be.greaterThan(-1,'path parameters must appear in the path');
        }
    }
    if (typeof param.required !== 'undefined') should(param.required).have.type('boolean');
    param.should.not.have.property('items');
    param.should.not.have.property('collectionFormat');
    param.should.not.have.property('type');
    for (let prop of common.parameterTypeProperties) {
        param.should.not.have.property(prop);
    }
    param.in.should.not.be.exactly('body', 'Parameter type body is no-longer valid');
    param.in.should.not.be.exactly('formData', 'Parameter type formData is no-longer valid');
    if (param.description) {
        param.description.should.have.type('string');
    }
    if (typeof param.deprecated !== 'undefined') {
        param.deprecated.should.be.a.Boolean();
    }
    if (typeof param.schema !== 'undefined') {
        param.should.not.have.property('content');
        if (typeof param.style !== 'undefined') {
            param.style.should.be.type('string');
            if (param.in === 'path') {
                param.style.should.not.be.exactly('form');
                param.style.should.not.be.exactly('spaceDelimited');
                param.style.should.not.be.exactly('pipeDelimited');
                param.style.should.not.be.exactly('deepObject');
            }
            if (param.in === 'query') {
                param.style.should.not.be.exactly('matrix');
                param.style.should.not.be.exactly('label');
                param.style.should.not.be.exactly('simple');
            }
            if (param.in === 'header') {
                param.style.should.be.exactly('simple');
            }
            if (param.in === 'cookie') {
                param.style.should.be.exactly('form');
            }
        }
        if (typeof param.explode !== 'undefined') {
            param.explode.should.be.type('boolean');
        }
        if (typeof param.allowReserved !== 'undefined') {
            param.allowReserved.should.be.type('boolean');
        }
        if (typeof param.example !== 'undefined') {
            param.should.not.have.key('examples');
        }
        if (typeof param.examples !== 'undefined') {
            contextAppend(options, 'examples');
            param.should.not.have.key('example');
            param.examples.should.be.an.Object();
            param.examples.should.not.be.an.Array();
            for (let e in param.examples) {
                contextAppend(options, e);
                let example = param.examples[e];
                checkExample(example, contextServers, openapi, options);
                options.context.pop();
            }
            options.context.pop();
        }
        checkSchema(param.schema, emptySchema, 'schema', openapi, options);
    }
    if (param.content) {
        param.should.not.have.property('schema');
        param.should.not.have.property('style');
        param.should.not.have.property('explode');
        param.should.not.have.property('allowReserved');
        param.should.not.have.property('example');
        param.should.not.have.property('examples');
        should(Object.keys(param.content).length).be.exactly(1, 'Parameter content must have only one entry');
        checkContent(param.content, contextServers, openapi, options);
    }
    if (!param.schema && !param.content) {
        param.should.have.property('schema', 'Parameter should have schema or content');
    }
    if (options.lint) options.linter('parameter',param,index,options);
    options.context.pop();
    return param;
}

function checkPathItem(pathItem, path, openapi, options) {

    let contextServers = [];
    contextServers.push(openapi.servers);
    if (pathItem.servers) contextServers.push(pathItem.servers);

    let pathParameters = {};
    if (typeof pathItem.parameters !== 'undefined') should(pathItem.parameters).be.an.Array();
    for (let p in pathItem.parameters) {
        contextAppend(options, 'parameters');
        let param = checkParam(pathItem.parameters[p], p, path, contextServers, openapi, options);
        if (pathParameters[param.in+':'+param.name]) {
            should.fail(false,true,'Duplicate path-level parameter '+param.name);
        }
        else {
            pathParameters[param.in+':'+param.name] = param;
        }
        options.context.pop();
    }

    for (let o in pathItem) {
        contextAppend(options, o);
        let op = pathItem[o];
        if (o === '$ref') {
            should(op).be.ok();
            op.should.have.type('string');
            should(op.startsWith('#/')).equal(false,'PathItem $refs must be external ('+op+')');
            if (options.lint) options.linter('reference',pathItem,'$ref',options);
        }
        else if (o === 'parameters') {
            // checked above
        }
        else if (o === 'servers') {
            contextAppend(options, 'servers');
            checkServers(op, options); // won't be here in converted definitions
            options.context.pop();
        }
        else if (o === 'summary') {
            pathItem.summary.should.have.type('string');
        }
        else if (o === 'description') {
            pathItem.description.should.have.type('string');
        }
        else if (common.httpMethods.indexOf(o) >= 0) {
            op.should.not.be.empty();
            op.should.not.have.property('consumes');
            op.should.not.have.property('produces');
            op.should.not.have.property('schemes');
            op.should.have.property('responses');
            op.responses.should.not.be.empty();
            if (op.summary) op.summary.should.have.type('string');
            if (op.description) op.description.should.have.type('string');
            if (typeof op.operationId !== 'undefined') {
                op.operationId.should.have.type('string');
                should(options.operationIds.indexOf(op.operationId)).be.exactly(-1,'operationIds must be unique');
                options.operationIds.push(op.operationId);
            }

            if (op.servers) {
                contextAppend(options, 'servers');
                checkServers(op.servers, options); // won't be here in converted definitions
                options.context.pop();
                contextServers.push(op.servers);
            }

            if (op.tags) {
                contextAppend(options, 'tags');
                op.tags.should.be.an.Array();
                for (let tag of op.tags) {
                    tag.should.be.a.String();
                }
                options.context.pop();
            }

            if (op.requestBody && op.requestBody.content) {
                contextAppend(options, 'requestBody');
                op.requestBody.should.have.property('content');
                if (typeof op.requestBody.description !== 'undefined') should(op.requestBody.description).have.type('string');
                if (typeof op.requestBody.required !== 'undefined') op.requestBody.required.should.have.type('boolean');
                checkContent(op.requestBody.content, contextServers, openapi, options);
                options.context.pop();
            }

            contextAppend(options, 'responses');
            for (let r in op.responses) {
                if (!r.startsWith('x-')) {
                    contextAppend(options, r);
                    let response = op.responses[r];
                    checkResponse(response, contextServers, openapi, options);
                    options.context.pop();
                }
            }
            options.context.pop();

            if (typeof op.parameters !== 'undefined') {
                should(op.parameters).be.an.Array();
                let localPathParameters = clone(pathParameters);
                let opParameters = {};
                contextAppend(options, 'parameters');
                for (let p in op.parameters) {
                    let param = checkParam(op.parameters[p], p, path, contextServers, openapi, options);
                    if (opParameters[param.in+':'+param.name]) {
                        should.fail(false,true,'Duplicate operation-level parameter '+param.name);
                    }
                    else {
                        opParameters[param.in+':'+param.name] = param;
                        delete localPathParameters[param.in+':'+param.name];
                    }
                }

                let contextParameters = Object.assign({},localPathParameters,opParameters);
                path.replace(/\{(.+?)\}/g, function (match, group1) {
                    if (!contextParameters['path:'+group1]) {
                        should.fail(false,true,'Templated parameter '+group1+' not found');
                    }
                });

                options.context.pop();
            }
            if (typeof op.deprecated !== 'undefined') {
                op.deprecated.should.be.a.Boolean();
            }
            if (op.externalDocs) {
                contextAppend(options, 'externalDocs');
                op.externalDocs.should.have.key('url');
                should.doesNotThrow(function () { validateUrl(op.externalDocs.url, contextServers, 'externalDocs', options) },'Invalid externalDocs.url');
                options.context.pop();
            }
            if (op.callbacks) {
                contextAppend(options, 'callbacks');
                for (let c in op.callbacks) {
                    let callback = op.callbacks[c];
                    if (callback && typeof callback.$ref !== 'undefined') {
                        if (options.lint) options.linter('reference',callback,'$ref',options);
                    }
                    else {
                        contextAppend(options, c);
                        for (let p in callback) {
                            let cbPi = callback[p];
                            options.isCallback = true;
                            checkPathItem(cbPi, p, openapi, options);
                            options.isCallBack = false;
                        }
                        options.context.pop();
                    }
                }
                options.context.pop();
            }
            if (op.security) {
                checkSecurity(op.security,openapi,options);
            }
            if (options.lint) options.linter('operation',op,o,options);
        }
        else if (!o.startsWith('x-')) {
            should.fail(false,true,'PathItem should not have additional property '+o);
        }
        options.context.pop();
    }
    if (options.lint) options.linter('pathItem',pathItem,path,options);
    return true;
}

function checkSecurity(security,openapi,options) {
    contextAppend(options, 'security');
    security.should.be.an.Array();
    //common.distinctArray(security).should.be.exactly(true,'security array should be distinct'); // TODO move to linter
    for (let sr of security) {
        sr.should.be.an.Object();
        sr.should.not.be.an.Array();
        for (let i in sr) {
            sr[i].should.be.an.Array();
            let sec = jptr.jptr(openapi,'#/components/securitySchemes/'+i);
            sec.should.not.be.exactly(false,'Could not dereference securityScheme '+i);
            if (sec.type !== 'oauth2') {
                sr[i].should.be.empty();
            }
            else if (sr[i].length) {
                if (!options.allScopes[i]) {
                    options.allScopes[i] = {};
                    if (sec.flows.password) Object.assign(options.allScopes[i],sec.flows.password.scopes);
                    if (sec.flows.implicit) Object.assign(options.allScopes[i],sec.flows.implicit.scopes);
                    if (sec.flows.authorizationCode) Object.assign(options.allScopes[i],sec.flows.authorizationCode.scopes);
                    if (sec.flows.clientCredentials) Object.assign(options.allScopes[i],sec.flows.clientCredentials.scopes);
                }
                for (let scope of sr[i]) {
                    options.allScopes[i].should.have.property(scope);
                }
            }
        }
    }
    if (options.lint) options.linter('security',security,'security',options);
    options.context.pop();
}

function validateSync(openapi, options, callback) {
    setupOptions(options,openapi);
    let contextServers = [];

    if (options.jsonschema) {
        let schemaStr = fs.readFileSync(options.jsonschema, 'utf8');
        openapi3Schema = yaml.safeLoad(schemaStr, { json: true });
        validateOpenAPI3 = ajv.compile(openapi3Schema);
    }

    should(openapi).be.an.Object();
    openapi.should.not.have.key('swagger');
    openapi.should.have.key('openapi');
    openapi.openapi.should.have.type('string');
    should.ok(openapi.openapi.startsWith('3.0.'), 'Must be an OpenAPI 3.0.x document');
    openapi.should.not.have.key('host');
    openapi.should.not.have.key('basePath');
    openapi.should.not.have.key('schemes');
    openapi.should.have.key('paths');
    should(openapi.paths).be.an.Object();
    openapi.should.not.have.key('definitions');
    openapi.should.not.have.key('parameters');
    openapi.should.not.have.key('responses');
    openapi.should.not.have.key('securityDefinitions');
    openapi.should.not.have.key('produces');
    openapi.should.not.have.key('consumes');

    for (let k in openapi) {
        if (!k.startsWith('x-')) {
            should(['openapi','info','servers','security','externalDocs','tags','paths','components'].indexOf(k)).be.greaterThan(-1,'OpenAPI object cannot have additionalProperty: '+k);
        }
    }

    openapi.should.have.key('info');
    should(openapi.info).be.an.Object();
    openapi.info.should.not.be.an.Array();
    contextAppend(options, 'info');
    openapi.info.should.have.key('title');
    should(openapi.info.title).be.type('string', 'title should be of type string');
    openapi.info.should.have.key('version');
    should(openapi.info.version).be.type('string', 'version should be of type string');
    if (typeof openapi.servers !== 'undefined') {
        should(openapi.servers).be.an.Object();
        contextAppend(options, 'servers');
        checkServers(openapi.servers, options);
        options.context.pop();
        contextServers.push(openapi.servers);
    }
    if (typeof openapi.info.license !== 'undefined') {
        should(openapi.info.license).be.an.Object();
        openapi.info.license.should.not.be.an.Array();
        contextAppend(options, 'license');
        openapi.info.license.should.have.key('name');
        should(openapi.info.license.name).have.type('string');
        if (typeof openapi.info.license.url !== 'undefined') {
            should.doesNotThrow(function () { validateUrl(openapi.info.license.url, contextServers, 'license.url', options) },'Invalid license.url');
        }
        if (options.lint) options.linter('license',openapi.info.license,'license',options);
        options.context.pop();
    }
    if (typeof openapi.info.termsOfService !== 'undefined') {
        should.doesNotThrow(function () { validateUrl(openapi.info.termsOfService, contextServers, 'termsOfService', options) },'Invalid termsOfService.url');
    }
    if (typeof openapi.info.contact !== 'undefined') {
        contextAppend(options, 'contact');
        should(openapi.info.contact).be.type('object');
        should(openapi.info.contact).not.be.an.Array();
        should(openapi.info.contact).not.be.Null();
        if (typeof openapi.info.contact.name !== 'undefined') {
            should(openapi.info.contact.name).be.a.String();
        }
        if (typeof openapi.info.contact.url !== 'undefined') {
            should.doesNotThrow(function () { validateUrl(openapi.info.contact.url, contextServers, 'url', options) },'Invalid contact.url');
        }
        if (typeof openapi.info.contact.email !== 'undefined') {
            openapi.info.contact.email.should.have.type('string');
            should(openapi.info.contact.email.indexOf('@')).be.greaterThan(-1,'Contact email must be a valid email address');
        }
        if (options.lint) options.linter('contact',openapi.info.contact,'contact',options);
        for (let k in openapi.info.contact) {
            if (!k.startsWith('x-')) {
                should(['name','url','email'].indexOf(k)).be.greaterThan(-1,'info object cannot have additionalProperty: '+k);
            }
        }
        options.context.pop();
    }
    if (typeof openapi.info.description !== 'undefined') {
        should(openapi.info.description).be.a.String();
    }
    if (options.lint) options.linter('info',openapi.info,'info',options);
    options.context.pop();

    if (typeof openapi.externalDocs !== 'undefined') {
        should(openapi.externalDocs).be.an.Object();
        contextAppend(options, 'externalDocs');
        openapi.externalDocs.should.have.key('url');
        if (typeof openapi.externalDocs.description !== 'undefined') {
            should(openapi.externalDocs.description).be.a.String();
        }
        should.doesNotThrow(function () { validateUrl(openapi.externalDocs.url, contextServers, 'externalDocs', options) },'Invalid externalDocs.url');
        options.context.pop();
    }

    if (typeof openapi.tags !== 'undefined') {
        should(openapi.tags).be.an.Array();
        contextAppend(options, 'tags');
        let tagsSeen = new Map();
        for (let tag of openapi.tags) {
            tag.should.have.property('name');
            contextAppend(options, tag.name);
            tag.name.should.have.type('string');
            tagsSeen.has(tag.name).should.be.exactly(false,'Tag names must be unique');
            tagsSeen.set(tag.name,true);
            if (typeof tag.externalDocs !== 'undefined') {
                contextAppend(options, 'externalDocs');
                should(tag.externalDocs).be.an.Object();
                tag.externalDocs.should.not.be.an.Array();
                if (typeof tag.externalDocs.description !== 'undefined') {
                    should(tag.externalDocs.description).be.a.String();
                }
                tag.externalDocs.should.have.key('url');
                should.doesNotThrow(function () { validateUrl(tag.externalDocs.url, contextServers, 'tag.externalDocs', options) },'Invalid externalDocs.url');
                options.context.pop();
            }
            if (options.lint) options.linter('tag',tag,tag.name,options); // should be index
            options.context.pop();
        }
        options.context.pop();
    }

    if (openapi.security) {
        checkSecurity(openapi.security,openapi,options);
    }

    if (typeof openapi.components !== 'undefined') {
        options.context.push('#/components');
        should(openapi.components).be.an.Object();
        openapi.components.should.not.be.an.Array();
        if (options.lint) options.linter('components',openapi.components,'components',options);
        options.context.pop();
    }

    if (openapi.components && (typeof openapi.components.securitySchemes !== 'undefined')) {
        options.context.push('#/components/securitySchemes');
        should(openapi.components.securitySchemes).be.an.Object();
        openapi.components.securitySchemes.should.not.be.an.Array();
        for (let s in openapi.components.securitySchemes) {
            options.context.push('#/components/securitySchemes/' + s);
{'fraction_non_alphanumeric': 0.1620400865719394, 'fraction_numerical': 0.00338759762868166, 'mean_word_length': 2.4428201182473477, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 15, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7637559', 'n_tokens_mistral': 24986, 'n_tokens_neox': 24519, 'n_words': 4031}