From e92f6de685c1fbbcf23d4e1a039eee0ba0309030 Mon Sep 17 00:00:00 2001 From: dhananjay Date: Tue, 25 Nov 2025 01:35:19 +0100 Subject: [PATCH 1/2] fix #461: add minlength breaking change detection --- .../schemadiffresult/SchemaDiffResult.java | 1 + .../core/model/BackwardIncompatibleProp.java | 2 + .../openapidiff/core/model/ChangedSchema.java | 17 ++++ .../core/model/schema/ChangedMinLength.java | 78 +++++++++++++++++++ .../core/backcompat/MinLengthBCTest.java | 30 +++++++ .../src/test/resources/bc_minlength_base.yaml | 30 +++++++ .../bc_request_minlength_increased.yaml | 31 ++++++++ .../bc_response_minlength_increased.yaml | 31 ++++++++ 8 files changed, 220 insertions(+) create mode 100644 core/src/main/java/org/openapitools/openapidiff/core/model/schema/ChangedMinLength.java create mode 100644 core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java create mode 100644 core/src/test/resources/bc_minlength_base.yaml create mode 100644 core/src/test/resources/bc_request_minlength_increased.yaml create mode 100644 core/src/test/resources/bc_response_minlength_increased.yaml diff --git a/core/src/main/java/org/openapitools/openapidiff/core/compare/schemadiffresult/SchemaDiffResult.java b/core/src/main/java/org/openapitools/openapidiff/core/compare/schemadiffresult/SchemaDiffResult.java index dd3c8ab3..67d976a3 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/compare/schemadiffresult/SchemaDiffResult.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/compare/schemadiffresult/SchemaDiffResult.java @@ -66,6 +66,7 @@ public , X> DeferredChanged diff( .setReadOnly(new ChangedReadOnly(left.getReadOnly(), right.getReadOnly(), context)) .setWriteOnly(new ChangedWriteOnly(left.getWriteOnly(), right.getWriteOnly(), context)) .setMaxLength(new ChangedMaxLength(left.getMaxLength(), right.getMaxLength(), context)) + .setMinLength(new ChangedMinLength(left.getMinLength(), right.getMinLength(), context)) .setNumericRange( new ChangedNumericRange( left.getMinimum(), diff --git a/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java b/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java index f2180b34..46f7065b 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java @@ -14,6 +14,7 @@ public enum BackwardIncompatibleProp { REQUEST_CONTENT_DECREASED("incompatible.request.content.decreased", true), REQUEST_ENUM_DECREASED("incompatible.request.enum.decreased", true), REQUEST_MAX_LENGTH_DECREASED("incompatible.request.max.length.decreased", true), + REQUEST_MIN_LENGTH_INCREASED("incompatible.request.min.length.increased", true), REQUEST_NUMERIC_RANGE_DECREASED("incompatible.request.numeric.range.decreased", true), REQUEST_ONEOF_DECREASED("incompatible.request.oneof.decreased", true), REQUEST_PARAM_ALLOWEMPTY_DECREASED("incompatible.request.param.allowempty.decreased", true), @@ -31,6 +32,7 @@ public enum BackwardIncompatibleProp { RESPONSE_HEADER_REQUIRED_INCREASED("incompatible.response.header.required.increased", true), RESPONSE_HEADERS_DECREASED("incompatible.response.headers.decreased", true), RESPONSE_MAX_LENGTH_INCREASED("incompatible.response.max.length.increased", true), + RESPONSE_MIN_LENGTH_INCREASED("incompatible.response.min.length.increased", true), RESPONSE_NUMERIC_RANGE_INCREASED("incompatible.response.numeric.range.increased", false), RESPONSE_ONEOF_INCREASED("incompatible.response.oneof.increased", true), RESPONSE_REQUIRED_DECREASED("incompatible.response.required.decreased", true), diff --git a/core/src/main/java/org/openapitools/openapidiff/core/model/ChangedSchema.java b/core/src/main/java/org/openapitools/openapidiff/core/model/ChangedSchema.java index 1896783a..30d126fb 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/model/ChangedSchema.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/model/ChangedSchema.java @@ -17,6 +17,7 @@ import org.openapitools.openapidiff.core.model.schema.ChangedMaxLength; import org.openapitools.openapidiff.core.model.schema.ChangedMaxProperties; import org.openapitools.openapidiff.core.model.schema.ChangedMinItems; +import org.openapitools.openapidiff.core.model.schema.ChangedMinLength; import org.openapitools.openapidiff.core.model.schema.ChangedMinProperties; import org.openapitools.openapidiff.core.model.schema.ChangedMultipleOf; import org.openapitools.openapidiff.core.model.schema.ChangedNullable; @@ -49,6 +50,7 @@ public class ChangedSchema implements ComposedChanged { protected ChangedWriteOnly writeOnly; protected boolean changedType; protected ChangedMaxLength maxLength; + protected ChangedMinLength minLength; protected ChangedNumericRange numericRange; protected ChangedMultipleOf multipleOf; protected ChangedMaxItems maxItems; @@ -139,6 +141,7 @@ public List getChangedElements() { enumeration, required, maxLength, + minLength, numericRange, multipleOf, maxItems, @@ -303,6 +306,10 @@ public ChangedMaxLength getMaxLength() { return this.maxLength; } + public ChangedMinLength getMinLength() { + return this.minLength; + } + public ChangedNumericRange getNumericRange() { return this.numericRange; } @@ -475,6 +482,12 @@ public ChangedSchema setMaxLength(final ChangedMaxLength maxLength) { return this; } + public ChangedSchema setMinLength(final ChangedMinLength minLength) { + clearChangedCache(); + this.minLength = minLength; + return this; + } + public ChangedSchema setNumericRange(final ChangedNumericRange numericRange) { clearChangedCache(); this.numericRange = numericRange; @@ -585,6 +598,7 @@ public boolean equals(Object o) { && Objects.equals(readOnly, that.readOnly) && Objects.equals(writeOnly, that.writeOnly) && Objects.equals(maxLength, that.maxLength) + && Objects.equals(minLength, that.minLength) && Objects.equals(numericRange, that.numericRange) && Objects.equals(multipleOf, that.multipleOf) && Objects.equals(maxItems, that.maxItems) @@ -623,6 +637,7 @@ public int hashCode() { writeOnly, changedType, maxLength, + minLength, numericRange, multipleOf, maxItems, @@ -681,6 +696,8 @@ public java.lang.String toString() { + this.isChangedType() + ", maxLength=" + this.getMaxLength() + + ", minLength=" + + this.getMinLength() + ", numericRange=" + this.getNumericRange() + ", multipleOf=" diff --git a/core/src/main/java/org/openapitools/openapidiff/core/model/schema/ChangedMinLength.java b/core/src/main/java/org/openapitools/openapidiff/core/model/schema/ChangedMinLength.java new file mode 100644 index 00000000..a964f4a1 --- /dev/null +++ b/core/src/main/java/org/openapitools/openapidiff/core/model/schema/ChangedMinLength.java @@ -0,0 +1,78 @@ +package org.openapitools.openapidiff.core.model.schema; + +import static org.openapitools.openapidiff.core.model.BackwardIncompatibleProp.REQUEST_MIN_LENGTH_INCREASED; +import static org.openapitools.openapidiff.core.model.BackwardIncompatibleProp.RESPONSE_MIN_LENGTH_INCREASED; + +import java.util.Objects; +import org.openapitools.openapidiff.core.model.Changed; +import org.openapitools.openapidiff.core.model.DiffContext; +import org.openapitools.openapidiff.core.model.DiffResult; + +public final class ChangedMinLength implements Changed { + private final Integer oldValue; + private final Integer newValue; + private final DiffContext context; + + @Override + public DiffResult isChanged() { + if (Objects.equals(oldValue, newValue)) { + return DiffResult.NO_CHANGES; + } + if (context.isRequest() && (newValue != null && (oldValue == null || newValue > oldValue))) { + if (REQUEST_MIN_LENGTH_INCREASED.enabled(context)) { + return DiffResult.INCOMPATIBLE; + } + } + if (context.isResponse() && (newValue != null && (oldValue == null || newValue > oldValue))) { + if (RESPONSE_MIN_LENGTH_INCREASED.enabled(context)) { + return DiffResult.INCOMPATIBLE; + } + } + return DiffResult.COMPATIBLE; + } + + public ChangedMinLength( + final Integer oldValue, final Integer newValue, final DiffContext context) { + this.oldValue = oldValue; + this.newValue = newValue; + this.context = context; + } + + public Integer getOldValue() { + return this.oldValue; + } + + public Integer getNewValue() { + return this.newValue; + } + + public DiffContext getContext() { + return this.context; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChangedMinLength that = (ChangedMinLength) o; + return Objects.equals(oldValue, that.oldValue) + && Objects.equals(newValue, that.newValue) + && Objects.equals(context, that.context); + } + + @Override + public int hashCode() { + return Objects.hash(oldValue, newValue, context); + } + + @Override + public String toString() { + return "ChangedMinLength(oldValue=" + + this.getOldValue() + + ", newValue=" + + this.getNewValue() + + ", context=" + + this.getContext() + + ")"; + } +} diff --git a/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java b/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java new file mode 100644 index 00000000..43b4d1e5 --- /dev/null +++ b/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java @@ -0,0 +1,30 @@ +package org.openapitools.openapidiff.core.backcompat; + +import static org.openapitools.openapidiff.core.TestUtils.assertSpecIncompatible; +import static org.openapitools.openapidiff.core.TestUtils.assertSpecUnchanged; +import static org.openapitools.openapidiff.core.model.BackwardIncompatibleProp.REQUEST_MIN_LENGTH_INCREASED; +import static org.openapitools.openapidiff.core.model.BackwardIncompatibleProp.RESPONSE_MIN_LENGTH_INCREASED; + +import org.junit.jupiter.api.Test; +import org.openapitools.openapidiff.core.model.BackwardIncompatibleProp; + +public class MinLengthBCTest { + private final String BASE = "bc_minlength_base.yaml"; + + @Test + public void minLengthUnchanged() { + assertSpecUnchanged(BASE, BASE); + } + + @Test + public void requestMinLengthIncreased() { + BackwardIncompatibleProp prop = REQUEST_MIN_LENGTH_INCREASED; + assertSpecIncompatible(BASE, "bc_request_minlength_increased.yaml", prop); + } + + @Test + public void responseMinLengthIncreased() { + BackwardIncompatibleProp prop = RESPONSE_MIN_LENGTH_INCREASED; + assertSpecIncompatible(BASE, "bc_response_minlength_increased.yaml", prop); + } +} diff --git a/core/src/test/resources/bc_minlength_base.yaml b/core/src/test/resources/bc_minlength_base.yaml new file mode 100644 index 00000000..3e61e691 --- /dev/null +++ b/core/src/test/resources/bc_minlength_base.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + description: myDesc + title: myTitle + version: 1.0.0 +paths: + /widgets: + post: + operationId: widgetCreate + requestBody: + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + diff --git a/core/src/test/resources/bc_request_minlength_increased.yaml b/core/src/test/resources/bc_request_minlength_increased.yaml new file mode 100644 index 00000000..f7760ba6 --- /dev/null +++ b/core/src/test/resources/bc_request_minlength_increased.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + description: myDesc + title: myTitle + version: 1.0.0 +paths: + /widgets: + post: + operationId: widgetCreate + requestBody: + content: + application/json: + schema: + type: string + minLength: 10 + application/xml: + schema: + type: string + minLength: 10 + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + diff --git a/core/src/test/resources/bc_response_minlength_increased.yaml b/core/src/test/resources/bc_response_minlength_increased.yaml new file mode 100644 index 00000000..06197c03 --- /dev/null +++ b/core/src/test/resources/bc_response_minlength_increased.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + description: myDesc + title: myTitle + version: 1.0.0 +paths: + /widgets: + post: + operationId: widgetCreate + requestBody: + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + minLength: 10 + application/xml: + schema: + type: string + minLength: 10 + From 7d513113c6cb13d37c7831bfe362f87e8c5c7441 Mon Sep 17 00:00:00 2001 From: dhananjay Date: Wed, 26 Nov 2025 01:03:21 +0100 Subject: [PATCH 2/2] fix #461: add minlength breaking change detection --- .../core/model/BackwardIncompatibleProp.java | 4 +-- .../core/backcompat/MinLengthBCTest.java | 11 +++++++ ...uest_minlength_changed_but_compatible.yaml | 30 ++++++++++++++++++ ...onse_minlength_changed_but_compatible.yaml | 31 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 core/src/test/resources/bc_request_minlength_changed_but_compatible.yaml create mode 100644 core/src/test/resources/bc_response_minlength_changed_but_compatible.yaml diff --git a/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java b/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java index 46f7065b..b3da97ca 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/model/BackwardIncompatibleProp.java @@ -14,7 +14,7 @@ public enum BackwardIncompatibleProp { REQUEST_CONTENT_DECREASED("incompatible.request.content.decreased", true), REQUEST_ENUM_DECREASED("incompatible.request.enum.decreased", true), REQUEST_MAX_LENGTH_DECREASED("incompatible.request.max.length.decreased", true), - REQUEST_MIN_LENGTH_INCREASED("incompatible.request.min.length.increased", true), + REQUEST_MIN_LENGTH_INCREASED("incompatible.request.min.length.increased", false), REQUEST_NUMERIC_RANGE_DECREASED("incompatible.request.numeric.range.decreased", true), REQUEST_ONEOF_DECREASED("incompatible.request.oneof.decreased", true), REQUEST_PARAM_ALLOWEMPTY_DECREASED("incompatible.request.param.allowempty.decreased", true), @@ -32,7 +32,7 @@ public enum BackwardIncompatibleProp { RESPONSE_HEADER_REQUIRED_INCREASED("incompatible.response.header.required.increased", true), RESPONSE_HEADERS_DECREASED("incompatible.response.headers.decreased", true), RESPONSE_MAX_LENGTH_INCREASED("incompatible.response.max.length.increased", true), - RESPONSE_MIN_LENGTH_INCREASED("incompatible.response.min.length.increased", true), + RESPONSE_MIN_LENGTH_INCREASED("incompatible.response.min.length.increased", false), RESPONSE_NUMERIC_RANGE_INCREASED("incompatible.response.numeric.range.increased", false), RESPONSE_ONEOF_INCREASED("incompatible.response.oneof.increased", true), RESPONSE_REQUIRED_DECREASED("incompatible.response.required.decreased", true), diff --git a/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java b/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java index 43b4d1e5..fbc792b6 100644 --- a/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java +++ b/core/src/test/java/org/openapitools/openapidiff/core/backcompat/MinLengthBCTest.java @@ -1,5 +1,6 @@ package org.openapitools.openapidiff.core.backcompat; +import static org.openapitools.openapidiff.core.TestUtils.assertSpecChangedButCompatible; import static org.openapitools.openapidiff.core.TestUtils.assertSpecIncompatible; import static org.openapitools.openapidiff.core.TestUtils.assertSpecUnchanged; import static org.openapitools.openapidiff.core.model.BackwardIncompatibleProp.REQUEST_MIN_LENGTH_INCREASED; @@ -22,9 +23,19 @@ public void requestMinLengthIncreased() { assertSpecIncompatible(BASE, "bc_request_minlength_increased.yaml", prop); } + @Test + public void requestMinLengthDecreasedButCompatible() { + assertSpecChangedButCompatible(BASE, "bc_request_minlength_changed_but_compatible.yaml"); + } + @Test public void responseMinLengthIncreased() { BackwardIncompatibleProp prop = RESPONSE_MIN_LENGTH_INCREASED; assertSpecIncompatible(BASE, "bc_response_minlength_increased.yaml", prop); } + + @Test + public void responseMinLengthDecreasedButCompatible() { + assertSpecChangedButCompatible(BASE, "bc_response_minlength_changed_but_compatible.yaml"); + } } diff --git a/core/src/test/resources/bc_request_minlength_changed_but_compatible.yaml b/core/src/test/resources/bc_request_minlength_changed_but_compatible.yaml new file mode 100644 index 00000000..663ee7c0 --- /dev/null +++ b/core/src/test/resources/bc_request_minlength_changed_but_compatible.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + description: myDesc + title: myTitle + version: 1.0.0 +paths: + /widgets: + post: + operationId: widgetCreate + requestBody: + content: + application/json: + schema: + type: string + minLength: 3 + application/xml: + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + diff --git a/core/src/test/resources/bc_response_minlength_changed_but_compatible.yaml b/core/src/test/resources/bc_response_minlength_changed_but_compatible.yaml new file mode 100644 index 00000000..a965540a --- /dev/null +++ b/core/src/test/resources/bc_response_minlength_changed_but_compatible.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + description: myDesc + title: myTitle + version: 1.0.0 +paths: + /widgets: + post: + operationId: widgetCreate + requestBody: + content: + application/json: + schema: + type: string + minLength: 5 + application/xml: + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + minLength: 3 + application/xml: + schema: + type: string + minLength: 3 +