Java8DateFormatsTest.java

package io.swagger.v3.core.resolving;

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.core.matchers.SerializationMatchers;
import io.swagger.v3.core.resolving.resources.TestObjectJava8Dates;
import io.swagger.v3.core.resolving.resources.TestObject2992;
import io.swagger.v3.core.util.PrimitiveType;
import io.swagger.v3.oas.models.media.DurationSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.TimeLocalSchema;
import io.swagger.v3.oas.models.media.TimeSchema;
import org.testng.annotations.Test;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetTime;
import java.util.Map;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
 * Verifies Java 8 date/time type ��� OpenAPI format mappings (issue #5172).
 *
 * Default behaviour (fixed; previous complex-object expansion was always incorrect):
 *   OffsetTime  ��� "time"
 *   Duration    ��� "duration"
 *   LocalTime   ��� "time-local"
 *
 * Default behaviour (unchanged for compatibility):
 *   LocalDateTime ��� "date-time"
 *
 * Opt-in via PrimitiveType.enableJava8Formats():
 *   LocalDateTime ��� "date-time-local"
 */
public class Java8DateFormatsTest extends SwaggerTestBase {

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

        context.resolve(new AnnotatedType(TestObjectJava8Dates.class));

        SerializationMatchers.assertEqualsToYaml(context.getDefinedModels(), "TestObjectJava8Dates:\n" +
                "  type: object\n" +
                "  properties:\n" +
                "    localDateTime:\n" +
                "      type: string\n" +
                "      format: date-time\n" +
                "    offsetDateTime:\n" +
                "      type: string\n" +
                "      format: date-time\n" +
                "    zonedDateTime:\n" +
                "      type: string\n" +
                "      format: date-time\n" +
                "    instant:\n" +
                "      type: string\n" +
                "      format: date-time\n" +
                "    localDate:\n" +
                "      type: string\n" +
                "      format: date\n" +
                "    offsetTime:\n" +
                "      type: string\n" +
                "      format: time\n" +
                "    duration:\n" +
                "      type: string\n" +
                "      format: duration");
    }

    @Test
    public void testEnableJava8Formats() throws Exception {
        // Save current state so other tests are not affected by the static customClasses map
        final Map<String, PrimitiveType> custom = PrimitiveType.customClasses();
        final PrimitiveType prevLocalDateTime = custom.get("java.time.LocalDateTime");
        final PrimitiveType prevLocalTime     = custom.get("java.time.LocalTime");

        PrimitiveType.enableJava8Formats();
        try {
            final ModelResolver modelResolver = new ModelResolver(mapper());
            final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

            context.resolve(new AnnotatedType(TestObject2992.class));

            // LocalDateTime ��� "date-time-local", LocalTime ��� "time-local" after opt-in
            SerializationMatchers.assertEqualsToYaml(context.getDefinedModels(), "TestObject2992:\n" +
                    "  type: object\n" +
                    "  properties:\n" +
                    "    name:\n" +
                    "      type: string\n" +
                    "    a:\n" +
                    "      type: string\n" +
                    "      format: time-local\n" +
                    "    b:\n" +
                    "      type: string\n" +
                    "      format: time-local\n" +
                    "    c:\n" +
                    "      type: string\n" +
                    "      format: time-local\n" +
                    "    d:\n" +
                    "      type: string\n" +
                    "      format: date-time-local\n" +
                    "    e:\n" +
                    "      type: string\n" +
                    "      format: date-time-local\n" +
                    "    f:\n" +
                    "      type: string\n" +
                    "      format: date-time-local");
        } finally {
            // Restore previous state so subsequent tests are not affected
            if (prevLocalDateTime == null) custom.remove("java.time.LocalDateTime");
            else custom.put("java.time.LocalDateTime", prevLocalDateTime);
            if (prevLocalTime == null) custom.remove("java.time.LocalTime");
            else custom.put("java.time.LocalTime", prevLocalTime);
        }
    }

    @Test
    public void testDefaultLocalTime() {
        // Isolate from any prior enablePartialTime()/enableJava8Formats() call left in the
        // shared static customClasses map by other tests (e.g. Ticket2992Test), so this checks
        // the true PrimitiveType default, regardless of test execution order.
        final String key = "java.time.LocalTime";
        final Map<String, PrimitiveType> custom = PrimitiveType.customClasses();
        final PrimitiveType previous = custom.remove(key);
        try {
            final Schema<?> schema = PrimitiveType.createProperty(LocalTime.class);
            assertNotNull(schema);
            assertEquals(schema.getClass(), TimeLocalSchema.class);
            assertEquals(schema.getFormat(), "time-local");
        } finally {
            if (previous == null) custom.remove(key);
            else custom.put(key, previous);
        }
    }

    @Test
    public void testDefaultOffsetTime() {
        final Schema<?> schema = PrimitiveType.createProperty(OffsetTime.class);
        assertNotNull(schema);
        assertEquals(schema.getClass(), TimeSchema.class);
        assertEquals(schema.getFormat(), "time");
    }

    @Test
    public void testDefaultDuration() {
        final Schema<?> schema = PrimitiveType.createProperty(Duration.class);
        assertNotNull(schema);
        assertEquals(schema.getClass(), DurationSchema.class);
        assertEquals(schema.getFormat(), "duration");
    }

    @Test
    public void testDefaultFormats31() {
        assertSchema31(PrimitiveType.createProperty(OffsetTime.class, true), "time");
        assertSchema31(PrimitiveType.createProperty(Duration.class, true), "duration");

        // LocalTime: same guard as testDefaultLocalTime ��� isolate from customClasses
        final String key = "java.time.LocalTime";
        final Map<String, PrimitiveType> custom = PrimitiveType.customClasses();
        final PrimitiveType previous = custom.remove(key);
        try {
            assertSchema31(PrimitiveType.createProperty(LocalTime.class, true), "time-local");
        } finally {
            if (previous == null) custom.remove(key);
            else custom.put(key, previous);
        }
    }

    @Test
    public void testEnableJava8Formats31() {
        final Map<String, PrimitiveType> custom = PrimitiveType.customClasses();
        final PrimitiveType previous = custom.get("java.time.LocalDateTime");

        PrimitiveType.enableJava8Formats();
        try {
            assertSchema31(PrimitiveType.createProperty(LocalDateTime.class, true), "date-time-local");
        } finally {
            if (previous == null) custom.remove("java.time.LocalDateTime");
            else custom.put("java.time.LocalDateTime", previous);
        }
    }

    private static void assertSchema31(Schema<?> schema, String format) {
        assertNotNull(schema);
        assertNotNull(schema.getTypes());
        assertTrue(schema.getTypes().contains("string"));
        assertEquals(schema.getFormat(), format);
    }
}