"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateObjectNodeFieldName = exports.fieldApiPrefixes = exports.bannedFieldNames = exports.validateField = exports.validateRootField = exports.validateSchemaCollection = exports.aggregateSchemaLibraries = exports.schemaLintDefault = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../../util");
const default_schema_1 = require("../default-schema");
const multiplicity_1 = require("../multiplicity");
const typedTreeSchema_1 = require("./typedTreeSchema");
const flexList_1 = require("./flexList");
{
}
exports.schemaLintDefault = {
    rejectForbidden: true,
    rejectEmpty: true,
};
/**
 * Build and validate a SchemaCollection.
 *
 * As much as possible tries to detect anything that might be a mistake made by the schema author.
 * This will error on some valid but probably never intended to be used patterns
 * (like libraries with the same name, nodes which are impossible to create etc).
 *
 * @param name - Name of the resulting library.
 * @param lintConfiguration - configuration for what errors to generate.
 * @param libraries - Data to aggregate into the SchemaCollection.
 * @param rootFieldSchema - Only validated: not included in the result.
 *
 * @privateRemarks
 * This checks that input works with defaultSchemaPolicy.
 * If support fo other policies is added, this will need to take in the policy.
 */
function aggregateSchemaLibraries(name, lintConfiguration, libraries, rootFieldSchema) {
    const nodeSchema = new Map();
    const adapters = { tree: [] };
    const errors = [];
    const librarySet = new Set();
    const libraryNames = new Set();
    for (const library of libraries) {
        if (librarySet.has(library)) {
            // SchemaBuilder should ensure this doesn't happen, but include it here for completeness.
            errors.push(`Duplicate library named "${library.name}"`);
            continue;
        }
        librarySet.add(library);
        if (libraryNames.has(library.name)) {
            // This wouldn't break anything, but could make error messages confusing, so its better to avoid duplicates.
            errors.push(`Found another library with name "${library.name}"`);
        }
        for (const [key, tree] of library.nodeSchema) {
            // This check is an assert since if it fails, the other error messages would be incorrect.
            (0, core_utils_1.assert)(tree.builder.name === library.name, 0x6a9 /* tree must be part by the library its in */);
            const existing = nodeSchema.get(key);
            if (existing !== undefined) {
                errors.push(`Multiple tree schema for identifier "${key}". One from library "${existing.builder.name}" and one from "${tree.builder.name}"`);
            }
            else {
                nodeSchema.set(key, tree);
            }
        }
        for (const _adapter of library.adapters.tree ?? []) {
            (0, util_1.fail)("Adapters not yet supported");
        }
    }
    if (errors.length !== 0) {
        (0, util_1.fail)(errors.join("\n"));
    }
    const result = { rootFieldSchema, nodeSchema, adapters, policy: default_schema_1.defaultSchemaPolicy };
    const errors2 = validateSchemaCollection(lintConfiguration, result, rootFieldSchema);
    if (errors2.length !== 0) {
        (0, util_1.fail)(errors2.join("\n"));
    }
    return {
        name,
        nodeSchema,
        adapters,
    };
}
exports.aggregateSchemaLibraries = aggregateSchemaLibraries;
/**
 * Returns an array of descriptions of errors in the collection.
 *
 * As much as possible tries to detect anything that might be a mistake made by the schema author.
 * This will error on some valid but probably never intended to be used patterns (like never nodes).
 */
function validateSchemaCollection(lintConfiguration, collection, rootFieldSchema) {
    const errors = [];
    // TODO: make this check specific to document schema. Replace check here for no tre or field schema (empty library).
    if (collection.nodeSchema.size === 0 && lintConfiguration.rejectEmpty) {
        errors.push("No tree schema are included, meaning no data can possibly be stored.");
    }
    // Validate that all schema referenced are included, and none are "never".
    if (rootFieldSchema !== undefined) {
        validateRootField(lintConfiguration, collection, rootFieldSchema, errors);
    }
    for (const [identifier, tree] of collection.nodeSchema) {
        if (tree instanceof typedTreeSchema_1.MapNodeSchema) {
            validateField(lintConfiguration, collection, tree.info, () => `Map fields of "${identifier}" schema from library "${tree.builder.name}"`, errors);
            if (tree.info.kind.multiplicity === multiplicity_1.Multiplicity.Single) {
                errors.push(`Map fields of "${identifier}" schema from library "${tree.builder.name}" has kind with multiplicity "Single". This is invalid since it requires all possible field keys to have a value under them.`);
            }
        }
        else if (tree instanceof typedTreeSchema_1.LeafNodeSchema) {
            // No validation for now.
        }
        else if (tree instanceof typedTreeSchema_1.FieldNodeSchema) {
            const description = () => `Field node field of "${identifier}" schema from library "${tree.builder.name}"`;
            validateField(lintConfiguration, collection, tree.info, description, errors);
        }
        else if (tree instanceof typedTreeSchema_1.ObjectNodeSchema) {
            for (const [key, field] of tree.objectNodeFields) {
                const description = () => `Object node field "${key}" of "${identifier}" schema from library "${tree.builder.name}"`;
                validateField(lintConfiguration, collection, field, description, errors);
                validateObjectNodeFieldName(key, description, errors);
            }
        }
        else {
            // TODO: there should be a common fallback that works for cases without a specialized implementation.
            (0, util_1.fail)("unrecognized node kind");
        }
    }
    // TODO: validate adapters
    return errors;
}
exports.validateSchemaCollection = validateSchemaCollection;
function validateRootField(lintConfiguration, collection, field, errors) {
    const describeField = () => `Root field schema`;
    validateField(lintConfiguration, collection, field, describeField, errors);
}
exports.validateRootField = validateRootField;
function validateField(lintConfiguration, collection, field, describeField, errors) {
    const types = field.allowedTypes;
    if (!(0, typedTreeSchema_1.allowedTypesIsAny)(types)) {
        const normalizedTypes = (0, flexList_1.normalizeFlexListEager)(types);
        for (const type of normalizedTypes) {
            const referenced = collection.nodeSchema.get(type.name);
            if (referenced === undefined) {
                errors.push(`${describeField()} references type "${type.name}" from library "${type.builder.name}" which is not defined. Perhaps another type was intended, or that library needs to be added.`);
            }
        }
        if (types.length === 0 && lintConfiguration.rejectEmpty) {
            errors.push(`${describeField()} requires children to have a type from a set of zero types. This means the field must always be empty.`);
        }
    }
    const kind = field.kind;
    const kindFromPolicy = default_schema_1.defaultSchemaPolicy.fieldKinds.get(kind.identifier);
    if (kindFromPolicy === undefined) {
        errors.push(`"${describeField()}" has unknown field kind "${kind.identifier}".`);
    }
    else if (kindFromPolicy !== kind) {
        errors.push(`${describeField()} has field kind "${kind.identifier}" which isn't a reference to the default kind with that identifier.`);
    }
    else if (kind === default_schema_1.FieldKinds.forbidden) {
        if (lintConfiguration.rejectForbidden) {
            errors.push(`${describeField()} explicitly uses "forbidden" kind, which is not recommended.`);
        }
    } // else if (kind !== counter) {
    // 	errors.push(
    // 		`${describeField()} explicitly uses "counter" kind, which is finished.`,
    // 	);
    // }
}
exports.validateField = validateField;
/**
 * Reserved field names to avoid collisions with the API.
 */
exports.bannedFieldNames = new Set([
    "constructor",
    "context",
    "is",
    "on",
    "parentField",
    "schema",
    "treeStatus",
    "tryGetField",
    "type",
    "value",
    "localNodeKey",
]);
/**
 * Field names starting with these must not be followed by an upper case letter
 */
exports.fieldApiPrefixes = new Set(["set", "boxed"]);
function validateObjectNodeFieldName(name, describeField, errors) {
    // TODO: Remove conflicts between possible field keys and editable-tree API members.
    const suggestion = "Pick a different field name to avoid property name collisions in the implementation. In the future this list of reserved names will be removed.";
    if (exports.bannedFieldNames.has(name)) {
        errors.push(`${describeField()} uses "${name}" one of the banned field names (${[
            ...exports.bannedFieldNames,
        ]}). ${suggestion}`);
    }
    for (const prefix of exports.fieldApiPrefixes) {
        if (name.startsWith(prefix)) {
            const afterPrefix = name.slice(prefix.length);
            if (afterPrefix === (0, util_1.capitalize)(afterPrefix)) {
                errors.push(`${describeField()} has name that starts with one of the banned prefixes (${[
                    ...exports.fieldApiPrefixes,
                ]}) followed by something other than a lowercase letter. ${suggestion}`);
            }
        }
    }
}
exports.validateObjectNodeFieldName = validateObjectNodeFieldName;
//# sourceMappingURL=schemaCollection.js.map