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
53 changes: 49 additions & 4 deletions libs/core/src/main/java/org/opensearch/semver/SemverRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
import org.opensearch.semver.expr.Caret;
import org.opensearch.semver.expr.Equal;
import org.opensearch.semver.expr.Expression;
import org.opensearch.semver.expr.Range;
import org.opensearch.semver.expr.Tilde;

import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Arrays.stream;

Expand All @@ -31,19 +35,24 @@
* <li>'=' Requires exact match with the range version. For example, =1.2.3 range would match only 1.2.3</li>
* <li>'~' Allows for patch version variability starting from the range version. For example, ~1.2.3 range would match versions greater than or equal to 1.2.3 but less than 1.3.0</li>
* <li>'^' Allows for patch and minor version variability starting from the range version. For example, ^1.2.3 range would match versions greater than or equal to 1.2.3 but less than 2.0.0</li>
* <li>Explicit ranges: [2.0.0,3.0.0], (2.0.0,3.0.0), [2.0.0,3.0.0), (2.0.0,3.0.0]</li>
* </ul>
*
* @opensearch.api
*/
@PublicApi(since = "2.13.0")
public class SemverRange implements ToXContentFragment {

private static final Pattern RANGE_PATTERN = Pattern.compile("([\\[\\(])([\\d.]+)\\s*,\\s*([\\d.]+)([\\]\\)])");

private final Version rangeVersion;
private final RangeOperator rangeOperator;
private final Expression expression;

public SemverRange(final Version rangeVersion, final RangeOperator rangeOperator) {
this.rangeVersion = rangeVersion;
this.rangeOperator = rangeOperator;
this.expression = rangeOperator.expression;
}

/**
Expand All @@ -52,6 +61,23 @@ public SemverRange(final Version rangeVersion, final RangeOperator rangeOperator
* @return a {@code SemverRange}
*/
public static SemverRange fromString(final String range) {
// Check if it's a range expression
Matcher matcher = RANGE_PATTERN.matcher(range);
if (matcher.matches()) {
char leftBracket = matcher.group(1).charAt(0);
String lowerVersionStr = matcher.group(2);
String upperVersionStr = matcher.group(3);
char rightBracket = matcher.group(4).charAt(0);

Version lowerVersion = Version.fromString(matcher.group(2));
Version upperVersion = Version.fromString(matcher.group(3));
boolean includeLower = leftBracket == '[';
boolean includeUpper = rightBracket == ']';

Range rangeExpression = new Range(lowerVersion, upperVersion, includeLower, includeUpper);
return new SemverRange(lowerVersion, RangeOperator.RANGE, rangeExpression);
}

RangeOperator rangeOperator = RangeOperator.fromRange(range);
String version = range.replaceFirst(rangeOperator.asEscapedString(), "");
if (!Version.stringHasLength(version)) {
Expand All @@ -60,6 +86,12 @@ public static SemverRange fromString(final String range) {
return new SemverRange(Version.fromString(version), rangeOperator);
}

public SemverRange(Version rangeVersion, RangeOperator operator, Expression customExpression) {
this.rangeVersion = rangeVersion;
this.rangeOperator = operator;
this.expression = customExpression;
}

/**
* Return the range operator for this range.
* @return range operator
Expand Down Expand Up @@ -94,7 +126,7 @@ public boolean isSatisfiedBy(final String versionToEvaluate) {
* @see #isSatisfiedBy(String)
*/
public boolean isSatisfiedBy(final Version versionToEvaluate) {
return this.rangeOperator.expression.evaluate(this.rangeVersion, versionToEvaluate);
return this.expression.evaluate(this.rangeVersion, versionToEvaluate);
}

@Override
Expand All @@ -106,16 +138,29 @@ public boolean equals(@Nullable final Object o) {
return false;
}
SemverRange range = (SemverRange) o;
return Objects.equals(rangeVersion, range.rangeVersion) && rangeOperator == range.rangeOperator;
return Objects.equals(rangeVersion, range.rangeVersion)
&& rangeOperator == range.rangeOperator
&& Objects.equals(expression, range.expression);
}

@Override
public int hashCode() {
return Objects.hash(rangeVersion, rangeOperator);
return Objects.hash(rangeVersion, rangeOperator, expression);
}

@Override
public String toString() {
if (rangeOperator == RangeOperator.RANGE && expression instanceof Range) {
Range range = (Range) expression;
return String.format(
Locale.ROOT,
"%s%s,%s%s",
range.isIncludeLower() ? "[" : "(",
range.getLowerBound(),
range.getUpperBound(),
range.isIncludeUpper() ? "]" : ")"
);
}
return rangeOperator.asString() + rangeVersion;
}

Expand All @@ -128,10 +173,10 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa
* A range operator.
*/
public enum RangeOperator {

EQ("=", new Equal()),
TILDE("~", new Tilde()),
CARET("^", new Caret()),
RANGE("range", new Range()),
DEFAULT("", new Equal());

private final String operator;
Expand Down
95 changes: 95 additions & 0 deletions libs/core/src/main/java/org/opensearch/semver/expr/Range.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.semver.expr;

import org.opensearch.Version;

import java.util.Objects;

/**
* Expression to evaluate version compatibility within a specified range with configurable bounds.
*/
public class Range implements Expression {
private final Version lowerBound;
private final Version upperBound;
private final boolean includeLower;
private final boolean includeUpper;

public Range() {
this.lowerBound = Version.fromString("0.0.0"); // Minimum version
this.upperBound = Version.fromString("999.999.999"); // Maximum version
this.includeLower = true; // Default to inclusive bounds
this.includeUpper = true;
}

public Range(Version lowerBound, Version upperBound, boolean includeLower, boolean includeUpper) {
if (lowerBound == null) {
throw new IllegalArgumentException("Lower bound cannot be null");
}
if (upperBound == null) {
throw new IllegalArgumentException("Upper bound cannot be null");
}
if (lowerBound.after(upperBound)) {
throw new IllegalArgumentException("Lower bound must be less than or equal to upper bound");
}
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.includeLower = includeLower;
this.includeUpper = includeUpper;
}

public void updateRange(Range other) {
if (other == null) {
throw new IllegalArgumentException("Range cannot be null");
}
}

Check warning on line 51 in libs/core/src/main/java/org/opensearch/semver/expr/Range.java

View check run for this annotation

Codecov / codecov/patch

libs/core/src/main/java/org/opensearch/semver/expr/Range.java#L51

Added line #L51 was not covered by tests

@Override
public boolean evaluate(final Version rangeVersion, final Version versionToEvaluate) {

boolean satisfiesLower = includeLower ? versionToEvaluate.onOrAfter(lowerBound) : versionToEvaluate.after(lowerBound);

boolean satisfiesUpper = includeUpper ? versionToEvaluate.onOrBefore(upperBound) : versionToEvaluate.before(upperBound);

return satisfiesLower && satisfiesUpper;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Range range = (Range) o;
return includeLower == range.includeLower
&& includeUpper == range.includeUpper
&& Objects.equals(lowerBound, range.lowerBound)
&& Objects.equals(upperBound, range.upperBound);
}

@Override
public int hashCode() {
return Objects.hash(lowerBound, upperBound, includeLower, includeUpper);
}

public boolean isIncludeLower() {
return includeLower;
}

public boolean isIncludeUpper() {
return includeUpper;
}

public Version getLowerBound() {
return lowerBound;
}

public Version getUpperBound() {
return upperBound;
}

}
Loading
Loading