Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -1789,6 +1790,8 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
return null;
}

normalizeExclusiveMinMax31(schema);

if (schema instanceof JsonSchema &&
schema.get$schema() == null &&
schema.getTypes() == null && schema.getType() == null) {
Expand Down Expand Up @@ -1883,6 +1886,51 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
return schema;
}

private void normalizeExclusiveMinMax31(Schema<?> schema) {
if (schema == null || schema.get$ref() != null) return;

// OAS 3.1 numeric exclusiveMinimum
BigDecimal exclusiveMinValue = schema.getExclusiveMinimumValue();
if (exclusiveMinValue != null) {
BigDecimal minimum = schema.getMinimum();

if (minimum == null) {
schema.setMinimum(exclusiveMinValue);
schema.setExclusiveMinimum(Boolean.TRUE);
} else {
int cmp = exclusiveMinValue.compareTo(minimum);

if (cmp > 0) {
schema.setMinimum(exclusiveMinValue);
schema.setExclusiveMinimum(Boolean.TRUE);
} else if (cmp == 0) {
schema.setExclusiveMinimum(Boolean.TRUE);
}
}
}

// OAS 3.1 numeric exclusiveMaximum
BigDecimal exclusiveMaxValue = schema.getExclusiveMaximumValue();
if (exclusiveMaxValue != null) {
BigDecimal maximum = schema.getMaximum();

if (maximum == null) {
schema.setMaximum(exclusiveMaxValue);
schema.setExclusiveMaximum(Boolean.TRUE);
} else {
int cmp = exclusiveMaxValue.compareTo(maximum);

if (cmp < 0) {
schema.setMaximum(exclusiveMaxValue);
schema.setExclusiveMaximum(Boolean.TRUE);
} else if (cmp == 0) {
schema.setExclusiveMaximum(Boolean.TRUE);
}
}
}
}


// ===================== end of rules =====================

protected static class Filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.testng.annotations.Test;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.*;

import static org.testng.Assert.*;
Expand Down Expand Up @@ -619,6 +620,131 @@ public void testNormalize31Parameters() {
assertNotNull(pathItem.getDelete().getParameters().get(0).getSchema().getTypes());
}

@Test
public void testNormalize31ExclusiveMinMaxNumericOnly() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");

OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
n.normalize();

Schema<?> schema = openAPI.getPaths()
.get("/x")
.getGet()
.getParameters()
.get(0)
.getSchema();

// exclusiveMinimum: 0
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
assertEquals(new BigDecimal("0"), schema.getMinimum());
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());

// exclusiveMaximum: 10
assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
assertEquals(new BigDecimal("10"), schema.getMaximum());
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
}

@Test
public void testNormalize31ExclusiveMinMaxStricterThanMinMax() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");

OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
n.normalize();

Schema<?> schema = openAPI.getPaths()
.get("/foo")
.getGet()
.getParameters()
.get(0)
.getSchema();

assertEquals(new BigDecimal("1"), schema.getExclusiveMinimumValue());
assertEquals(new BigDecimal("1"), schema.getMinimum());
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());

assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
assertEquals(new BigDecimal("10"), schema.getMaximum());
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
}

@Test
public void testNormalize31ExclusiveMinMaxEqualToMinMax() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");

OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
n.normalize();

Schema<?> schema = openAPI.getPaths()
.get("/bar")
.getGet()
.getParameters()
.get(0)
.getSchema();

// minimum: 0 + exclusiveMinimum: 0 → must remain exclusive
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
assertEquals(new BigDecimal("0"), schema.getMinimum());
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());

// maximum: 10 + exclusiveMaximum: 10 → must remain exclusive
assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
assertEquals(new BigDecimal("10"), schema.getMaximum());
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
}

@Test
public void testNormalize31ExclusiveMinMaxInclusiveStricterThanExclusiveValue() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");

OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
n.normalize();

Schema<?> schema = openAPI.getPaths()
.get("/baz")
.getGet()
.getParameters()
.get(0)
.getSchema();

// minimum: 5 is stricter than exclusiveMinimum: 0 (x >= 5 dominates x > 0)
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
assertEquals(new BigDecimal("5"), schema.getMinimum());
assertNull(schema.getExclusiveMinimum());

// maximum: 10 is stricter than exclusiveMaximum: 11 (x <= 10 dominates x < 11)
assertEquals(new BigDecimal("11"), schema.getExclusiveMaximumValue());
assertEquals(new BigDecimal("10"), schema.getMaximum());
assertNull(schema.getExclusiveMaximum());
}

@Test
public void testNormalize31ExclusiveMinMaxBooleanExclusiveAlreadySet() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");

OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
n.normalize();

Schema<?> schema = openAPI.getPaths()
.get("/old")
.getGet()
.getParameters()
.get(0)
.getSchema();

// 3.0-style boolean exclusive flags should remain intact
assertEquals(new BigDecimal("0"), schema.getMinimum());
assertNull(schema.getExclusiveMinimum());

assertEquals(new BigDecimal("10"), schema.getMaximum());
assertNull(schema.getExclusiveMaximum());

// Ensure numeric 3.1 value fields are not unexpectedly set by normalization
assertNull(schema.getExclusiveMinimumValue());
assertNull(schema.getExclusiveMaximumValue());
}


@Test
public void testRemoveXInternal() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,60 @@ public void shouldApiNameSuffixForApiClassname() throws IOException {
assertThat(notExisting).isNull();
}

@Test
public void shouldGenerateExclusiveMinMaxForOAS31() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_1/exclusive-min-max.yaml", null, new ParseOptions())
.getOpenAPI();

SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(INTERFACE_ONLY, "true");
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");

ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen);

DefaultGenerator generator = new DefaultGenerator();
generator.setGenerateMetadata(false);
Map<String, File> files = generator.opts(input).generate().stream()
.collect(Collectors.toMap(File::getName, Function.identity()));

System.out.println("Generated files:");
files.keySet().stream().sorted().forEach(System.out::println);


File apiFile = files.get("XApi.java");
assertThat(apiFile).isNotNull();

String content = Files.readString(apiFile.toPath());

var param = openAPI.getPaths()
.get("/x").getGet().getParameters().get(0);

var schema = (io.swagger.v3.oas.models.media.Schema<?>) param.getSchema();

System.out.println("minimum=" + schema.getMinimum());
System.out.println("maximum=" + schema.getMaximum());
System.out.println("exclusiveMinimum=" + schema.getExclusiveMinimum());
System.out.println("exclusiveMaximum=" + schema.getExclusiveMaximum());
System.out.println("exclusiveMinimum class=" + (schema.getExclusiveMinimum() == null ? null : schema.getExclusiveMinimum().getClass()));

System.out.println("schema extensions=" + schema.getExtensions());

assertThat(content).contains("@DecimalMin");
assertThat(content).contains("\"0\"");
assertThat(content).contains("@DecimalMax");
assertThat(content).contains("\"10\"");
assertThat(content).contains("inclusive = false");
assertThat(content).doesNotContain("inclusive = true");
}


@Test
public void shouldUseTagsForClassname() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
openapi: 3.1.0
info: { title: t, version: 1.0.0 }
paths:
/x:
get:
operationId: getX
parameters:
- name: price
in: query
required: true
schema:
type: number
exclusiveMinimum: 0
exclusiveMaximum: 10
responses:
"200":
description: ok
/foo:
get:
operationId: getFoo
parameters:
- name: foo
in: query
required: true
schema:
type: number
minimum: 0
exclusiveMinimum: 1
maximum: 11
exclusiveMaximum: 10
responses:
"200":
description: ok
/bar:
get:
operationId: getBar
parameters:
- name: bar
in: query
required: true
schema:
type: number
minimum: 0
exclusiveMinimum: 0
maximum: 10
exclusiveMaximum: 10
responses:
"200":
description: ok
/baz:
get:
operationId: getBaz
parameters:
- name: baz
in: query
required: true
schema:
type: number
minimum: 5
exclusiveMinimum: 0
maximum: 10
exclusiveMaximum: 11
responses:
"200":
description: ok
/old:
get:
operationId: getOld
parameters:
- name: old
in: query
required: true
schema:
type: number
minimum: 0
exclusiveMinimum: true
maximum: 10
exclusiveMaximum: true
responses:
"200":
description: ok
Loading