"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
const ajv_1 = __importDefault(require("ajv"));
const probe_image_size_1 = __importDefault(require("probe-image-size"));
const read_chunk_1 = __importDefault(require("read-chunk"));
const json_schema_traverse_1 = __importDefault(require("json-schema-traverse"));
const Util_1 = require("./Util");
const Error_1 = require("./Error");
var Error_2 = require("./Error");
exports.SchemerError = Error_2.SchemerError;
exports.ValidationError = Error_2.ValidationError;
exports.ErrorCodes = Error_2.ErrorCodes;
class Schemer {
    // Schema is a JSON Schema object
    constructor(schema, options = {}) {
        this.options = lodash_1.default.extend({
            allErrors: true,
            verbose: true,
            format: 'full',
            metaValidation: true,
        }, options);
        this.ajv = new ajv_1.default(this.options);
        this.schema = schema;
        this.rootDir = this.options.rootDir || __dirname;
        this.manualValidationErrors = [];
    }
    _formatAjvErrorMessage({ keyword, dataPath, params, parentSchema, data, message, }) {
        const meta = parentSchema && parentSchema.meta;
        // This removes the "." in front of a fieldPath
        dataPath = dataPath.slice(1);
        switch (keyword) {
            case 'additionalProperties': {
                return new Error_1.ValidationError({
                    errorCode: 'SCHEMA_ADDITIONAL_PROPERTY',
                    fieldPath: dataPath,
                    message: `should NOT have additional property '${params.additionalProperty}'`,
                    data,
                    meta,
                });
            }
            case 'required':
                return new Error_1.ValidationError({
                    errorCode: 'SCHEMA_MISSING_REQUIRED_PROPERTY',
                    fieldPath: dataPath,
                    message: `is missing required property '${params.missingProperty}'`,
                    data,
                    meta,
                });
            case 'pattern': {
                //@TODO Parse the message in a less hacky way. Perhaps for regex validation errors, embed the error message under the meta tag?
                const regexHuman = lodash_1.default.get(parentSchema, 'meta.regexHuman');
                const regexErrorMessage = regexHuman
                    ? `'${dataPath}' should be a ${regexHuman[0].toLowerCase() + regexHuman.slice(1)}`
                    : `'${dataPath}' ${message}`;
                return new Error_1.ValidationError({
                    errorCode: 'SCHEMA_INVALID_PATTERN',
                    fieldPath: dataPath,
                    message: regexErrorMessage,
                    data,
                    meta,
                });
            }
            default:
                return new Error_1.ValidationError({
                    errorCode: 'SCHEMA_VALIDATION_ERROR',
                    fieldPath: dataPath,
                    message: message || 'Validation error',
                    data,
                    meta,
                });
        }
    }
    getErrors() {
        // Convert AJV JSONSchema errors to our ValidationErrors
        let valErrors = [];
        if (this.ajv.errors) {
            valErrors = this.ajv.errors.map(e => this._formatAjvErrorMessage(e));
        }
        const bothErrors = lodash_1.default.concat(valErrors, this.manualValidationErrors);
        return bothErrors;
    }
    _throwOnErrors() {
        // Clean error state after each validation
        const errors = this.getErrors();
        if (errors.length > 0) {
            this.manualValidationErrors = [];
            this.ajv.errors = [];
            throw new Error_1.SchemerError(errors);
        }
    }
    validateAll(data) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._validateSchemaAsync(data);
            yield this._validateAssetsAsync(data);
            this._throwOnErrors();
        });
    }
    validateAssetsAsync(data) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._validateAssetsAsync(data);
            this._throwOnErrors();
        });
    }
    validateSchemaAsync(data) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._validateSchemaAsync(data);
            this._throwOnErrors();
        });
    }
    _validateSchemaAsync(data) {
        this.ajv.validate(this.schema, data);
    }
    _validateAssetsAsync(data) {
        return __awaiter(this, void 0, void 0, function* () {
            let assets = [];
            json_schema_traverse_1.default(this.schema, { allKeys: true }, (subSchema, jsonPointer, a, b, c, d, property) => {
                if (property && subSchema.meta && subSchema.meta.asset) {
                    const fieldPath = Util_1.schemaPointerToFieldPath(jsonPointer);
                    assets.push({
                        fieldPath,
                        data: lodash_1.default.get(data, fieldPath),
                        meta: subSchema.meta,
                    });
                }
            });
            yield Promise.all(assets.map(this._validateAssetAsync.bind(this)));
        });
    }
    _validateImageAsync({ fieldPath, data, meta }) {
        return __awaiter(this, void 0, void 0, function* () {
            if (meta && meta.asset && data) {
                const { dimensions, square, contentTypePattern } = meta;
                // filePath could be an URL
                const filePath = path_1.default.resolve(this.rootDir, data);
                try {
                    // NOTE(nikki): The '4100' below should be enough for most file types, though we
                    //              could probably go shorter....
                    //              http://www.garykessler.net/library/file_sigs.html
                    //  The metadata content for .jpgs might be located a lot farther down the file, so this
                    //  may pose problems in the future.
                    //  This cases on whether filePath is a remote URL or located on the machine
                    const probeResult = fs_1.default.existsSync(filePath)
                        ? probe_image_size_1.default.sync(yield read_chunk_1.default(filePath, 0, 4100))
                        : yield probe_image_size_1.default(data, { useElectronNet: false });
                    if (!probeResult) {
                        return;
                    }
                    const { width, height, type, mime } = probeResult;
                    if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) {
                        this.manualValidationErrors.push(new Error_1.ValidationError({
                            errorCode: 'INVALID_CONTENT_TYPE',
                            fieldPath,
                            message: `field '${fieldPath}' should point to ${meta.contentTypeHuman} but the file at '${data}' has type ${type}`,
                            data,
                            meta,
                        }));
                    }
                    if (dimensions && (dimensions.height !== height || dimensions.width !== width)) {
                        this.manualValidationErrors.push(new Error_1.ValidationError({
                            errorCode: 'INVALID_DIMENSIONS',
                            fieldPath,
                            message: `'${fieldPath}' should have dimensions ${dimensions.width}x${dimensions.height}, but the file at '${data}' has dimensions ${width}x${height}`,
                            data,
                            meta,
                        }));
                    }
                    if (square && width !== height) {
                        this.manualValidationErrors.push(new Error_1.ValidationError({
                            errorCode: 'NOT_SQUARE',
                            fieldPath,
                            message: `image should be square, but the file at '${data}' has dimensions ${width}x${height}`,
                            data,
                            meta,
                        }));
                    }
                }
                catch (e) {
                    this.manualValidationErrors.push(new Error_1.ValidationError({
                        errorCode: 'INVALID_ASSET_URI',
                        fieldPath,
                        message: `cannot access file at '${data}'`,
                        data,
                        meta,
                    }));
                }
            }
        });
    }
    _validateAssetAsync({ fieldPath, data, meta }) {
        return __awaiter(this, void 0, void 0, function* () {
            if (meta && meta.asset && data) {
                if (meta.contentTypePattern && meta.contentTypePattern.startsWith('^image')) {
                    yield this._validateImageAsync({ fieldPath, data, meta });
                }
            }
        });
    }
    validateProperty(fieldPath, data) {
        return __awaiter(this, void 0, void 0, function* () {
            const subSchema = Util_1.fieldPathToSchema(this.schema, fieldPath);
            this.ajv.validate(subSchema, data);
            if (subSchema.meta && subSchema.meta.asset) {
                yield this._validateAssetAsync({ fieldPath, data, meta: subSchema.meta });
            }
            this._throwOnErrors();
        });
    }
    validateName(name) {
        return this.validateProperty('name', name);
    }
    validateSlug(slug) {
        return this.validateProperty('slug', slug);
    }
    validateSdkVersion(version) {
        return this.validateProperty('sdkVersion', version);
    }
    validateIcon(iconPath) {
        return this.validateProperty('icon', iconPath);
    }
}
exports.default = Schemer;
//# sourceMappingURL=index.js.map