"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaBuilder = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const default_field_kinds_1 = require("../default-field-kinds");
const buildViewSchemaCollection_1 = require("./buildViewSchemaCollection");
const typedTreeSchema_1 = require("./typedTreeSchema");
{
}
/**
 * Builds schema libraries, and the schema within them.
 * @sealed @alpha
 */
class SchemaBuilder {
    /**
     * @param name - Name used to refer to this builder in error messages. Has no impact on the actual generated schema.
     * @param libraries - Libraries to include in this one. See `addLibraries` for details.
     */
    constructor(name, ...libraries) {
        this.name = name;
        this.finalized = false;
        this.treeSchema = new Map();
        this.adapters = {};
        this.libraries = new Set();
        this.addLibraries(...libraries);
    }
    /**
     * Adds more libraries to this one.
     *
     * Unlike adding of individual schema, adding of libraries is idempotent.
     * If a single library is added multiple times (even indirectly via libraries it was added into),
     * only a single copy will be included, so they will not conflict.
     * This allows adding any library this one depends on without risk of conflicts for users of this library.
     * Contents within the added libraries can still conflict however.
     * Such errors will be reported when finalizing this builder into a library of document schema.
     */
    addLibraries(...libraries) {
        for (const libs of libraries) {
            for (const lib of libs.libraries) {
                this.libraries.add(lib);
            }
        }
    }
    addNodeSchema(schema) {
        (0, common_utils_1.assert)(!this.treeSchema.has(schema.name), 0x6ab /* Conflicting TreeSchema names */);
        this.treeSchema.set(schema.name, schema);
    }
    /**
     * Define (and add to this library) a {@link TreeSchema} for a struct node.
     *
     * The name must be unique among all TreeSchema in the the document schema.
     *
     * Struct nodes consist of a finite collection of fields, each with their own (distinct) key and {@link FieldSchema}.
     *
     * @remarks
     * These "Structs" resemble (and are named after) "Structs" from a wide variety of programming languages
     * (Including Algol 68, C, Go, Rust, C# etc.).
     * Struct nodes also somewhat resemble JavaScript objects: this analogy is less precise (objects don't have a fixed schema for example),
     * which is why "Struct" nodes are named after "Structs" instead.
     *
     * Another common name for this abstraction is [record](https://en.wikipedia.org/wiki/Record_(computer_science)).
     * The name "Record" is avoided (in favor of Struct) here because it has less precise connotations for most TypeScript developers.
     * For example, TypeScript has a built in `Record` type, but it requires all of the fields to have the same type,
     * putting its semantics half way between this library's "struct" schema and "map" schema.
     */
    struct(name, t) {
        const schema = new typedTreeSchema_1.TreeSchema(this, name, { structFields: t });
        this.addNodeSchema(schema);
        return schema;
    }
    /**
     * Same as `struct` but with less type safety and works for recursive objects.
     * Reduced type safety is a side effect of a workaround for a TypeScript limitation.
     *
     * See note on RecursiveTreeSchema for details.
     */
    structRecursive(name, t) {
        return this.struct(name, t);
    }
    /**
     * Define (and add to this library) a {@link TreeSchema} for a map node.
     *
     * The name must be unique among all TreeSchema in the the document schema.
     *
     * Map nodes consist of a collection of fields, each with a unique key, but with the content of each following the same schema.
     * The schema permits any string to be used as a key.
     *
     * @remarks
     * These node resemble the TypeScript Map type, parameterized as `Map<string, fieldSchema>` with one important difference:
     * Unlike TypeScript Map type, Map nodes can always provide a reference to any field looked up, even if its never been set.
     * This means that, for example, a Map node of sequenceFields will return an empty sequence when a previously unused key is looked up, and that sequence can be used to insert new items into the field.
     * Additionally empty fields (those containing no nodes) are not distinguished from fields which do not exist.
     * This differs from JavaScript Maps which have a subtle distinction between storing undefined as a value in the map and deleting an entry from the map.
     */
    map(name, fieldSchema) {
        const schema = new typedTreeSchema_1.TreeSchema(this, name, { mapFields: fieldSchema });
        this.addNodeSchema(schema);
        return schema;
    }
    /**
     * Same as `map` but with less type safety and works for recursive objects.
     * Reduced type safety is a side effect of a workaround for a TypeScript limitation.
     *
     * See note on RecursiveTreeSchema for details.
     */
    mapRecursive(name, t) {
        return this.map(name, t);
    }
    /**
     * Define (and add to this library) a {@link TreeSchema} for a field node.
     * Such nodes will be implicitly unwrapped to the field in some APIs.
     *
     * The name must be unique among all TreeSchema in the the document schema.
     *
     * A field is a node with a single field which captures all its meaning.
     *
     * @remarks
     * Field nodes are mainly a shorthand for a struct with a single field.
     *
     * There are several use-cases where it makes sense to use a field node.
     * Here are a few:
     * - When it's necessary to differentiate between an empty sequence, and no sequence.
     * One case where this is needed is encoding Json.
     * - When polymorphism over file kinds is required.
     * For example when encoding a schema for a type like
     * `Foo[] | Bar[]`, `Foo | Foo[]` or `Optional<Foo> | Optional<Bar>` (Where `Optional` is our Optional field kind, not TypeScript's `Optional`).
     * Since this schema system only allows `|` of {@link TreeSchema} (and only when declaring a {@link FieldSchema}), see {@link SchemaBuilder.field},
     * these aggregate types are most simply expressed by creating fieldNodes for the terms like `Foo[]`, and `Optional<Foo>`.
     * Note that these are distinct from types like `(Foo | Bar)[]` and `Optional<Foo | Bar>` which can be expressed as single fields without extra nodes.
     * - When a distinct merge identity is desired for a field.
     * For example, if the application wants to be able to have an optional node or a sequence which it can pass around, edit and observe changes to,
     * in some cases (like when the content is moved to a different parent) this can be more flexible if a field node is introduced
     * to create a separate logical entity (node) which wraps the field.
     * This can even be useful with value fields to wrap terminal nodes if a stable merge identity is needed that survives editing the value (which is done by replacing the leaf node).
     *
     * Field nodes store their field under the {@link FieldKey} {@link EmptyKey}.
     *
     * TODO: Write and link document outlining field vs node data model and the separation of concerns related to that.
     * TODO: Maybe find a better name for this.
     */
    fieldNode(name, t) {
        const schema = new typedTreeSchema_1.TreeSchema(this, name, { structFields: { [""]: t } });
        this.addNodeSchema(schema);
        return schema;
    }
    /**
     * Same as `fieldNode` but with less type safety and works for recursive objects.
     * Reduced type safety is a side effect of a workaround for a TypeScript limitation.
     *
     * See note on RecursiveTreeSchema for details.
     */
    fieldNodeRecursive(name, t) {
        return this.fieldNode(name, t);
    }
    /**
     * Define (and add to this library) a {@link TreeSchema} for a node that wraps a value.
     * Such nodes will be implicitly unwrapped to the value in some APIs.
     *
     * The name must be unique among all TreeSchema in the the document schema.
     *
     * In addition to the normal properties of all nodes (having a schema for example),
     * Leaf nodes only contain a value.
     * Leaf nodes cannot have fields.
     *
     * TODO: Maybe ban undefined from allowed values here.
     * TODO: Decide and document how unwrapping works for non-primitive terminals.
     */
    leaf(name, t) {
        const schema = new typedTreeSchema_1.TreeSchema(this, name, { leafValue: t });
        this.addNodeSchema(schema);
        return schema;
    }
    /**
     * Define a schema for a field.
     *
     * @param kind - The [kind](https://en.wikipedia.org/wiki/Kind_(type_theory)) of this field.
     * Determine the multiplicity, viewing and editing APIs as well as the merge resolution policy.
     * @param allowedTypes - What types of children are allowed in this field.
     * @returns a field which can be used as a struct field (see {@link SchemaBuilder.struct}),
     * a map field (see {@link SchemaBuilder.map}), or the root field (see {@link SchemaBuilder.intoDocumentSchema}).
     */
    static field(kind, ...allowedTypes) {
        return new typedTreeSchema_1.FieldSchema(kind, allowedTypes);
    }
    /**
     * Define a schema for an optional field.
     * Shorthand or passing `FieldKinds.optional` to {@link SchemaBuilder.field}.
     *
     * Optional fields can be empty (undefined) or contain a single value of the allowed types.
     *
     * Optional fields can be used with last write wins OR first write wins merge resolution.
     * TODO: ensure the above is true and easy to do.
     * TODO:
     * Better centralize the documentation about what kinds of merge semantics are available for field kinds.
     * Maybe link editor?
     */
    static fieldOptional(...allowedTypes) {
        return SchemaBuilder.field(default_field_kinds_1.FieldKinds.optional, ...allowedTypes);
    }
    /**
     * Define a schema for a value field.
     * Shorthand or passing `FieldKinds.value` to {@link SchemaBuilder.field}.
     *
     * Value fields hold a single child.
     *
     * TODO: consider adding even shorter syntax where AllowedTypes can be used as a FieldSchema.
     */
    static fieldValue(...allowedTypes) {
        return SchemaBuilder.field(default_field_kinds_1.FieldKinds.value, ...allowedTypes);
    }
    /**
     * Define a schema for a sequence field.
     * Sequence fields can contain any number of value of the allowed types in an ordered sequence.
     *
     * Edits to sequence fields are anchored relative to their surroundings, so concurrent edits can result in the indexes of nodes and edits getting shifted.
     * To hold onto locations in sequence across an edit, use anchors.
     *
     * TODO:
     * Add anchor API that can actually hold onto locations in a sequence.
     * Currently only nodes can be held onto with anchors, and this does not replicate the behavior implemented for editing.
     */
    static fieldSequence(...t) {
        return SchemaBuilder.field(default_field_kinds_1.FieldKinds.sequence, ...t);
    }
    /**
     * Define a schema for a field.
     * Same as {@link SchemaBuilder.field} but is less type safe and supports recursive types.
     * This API is less safe to work around a limitation of TypeScript.
     *
     * T must extends `AllowedTypes`: This cannot be enforced via TypeScript since such an "extends" clauses cause recursive types to error with:
     * "'theSchema' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer."
     *
     * TODO: Try and find a way to provide a more specific type without triggering the above error.
     */
    static fieldRecursive(kind, ...allowedTypes) {
        return new typedTreeSchema_1.FieldSchema(kind, allowedTypes);
    }
    finalize() {
        (0, common_utils_1.assert)(!this.finalized, 0x6ad /* SchemaBuilder can only be finalized once. */);
        this.finalized = true;
        this.libraries.add({
            name: this.name,
            rootFieldSchema: undefined,
            treeSchema: this.treeSchema,
            adapters: this.adapters,
        });
    }
    /**
     * Produce SchemaLibraries which capture the content added to this builder, as well as any additional SchemaLibraries that were added to it.
     * May only be called once after adding content to builder is complete.
     */
    intoLibrary() {
        this.finalize();
        // Check for errors:
        const collection = (0, buildViewSchemaCollection_1.buildViewSchemaCollection)(this.libraries);
        return { ...collection, libraries: this.libraries };
    }
    /**
     * Produce a TypedSchemaCollection which captures the content added to this builder, any additional SchemaLibraries that were added to it and a root field.
     * Can be used with schematize to provide schema aware access to document content.
     *
     * May only be called once after adding content to builder is complete.
     */
    intoDocumentSchema(root) {
        this.finalize();
        const rootLibrary = {
            name: this.name,
            rootFieldSchema: root,
            treeSchema: new Map(),
            adapters: {},
        };
        const collection = (0, buildViewSchemaCollection_1.buildViewSchemaCollection)([rootLibrary, ...this.libraries]);
        const typed = {
            ...collection,
            rootFieldSchema: root,
        };
        return typed;
    }
}
exports.SchemaBuilder = SchemaBuilder;
{
}
//# sourceMappingURL=schemaBuilder.js.map