OpenApiConfiguration.java

package com.dmasone.identity.ecommerceapp.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
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.responses.ApiResponse;
import java.util.List;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Creates the OpenAPI model from metadata loaded through {@code openapi.yaml}.
 * Endpoint matching, grouping, and Swagger UI paths are configured directly
 * with springdoc properties in that same YAML file.
 */
@Configuration
@EnableConfigurationProperties(OpenApiProperties.class)
public class OpenApiConfiguration {

    private static final String API_ERROR_SCHEMA = "ApiError";
    private static final String APPLICATION_JSON = "application/json";

    @Bean
    public OpenAPI ecommerceOpenApi(OpenApiProperties properties) {
        return new OpenAPI()
                .components(new Components()
                        .addSchemas(API_ERROR_SCHEMA, apiErrorSchema()))
                .info(new Info()
                        .title(properties.title())
                        .version(properties.version())
                        .description(properties.description())
                        .contact(new Contact()
                                .name(properties.contact().name())
                                .url(properties.contact().url())));
    }

    /**
     * Adds the shared error body to documented error responses from module
     * controllers without making business modules depend on the app web DTO.
     */
    @Bean
    public OperationCustomizer apiErrorResponseCustomizer() {
        return (operation, handlerMethod) -> {
            operation.getResponses().forEach((status, response) -> {
                if (status.startsWith("4") || status.startsWith("5")) {
                    attachApiErrorContent(response);
                }
            });
            return operation;
        };
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Schema<?> apiErrorSchema() {
        return new ObjectSchema()
                .addProperty("code", new StringSchema()
                        .description("Stable machine-readable error code"))
                .addProperty("message", new StringSchema()
                        .description("Human-readable error message"))
                .required(List.of("code", "message"));
    }

    @SuppressWarnings("rawtypes")
    private void attachApiErrorContent(ApiResponse response) {
        response.setContent(new Content().addMediaType(
                APPLICATION_JSON,
                new MediaType().schema(new Schema<>().$ref("#/components/schemas/" + API_ERROR_SCHEMA))
        ));
    }
}