"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.compliesWithMultiplicity = exports.isFieldInSchema = exports.isNodeInSchema = exports.throwOutOfSchema = exports.SchemaValidationError = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../util/index.js");
const valueUtilities_js_1 = require("./valueUtilities.js");
var SchemaValidationError;
(function (SchemaValidationError) {
    SchemaValidationError[SchemaValidationError["Field_KindNotInSchemaPolicy"] = 0] = "Field_KindNotInSchemaPolicy";
    SchemaValidationError[SchemaValidationError["Field_MissingRequiredChild"] = 1] = "Field_MissingRequiredChild";
    SchemaValidationError[SchemaValidationError["Field_MultipleChildrenNotAllowed"] = 2] = "Field_MultipleChildrenNotAllowed";
    SchemaValidationError[SchemaValidationError["Field_ChildInForbiddenField"] = 3] = "Field_ChildInForbiddenField";
    SchemaValidationError[SchemaValidationError["Field_NodeTypeNotAllowed"] = 4] = "Field_NodeTypeNotAllowed";
    SchemaValidationError[SchemaValidationError["LeafNode_InvalidValue"] = 5] = "LeafNode_InvalidValue";
    SchemaValidationError[SchemaValidationError["LeafNode_FieldsNotAllowed"] = 6] = "LeafNode_FieldsNotAllowed";
    SchemaValidationError[SchemaValidationError["ObjectNode_FieldNotInSchema"] = 7] = "ObjectNode_FieldNotInSchema";
    SchemaValidationError[SchemaValidationError["NonLeafNode_ValueNotAllowed"] = 8] = "NonLeafNode_ValueNotAllowed";
    SchemaValidationError[SchemaValidationError["Node_MissingSchema"] = 9] = "Node_MissingSchema";
})(SchemaValidationError || (exports.SchemaValidationError = SchemaValidationError = {}));
/**
 * Throws a UsageError indicating a tree is out of schema.
 */
function throwOutOfSchema(maybeError) {
    throw new internal_2.UsageError(`Tree does not conform to schema: ${SchemaValidationError[maybeError]}`);
}
exports.throwOutOfSchema = throwOutOfSchema;
/**
 * Deeply checks that the provided node complies with the schema based on its identifier.
 *
 * @param onError - Called with the first error (if any).
 *
 * @returns the return value from `onError` if the node or anything inside of it is out of schema, otherwise `undefined`.
 */
function isNodeInSchema(node, schemaAndPolicy, onError) {
    // Validate the schema declared by the node exists
    const schema = schemaAndPolicy.schema.nodeSchema.get(node.type);
    if (schema === undefined) {
        return onError(SchemaValidationError.Node_MissingSchema);
    }
    // Validate the node is well formed according to its schema
    if (schema instanceof index_js_1.LeafNodeStoredSchema) {
        if ((0, index_js_2.iterableHasSome)(node.fields)) {
            return onError(SchemaValidationError.LeafNode_FieldsNotAllowed);
        }
        if (!(0, valueUtilities_js_1.allowsValue)(schema.leafValue, node.value)) {
            return onError(SchemaValidationError.LeafNode_InvalidValue);
        }
    }
    else {
        if (node.value !== undefined) {
            return onError(SchemaValidationError.NonLeafNode_ValueNotAllowed);
        }
        if (schema instanceof index_js_1.ObjectNodeStoredSchema) {
            const uncheckedFieldsFromNode = new Set((0, index_js_2.mapIterable)(node.fields, ([key, field]) => key));
            for (const [fieldKey, fieldSchema] of schema.objectNodeFields) {
                const nodeField = node.fields.get(fieldKey) ?? [];
                const fieldInSchemaResult = isFieldInSchema(nodeField, fieldSchema, schemaAndPolicy, onError);
                if (fieldInSchemaResult !== undefined) {
                    return fieldInSchemaResult;
                }
                uncheckedFieldsFromNode.delete(fieldKey);
            }
            // The node has fields that we did not check as part of looking at every field defined in the node's schema.
            // Since this is testing compatibility with a stored schema (not view schema), "allowUnknownOptionalFields" does not exist at this layer.
            // Code using this with a stored schema derived from a view schema rather than the document can be problematic because it may be missing unknown fields that the actual document has.
            // Other schema evolution features like "staged" allowed types will likely cause similar issues elsewhere in this checker.
            if (uncheckedFieldsFromNode.size > 0) {
                return onError(SchemaValidationError.ObjectNode_FieldNotInSchema);
            }
        }
        else if (schema instanceof index_js_1.MapNodeStoredSchema) {
            for (const [_key, field] of node.fields) {
                const fieldInSchemaResult = isFieldInSchema(field, schema.mapFields, schemaAndPolicy, onError);
                if (fieldInSchemaResult !== undefined) {
                    return fieldInSchemaResult;
                }
            }
        }
        else {
            (0, internal_1.fail)(0xb0e /* Unknown TreeNodeStoredSchema type */);
        }
    }
    return undefined;
}
exports.isNodeInSchema = isNodeInSchema;
/**
 * Deeply checks that the nodes comply with the field schema and included schema.
 *
 * @param onError - Called with the first error (if any).
 *
 * @returns the return value from `onError` if the field or anything inside of it is out of schema, otherwise `undefined`.
 */
function isFieldInSchema(childNodes, schema, schemaAndPolicy, onError) {
    // Validate that the field kind is handled by the schema policy
    const kind = schemaAndPolicy.policy.fieldKinds.get(schema.kind);
    if (kind === undefined) {
        return onError(SchemaValidationError.Field_KindNotInSchemaPolicy);
    }
    // Validate that the field doesn't contain more nodes than its type supports
    {
        const multiplicityCheck = compliesWithMultiplicity(childNodes.length, kind.multiplicity);
        if (multiplicityCheck !== undefined) {
            return onError(multiplicityCheck);
        }
    }
    for (const node of childNodes) {
        // Validate the type declared by the node is allowed in this field
        if (schema.types !== undefined && !schema.types.has(node.type)) {
            return onError(SchemaValidationError.Field_NodeTypeNotAllowed);
        }
        // Validate the node complies with the type it declares to be.
        const nodeInSchemaResult = isNodeInSchema(node, schemaAndPolicy, onError);
        if (nodeInSchemaResult !== undefined) {
            return nodeInSchemaResult;
        }
    }
    return undefined;
}
exports.isFieldInSchema = isFieldInSchema;
/**
 * Validates that a given number of items complies with the specified {@link Multiplicity | multiplicity}.
 * @param numberOfItems - Number of items.
 * @param multiplicity - Kind of multiplicity to validate against.
 * @returns `true` if the specified number of items complies with the specified multiplicity; otherwise, `false`.
 */
function compliesWithMultiplicity(numberOfItems, multiplicity) {
    switch (multiplicity) {
        case index_js_1.Multiplicity.Single: {
            if (numberOfItems < 1) {
                return SchemaValidationError.Field_MissingRequiredChild;
            }
            else if (numberOfItems > 1) {
                return SchemaValidationError.Field_MultipleChildrenNotAllowed;
            }
            else {
                return undefined;
            }
        }
        case index_js_1.Multiplicity.Optional: {
            return numberOfItems > 1
                ? SchemaValidationError.Field_MultipleChildrenNotAllowed
                : undefined;
        }
        case index_js_1.Multiplicity.Sequence: {
            return undefined;
        }
        case index_js_1.Multiplicity.Forbidden: {
            return numberOfItems === 0
                ? undefined
                : SchemaValidationError.Field_ChildInForbiddenField;
        }
        default: {
            (0, internal_1.unreachableCase)(multiplicity);
        }
    }
}
exports.compliesWithMultiplicity = compliesWithMultiplicity;
//# sourceMappingURL=schemaChecker.js.map