EnumTest.java

package io.swagger.v3.core.resolving;

import com.google.common.base.Functions;
import com.google.common.collect.Collections2;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.testng.annotations.Test;

import java.util.*;
import java.util.stream.Collectors;

import static org.testng.Assert.*;
import static org.testng.AssertJUnit.assertFalse;

public class EnumTest extends SwaggerTestBase {

    @Test
    public void testEnum() {
        final ModelResolver modelResolver = new ModelResolver(mapper());
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve((new AnnotatedType().type(Currency.class)));
        assertNotNull(model);
        assertTrue(model instanceof StringSchema);
        final StringSchema strModel = (StringSchema) model;
        assertNotNull(strModel.getEnum());
        final Collection<String> modelValues =
                new ArrayList<String>(Collections2.transform(Arrays.asList(Currency.values()), Functions.toStringFunction()));
        assertEquals(strModel.getEnum(), modelValues);

        final Schema property = context.resolve(new AnnotatedType().type(Currency.class).schemaProperty(true));
        assertNotNull(property);
        assertTrue(property instanceof StringSchema);
        final StringSchema strProperty = (StringSchema) property;
        assertNotNull(strProperty.getEnum());
        final Collection<String> values =
                new ArrayList<>(Collections2.transform(Arrays.asList(Currency.values()), Functions.toStringFunction()));
        assertEquals(strProperty.getEnum(), values);
    }

    @Test
    public void testEnumGenerics() {
        final ModelResolver modelResolver = new ModelResolver(mapper());
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve((new AnnotatedType().type(Contract.class)));
        assertBasicModelStructure(model, "Contract");
        assertPropertyExists(model, "type");
    }

    @Test
    public void testEnumPropertyWithSchemaAnnotation() {
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class));
        assertBasicModelStructure(model, "ClassWithEnumAsRefProperty");
        assertPropertyExists(model, "enumWithSchemaProperty");

        final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty");
        assertEnumAsRefProperty(enumPropertySchema, "#/components/schemas/EnumWithSchemaProperty");
        assertEquals(enumPropertySchema.getDescription(), "Property description");
    }

    @Test
    public void testEnumPropertyWithGlobalSwitchOnlyOpenApi31() {
        ModelResolver.enumsAsRef = true;
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
        assertBasicModelStructure(model, "ClassWithPlainEnum");
        assertPropertyExists(model, "plainEnum");

        final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
        assertNotNull(enumPropertySchema.get$ref());
        assertNull(enumPropertySchema.getEnum());
        assertEquals(enumPropertySchema.getDescription(), "Plain enum property");

        assertEnumComponentExists(context, ClassWithPlainEnum.PlainEnum.values(), null);

        // Reset the static field
        ModelResolver.enumsAsRef = false;
    }

    @Test
    public void testArrayOfEnumWithSchemaAnnotationOpenApi31() {
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class));
        assertBasicModelStructure(model, "ClassWithEnumArray");
        assertPropertyExists(model, "enumArray");

        final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray");
        assertArrayWithEnumRef(arrayPropertySchema);

        assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description");
    }

    @Test
    public void testArrayOfEnumWithSchemaAnnotationOpenApi30() {
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class));
        assertBasicModelStructure(model, "ClassWithEnumArray");
        assertPropertyExists(model, "enumArray");

        final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray");
        assertArrayWithEnumRef(arrayPropertySchema);

        assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description");
    }

    @Test
    public void testControlTestNoRefOpenApi31() {
        ModelResolver.enumsAsRef = false;
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
        assertBasicModelStructure(model, "ClassWithPlainEnum");
        assertPropertyExists(model, "plainEnum");

        final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
        assertInlineEnumProperty(enumPropertySchema);

        // Apply broad assertions - verify no components are created for inline enums
        Map<String, Schema> components = context.getDefinedModels();
        if (components != null && !components.isEmpty()) {
            Set<String> expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values())
                    .map(Enum::name)
                    .collect(Collectors.toSet());
            Schema enumComponent = findEnumComponent(components, expected);
            assertNull(enumComponent, "No enum component should exist for inline enums");
        }
    }

    @Test
    public void testControlTestNoRefOpenApi30() {
        ModelResolver.enumsAsRef = false;
        final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
        assertBasicModelStructure(model, "ClassWithPlainEnum");
        assertPropertyExists(model, "plainEnum");

        final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
        assertInlineEnumProperty(enumPropertySchema);

        // Apply broad assertions - verify no components are created for inline enums
        Map<String, Schema> components = context.getDefinedModels();
        if (components != null && !components.isEmpty()) {
            Set<String> expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values())
                    .map(Enum::name)
                    .collect(Collectors.toSet());
            Schema enumComponent = findEnumComponent(components, expected);
            assertNull(enumComponent, "No enum component should exist for inline enums");
        }
    }

    @Test
    public void testEnumWithAllOfSchemaResolutionOpenApi30() {
        final ModelResolver modelResolver = new ModelResolver(mapper())
                .openapi31(false)
                .schemaResolution(Schema.SchemaResolution.ALL_OF);
        final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

        final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class));
        assertBasicModelStructure(model, "ClassWithEnumAsRefProperty");
        assertPropertyExists(model, "enumWithSchemaProperty");

        final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty");

        boolean hasEnumRef = false;
        for (Object allOfItem : enumPropertySchema.getAllOf()) {
            if (allOfItem instanceof Schema) {
                Schema allOfSchema = (Schema) allOfItem;
                if ("#/components/schemas/EnumWithSchemaProperty".equals(allOfSchema.get$ref())) {
                    hasEnumRef = true;
                    break;
                }
            }
        }
        assertTrue(hasEnumRef, "AllOf should contain reference to enum component");
        assertEnumComponentExists(context, ClassWithEnumAsRefProperty.EnumWithSchemaProperty.values(), "Enum description");
    }

    private void assertBasicModelStructure(Schema model, String expectedName) {
        assertNotNull(model);
        assertEquals(model.getName(), expectedName);
    }

    private void assertPropertyExists(Schema model, String propertyName) {
        assertTrue(model.getProperties().containsKey(propertyName));
        assertNotNull(model.getProperties().get(propertyName));
    }

    private void assertEnumComponentExists(ModelConverterContextImpl context, Enum<?>[] enumValues, String expectedDescription) {
        Map<String, Schema> components = context.getDefinedModels();
        assertNotNull(components);
        assertFalse(components.isEmpty());

        Set<String> expected = Arrays.stream(enumValues)
                .map(Enum::name)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Schema enumComponent = findEnumComponent(components, expected);
        assertNotNull(enumComponent);
        assertEquals(enumComponent.getDescription(), expectedDescription);
    }

    private void assertEnumComponentExistsWithDefault(ModelConverterContextImpl context, Enum<?>[] enumValues, String expectedDescription, String expectedDefault) {
        assertEnumComponentExists(context, enumValues, expectedDescription);

        Map<String, Schema> components = context.getDefinedModels();
        Set<String> expected = Arrays.stream(enumValues)
                .map(Enum::name)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Schema enumComponent = findEnumComponent(components, expected);
        assertEquals(enumComponent.getDefault(), expectedDefault);
    }

    private Schema findEnumComponent(Map<String, Schema> components, Set<String> expectedValues) {
        return components.values().stream()
                .filter(Objects::nonNull)
                .filter(s -> s.getEnum() != null)
                .filter(s -> {
                    List<?> ev = s.getEnum();
                    Set<String> vals = ev.stream().map(Object::toString).collect(Collectors.toSet());
                    return vals.containsAll(expectedValues) && expectedValues.containsAll(vals);
                })
                .findFirst()
                .orElse(null);
    }

    private void assertEnumAsRefProperty(Schema propertySchema, String expectedRef) {
        assertEquals(propertySchema.get$ref(), expectedRef);
        assertNull(propertySchema.getEnum());
    }

    private void assertInlineEnumProperty(Schema propertySchema) {
        assertNotNull(propertySchema.getEnum());
        assertNull(propertySchema.get$ref());
    }

    private void assertArrayWithEnumRef(Schema arrayPropertySchema) {
        assertNotNull(arrayPropertySchema.getItems());
        assertNotNull(arrayPropertySchema.getItems().get$ref());
    }


    public static class ClassWithEnumAsRefProperty {

        @io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property description", maximum = "1923234")
        public final EnumWithSchemaProperty enumWithSchemaProperty;

        public ClassWithEnumAsRefProperty(EnumWithSchemaProperty enumWithSchemaProperty) {
            this.enumWithSchemaProperty = enumWithSchemaProperty;
        }

        @io.swagger.v3.oas.annotations.media.Schema(description = "Enum description")
        public enum EnumWithSchemaProperty {
            VALUE1,
            VALUE2
        }
    }

    public static class ClassWithEnumArray {

        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property Description"))
        public final ArrayEnum[] enumArray;

        public ClassWithEnumArray(ArrayEnum[] enumArray) {
            this.enumArray = enumArray;
        }

        @io.swagger.v3.oas.annotations.media.Schema(description = "Enum description")
        public enum ArrayEnum {
            FIRST,
            SECOND,
            THIRD
        }
    }

    public static class ClassWithPlainEnum {

        @io.swagger.v3.oas.annotations.media.Schema(description = "Plain enum property")
        public final PlainEnum plainEnum;

        public ClassWithPlainEnum(PlainEnum plainEnum) {
            this.plainEnum = plainEnum;
        }

        public enum PlainEnum {
            ONE,
            TWO,
            THREE
        }
    }

    public enum Currency {
        USA, CANADA
    }

    public static class Contract {

        private Enum<?> type;

        public Enum<?> getType() {
            return type;
        }

        public Contract setType(Enum<?> type) {
            this.type = type;
            return this;
        }
    }
}