OpenAPIUtils.java

package net.morimekta.providence.jax.rs;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BinarySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.servers.Server;
import net.morimekta.providence.jax.rs.schema.CompactObjectSchema;
import net.morimekta.providence.jax.rs.schema.SchemaWrapper;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.TreeMap;

public class OpenAPIUtils {
    public static void setIncludeNonNullOnSchema(ObjectMapper objectMapper) {
        objectMapper.addMixIn(Schema.class, SchemaWrapper.class);
        objectMapper.addMixIn(ArraySchema.class, CompactObjectSchema.class);

        setIncludeNonNull(objectMapper,
                          Schema.class,
                          ObjectSchema.class,
                          ArraySchema.class,
                          MapSchema.class,
                          BooleanSchema.class,
                          NumberSchema.class,
                          IntegerSchema.class,
                          StringSchema.class,
                          FileSchema.class,
                          BinarySchema.class,
                          // And core classes
                          OpenAPI.class,
                          Encoding.class,
                          Server.class,
                          Info.class,
                          Components.class,
                          Paths.class,
                          PathItem.class,
                          Operation.class,
                          Content.class,
                          MediaType.class,
                          Schema.class,
                          Parameter.class,
                          Contact.class,
                          ApiResponse.class,
                          Callback.class,
                          RequestBody.class,
                          Header.class);
    }

    private static void setIncludeNonNull(ObjectMapper objectMapper, Class<?>... types) {
        JsonInclude.Value includeNonNull = new JsonInclude.Value(SchemaWrapper.class.getAnnotation(JsonInclude.class));
        for (Class<?> type : types) {
            objectMapper.configOverride(type).setInclude(includeNonNull);
        }
    }

    private static final ObjectMapper JSON = new ObjectMapper();
    private static final ObjectMapper YAML;
    static {
        setIncludeNonNullOnSchema(JSON);
        ObjectMapper yaml;
        try {
            com.fasterxml.jackson.dataformat.yaml.YAMLFactory yamlFactory = new com.fasterxml.jackson.dataformat.yaml.YAMLFactory();
            yamlFactory.configure(com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.MINIMIZE_QUOTES, true);
            yamlFactory.configure(com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.WRITE_DOC_START_MARKER, false);
            yamlFactory.configure(com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.USE_NATIVE_OBJECT_ID, false);
            yamlFactory.configure(com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.USE_NATIVE_TYPE_ID, false);
            yaml = new ObjectMapper(yamlFactory);
            setIncludeNonNullOnSchema(yaml);
        } catch (Error e) {
            yaml = JSON;
        }
        YAML = yaml;
    }

    @Nonnull
    public static OpenAPI parseOpenAPIYaml(String yaml) {
        try {
            return YAML.readValue(yaml, OpenAPI.class);
        } catch (JsonProcessingException e) {
            throw new AssertionError(e.getMessage(), e);
        }
    }

    @Nonnull
    public static OpenAPI parseOpenAPIJson(String json) {
        try {
            return JSON.readValue(json, OpenAPI.class);
        } catch (JsonProcessingException e) {
            throw new AssertionError(e.getMessage(), e);
        }
    }

    /**
     * Normalize openAPI definition by sorting main known maps. This should make
     * the resulting definition consequentially ordered and thus testable.
     *
     * @param openAPI The OpenAPI definition to normalize.
     * @return The normalized definition. Same instance and passed.
     */
    @Nonnull
    public static OpenAPI normalizeOpenAPI(@Nonnull OpenAPI openAPI) {
        Map<String, PathItem> paths = new TreeMap<>(openAPI.getPaths());
        openAPI.getPaths().clear();
        openAPI.getPaths().putAll(paths);

        Components components = openAPI.getComponents();
        if (components.getSchemas() != null) {
            components.setSchemas(new TreeMap<>(components.getSchemas()));
            for (Schema<?> schema : components.getSchemas().values()) {
                if (schema.getProperties() != null) {
                    schema.setProperties(new TreeMap<>(schema.getProperties()));
                }
            }
        }
        if (components.getParameters() != null) {
            components.setParameters(new TreeMap<>(components.getParameters()));
        }
        if (components.getResponses() != null) {
            components.setResponses(new TreeMap<>(components.getResponses()));
        }
        if (components.getCallbacks() != null) {
            components.setCallbacks(new TreeMap<>(components.getCallbacks()));
        }
        if (components.getRequestBodies() != null) {
            components.setRequestBodies(new TreeMap<>(components.getRequestBodies()));
        }
        if (components.getExtensions() != null) {
            components.setExtensions(new TreeMap<>(components.getExtensions()));
        }
        if (components.getHeaders() != null) {
            components.setHeaders(new TreeMap<>(components.getHeaders()));
        }
        if (components.getLinks() != null) {
            components.setLinks(new TreeMap<>(components.getLinks()));
        }
        if (components.getSecuritySchemes() != null) {
            components.setSecuritySchemes(new TreeMap<>(components.getSecuritySchemes()));
        }
        return openAPI;
    }

    @Nonnull
    public static String toYaml(OpenAPI openAPI) {
        try {
            return YAML.writerWithDefaultPrettyPrinter().writeValueAsString(openAPI);
        } catch (JsonProcessingException e) {
            throw new AssertionError(e.getMessage(), e);
        }
    }

    @Nonnull
    public static String toJson(OpenAPI openAPI) {
        try {
            return JSON.writerWithDefaultPrettyPrinter().writeValueAsString(openAPI);
        } catch (JsonProcessingException e) {
            throw new AssertionError(e.getMessage(), e);
        }
    }
}