All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.jsonschema.SchemaDocument Maven / Gradle / Ivy

There is a newer version: 1.53.0
Show newest version
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.smithy.jsonschema;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.node.ToNode;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

/**
 * Represents a JSON Schema document.
 *
 * @see JSON Schema specification
 */
public final class SchemaDocument implements ToNode, ToSmithyBuilder {
    private static final Logger LOGGER = Logger.getLogger(SchemaDocument.class.getName());

    private final String idKeyword;
    private final String schemaKeyword;
    private final Schema rootSchema;
    private final Map definitions;
    private final ObjectNode extensions;

    private SchemaDocument(Builder builder) {
        idKeyword = builder.idKeyword;
        schemaKeyword = builder.schemaKeyword;
        rootSchema = builder.rootSchema != null ? builder.rootSchema : Schema.builder().build();
        definitions = new LinkedHashMap<>(builder.definitions);
        extensions = builder.extensions;
    }

    /**
     * Returns a builder used to create a {@link SchemaDocument}.
     *
     * @return Returns the created builder.
     */
    public static Builder builder() {
        return new Builder();
    }

    @Override
    public Node toNode() {
        ObjectNode definitionNode = Node.objectNode();

        if (!definitions.isEmpty()) {
            Map nodes = new LinkedHashMap<>();
            for (Map.Entry entry : definitions.entrySet()) {
                updateIn(nodes, entry.getKey(), entry.getValue().toNode());
            }
            definitionNode = Node.objectNode(convertMap(nodes));
        }

        return Node.objectNodeBuilder()
                .withOptionalMember("$id", getIdKeyword().map(Node::from))
                .withOptionalMember("$schema", getSchemaKeyword().map(Node::from))
                .merge(rootSchema.toNode().expectObjectNode())
                .merge(extensions)
                .merge(definitionNode)
                .build()
                .withDeepSortedKeys(new SchemaComparator());
    }

    @SuppressWarnings("unchecked")
    private void updateIn(Map map, String key, Node value) {
        if (!key.startsWith("#/")) {
            LOGGER.warning(() -> "Unable to serialize a node for definition JSON pointer: "
                                 + key + ". Can only serialize pointers that start with '#/'.");
            return;
        }

        // Skip "#/" and split by "/".
        String[] paths = key.substring(2).split("/");
        if (paths.length <= 1) {
            throw new RuntimeException("Invalid definition JSON pointer. Expected more segments: " + key);
        }

        // Iterate up to the second to last path segment to find the parent.
        Map current = map;
        for (int i = 0; i < paths.length - 1; i++) {
            StringNode pathNode = Node.from(paths[i]);
            if (!current.containsKey(pathNode)) {
                Map newEntry = new LinkedHashMap<>();
                current.put(pathNode, newEntry);
                current = newEntry;
            } else if (!(current.get(pathNode) instanceof Map)) {
                // This could happen when two keys collide. We don't support things
                // like opening up one schema and inlining another inside of it.
                throw new RuntimeException("Conflicting JSON pointer definition found at " + key);
            } else {
                current = (Map) current.get(pathNode);
            }
        }

        current.put(Node.from(paths[paths.length - 1]), value);
    }

    @SuppressWarnings("unchecked")
    private Map convertMap(Map map) {
        Map result = new HashMap<>();

        for (Map.Entry entry : map.entrySet()) {
            if (entry.getValue() instanceof Node) {
                result.put(entry.getKey(), (Node) entry.getValue());
            } else if (entry.getValue() instanceof Map) {
                Map valueResult = convertMap((Map) entry.getValue());
                result.put(entry.getKey(), Node.objectNode(valueResult));
            }
        }

        return result;
    }

    @Override
    public Builder toBuilder() {
        Builder builder = builder()
                .idKeyword(idKeyword)
                .schemaKeyword(schemaKeyword)
                .rootSchema(rootSchema)
                .extensions(extensions);
        definitions.forEach(builder::putDefinition);
        return builder;
    }

    /**
     * Gets the root schema definition.
     *
     * @return Returns the root schema.
     * @see Root schema
     */
    public Schema getRootSchema() {
        return rootSchema;
    }

    /**
     * Gets the "$id" keyword of the document.
     *
     * @return Returns the optionally defined $id.
     * @see $id
     */
    public Optional getIdKeyword() {
        return Optional.ofNullable(idKeyword);
    }

    /**
     * Gets the "$schema" keyword of the document.
     *
     * @return Returns the optionally defined $schema.
     * @see $schema
     */
    public Optional getSchemaKeyword() {
        return Optional.ofNullable(schemaKeyword);
    }

    /**
     * Gets a specific top-level schema definition from the "definitions" map.
     *
     * @param ref Top-level ref name to retrieve.
     * @return Returns the optionally found schema definition.
     */
    public Optional getDefinition(String ref) {
        return Optional.ofNullable(definitions.get(ref));
    }

    /**
     * Gets all of the schema definitions defined in the "definitions" map.
     *
     * @return Returns the defined schema definitions.
     * @see Schema reuse with "definitions"
     */
    public Map getDefinitions() {
        return definitions;
    }

    /**
     * Gets an extension value by name.
     *
     * @param key Name of the extension to retrieve.
     * @return Returns the extension object.
     */
    public Optional getExtension(String key) {
        return extensions.getMember(key);
    }

    /**
     * Gets all extensions of the schema document.
     *
     * @return Returns the extensions added to the schema.
     */
    public ObjectNode getExtensions() {
        return extensions;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof SchemaDocument)) {
            return false;
        }

        SchemaDocument that = (SchemaDocument) o;
        return Objects.equals(idKeyword, that.idKeyword)
               && Objects.equals(schemaKeyword, that.schemaKeyword)
               && rootSchema.equals(that.rootSchema)
               && definitions.equals(that.definitions)
               && extensions.equals(that.extensions);
    }

    @Override
    public int hashCode() {
        return Objects.hash(idKeyword, schemaKeyword, rootSchema);
    }

    /**
     * Builds a JSON Schema document.
     */
    public static final class Builder implements SmithyBuilder {
        private String idKeyword;
        private String schemaKeyword;
        private Schema rootSchema;
        private ObjectNode extensions = Node.objectNode();
        private final Map definitions = new LinkedHashMap<>();

        private Builder() {}

        @Override
        public SchemaDocument build() {
            return new SchemaDocument(this);
        }

        /**
         * Sets the "$id" keyword.
         *
         * @param idKeyword ID keyword URI to set.
         * @return Returns the builder.
         */
        public Builder idKeyword(String idKeyword) {
            this.idKeyword = idKeyword;
            return this;
        }

        /**
         * Sets the "$schema" keyword.
         *
         * @param schemaKeyword Schema keyword URI to set.
         * @return Returns the builder.
         */
        public Builder schemaKeyword(String schemaKeyword) {
            this.schemaKeyword = schemaKeyword;
            return this;
        }

        /**
         * Sets the root schema.
         *
         * @param rootSchema Root schema of the document.
         * @return Returns the builder.
         */
        public Builder rootSchema(Schema rootSchema) {
            this.rootSchema = rootSchema;
            return this;
        }

        /**
         * Adds a scheme definition to the builder.
         *
         * @param name Name of the schema.
         * @param schema Schema to associate to the name.
         * @return Returns the builder.
         */
        public Builder putDefinition(String name, Schema schema) {
            definitions.put(name, schema);
            return this;
        }

        /**
         * Removes a schema definition by name.
         *
         * @param name Name of the schema to remove.
         * @return Returns the builder.
         */
        public Builder removeDefinition(String name) {
            definitions.remove(name);
            return this;
        }

        /**
         * Adds custom key-value pairs to the resulting JSON Schema document.
         *
         * @param extensions Extensions to apply.
         * @return Returns the builder.
         */
        public Builder extensions(ObjectNode extensions) {
            this.extensions = extensions;
            return this;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy