diff --git a/README.md b/README.md index 57b835b..c08e6d9 100644 --- a/README.md +++ b/README.md @@ -231,12 +231,26 @@ int result = v1.compareWithBuildsTo(v2); // < 0 SemVer Expressions API (Ranges) ---------------------- -Since version 0.7.0 Java SemVer supports the SemVer Expressions API which is -implemented as an external DSL. The BNF grammar for the SemVer Expressions DSL -can be found in the corresponding issue -"[Implement the SemVer Expressions API](https://github.com/zafarkhaja/java-semver/issues/1)". +Java SemVer supports the SemVer Expressions API which is implemented as both +internal DSL and external DSL. The entry point for the API are +the `Version.satisfies` methods. -The entry point for the API is the `Version.satisfies` method. +### Internal DSL ### +The internal DSL is implemented by the `CompositeExpression` class using fluent +interface. For convenience, it also provides the `Helper` class with static +helper methods. + +~~~ java +import com.github.zafarkhaja.semver.Version; +import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.*; + +Version v = Version.valueOf("1.0.0-beta"); +boolean result = v.satisfies(gte("1.0.0").and(lt("2.0.0"))); // false +~~~ + +### External DSL ### +The BNF grammar for the external DSL can be found in the corresponding +[issue](https://github.com/zafarkhaja/java-semver/issues/1). ~~~ java import com.github.zafarkhaja.semver.Version; @@ -246,7 +260,7 @@ boolean result = v.satisfies(">=1.0.0 & <2.0.0"); // false ~~~ Below are examples of some common use cases, as well as syntactic sugar and some -other interesting capabilities of the SemVer Expressions DSL. +other interesting capabilities of the SemVer Expressions external DSL. * Wildcard - `1.*` which is equivalent to `>=1.0.0 & <2.0.0` * Tilde operator - `~1.5` which is equivalent to `>=1.5.0 & <2.0.0` * Range - `1.0-2.0` which is equivalent to `>=1.0.0 & <=2.0.0` diff --git a/src/main/java/com/github/zafarkhaja/semver/Version.java b/src/main/java/com/github/zafarkhaja/semver/Version.java index d64c006..97e61f2 100644 --- a/src/main/java/com/github/zafarkhaja/semver/Version.java +++ b/src/main/java/com/github/zafarkhaja/semver/Version.java @@ -308,11 +308,11 @@ public class Version implements Comparable { } /** - * Checks if this version satisfies the specified SemVer Expression. + * Checks if this version satisfies the specified SemVer Expression string. * * This method is a part of the SemVer Expressions API. * - * @param expr the SemVer Expression + * @param expr the SemVer Expression string * @return {@code true} if this version satisfies the specified * SemVer Expression or {@code false} otherwise * @throws ParseException in case of a general parse error @@ -322,7 +322,21 @@ public class Version implements Comparable { */ public boolean satisfies(String expr) { Parser parser = ExpressionParser.newInstance(); - return parser.parse(expr).interpret(this); + return satisfies(parser.parse(expr)); + } + + /** + * Checks if this version satisfies the specified SemVer Expression. + * + * This method is a part of the SemVer Expressions API. + * + * @param expr the SemVer Expression + * @return {@code true} if this version satisfies the specified + * SemVer Expression or {@code false} otherwise + * @since 0.9.0 + */ + public boolean satisfies(Expression expr) { + return expr.interpret(this); } /** diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/CompositeExpression.java b/src/main/java/com/github/zafarkhaja/semver/expr/CompositeExpression.java new file mode 100644 index 0000000..8a6c4f8 --- /dev/null +++ b/src/main/java/com/github/zafarkhaja/semver/expr/CompositeExpression.java @@ -0,0 +1,266 @@ +/* + * The MIT License + * + * Copyright 2012-2014 Zafar Khaja . + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.zafarkhaja.semver.expr; + +import com.github.zafarkhaja.semver.ParseException; +import com.github.zafarkhaja.semver.UnexpectedCharacterException; +import com.github.zafarkhaja.semver.Version; + +/** + * This class implements internal DSL for the + * SemVer Expressions using fluent interface. + * + * @author Zafar Khaja + * @since 0.9.0 + */ +public class CompositeExpression implements Expression { + + /** + * A class with static helper methods. + */ + public static class Helper { + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Not} expression. + * + * @param expr an {@code Expression} to negate + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression not(Expression expr) { + return new CompositeExpression(new Not(expr)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Equal} expression. + * + * @param version a {@code Version} to check for equality + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression eq(Version version) { + return new CompositeExpression(new Equal(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Equal} expression. + * + * @param version a {@code Version} string to check for equality + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression eq(String version) { + return eq(Version.valueOf(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code NotEqual} expression. + * + * @param version a {@code Version} to check for non-equality + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression neq(Version version) { + return new CompositeExpression(new NotEqual(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code NotEqual} expression. + * + * @param version a {@code Version} string to check for non-equality + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression neq(String version) { + return neq(Version.valueOf(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Greater} expression. + * + * @param version a {@code Version} to compare with + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression gt(Version version) { + return new CompositeExpression(new Greater(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Greater} expression. + * + * @param version a {@code Version} string to compare with + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression gt(String version) { + return gt(Version.valueOf(version)); + } + + /** + * Creates a {@code CompositeExpression} with an + * underlying {@code GreaterOrEqual} expression. + * + * @param version a {@code Version} to compare with + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression gte(Version version) { + return new CompositeExpression(new GreaterOrEqual(version)); + } + + /** + * Creates a {@code CompositeExpression} with an + * underlying {@code GreaterOrEqual} expression. + * + * @param version a {@code Version} string to compare with + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression gte(String version) { + return gte(Version.valueOf(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Less} expression. + * + * @param version a {@code Version} to compare with + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression lt(Version version) { + return new CompositeExpression(new Less(version)); + } + + /** + * Creates a {@code CompositeExpression} with + * an underlying {@code Less} expression. + * + * @param version a {@code Version} string to compare with + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression lt(String version) { + return lt(Version.valueOf(version)); + } + + /** + * Creates a {@code CompositeExpression} with an + * underlying {@code LessOrEqual} expression. + * + * @param version a {@code Version} to compare with + * @return a newly created {@code CompositeExpression} + */ + public static CompositeExpression lte(Version version) { + return new CompositeExpression(new LessOrEqual(version)); + } + + /** + * Creates a {@code CompositeExpression} with an + * underlying {@code LessOrEqual} expression. + * + * @param version a {@code Version} string to compare with + * @return a newly created {@code CompositeExpression} + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public static CompositeExpression lte(String version) { + return lte(Version.valueOf(version)); + } + } + + /** + * The underlying expression tree. + */ + private Expression expr; + + /** + * Constructs a {@code CompositeExpression} + * with an underlying {@code Expression}. + * + * @param expr the underlying expression + */ + public CompositeExpression(Expression expr) { + this.expr = expr; + } + + /** + * Adds another {@code Expression} to {@code CompositeExpression} + * using {@code And} logical expression. + * + * @param expr an expression to add + * @return this {@code CompositeExpression} + */ + public CompositeExpression and(Expression expr) { + this.expr = new And(this.expr, expr); + return this; + } + + + /** + * Adds another {@code Expression} to {@code CompositeExpression} + * using {@code Or} logical expression. + * + * @param expr an expression to add + * @return this {@code CompositeExpression} + */ + public CompositeExpression or(Expression expr) { + this.expr = new Or(this.expr, expr); + return this; + } + + /** + * Interprets the expression. + * + * @param version a {@code Version} string to interpret against + * @return the result of the expression interpretation + * @throws IllegalArgumentException if the input string is {@code NULL} or empty + * @throws ParseException when invalid version string is provided + * @throws UnexpectedCharacterException is a special case of {@code ParseException} + */ + public boolean interpret(String version) { + return interpret(Version.valueOf(version)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean interpret(Version version) { + return expr.interpret(version); + } +} diff --git a/src/test/java/com/github/zafarkhaja/semver/VersionTest.java b/src/test/java/com/github/zafarkhaja/semver/VersionTest.java index 845e384..1b5fb9f 100644 --- a/src/test/java/com/github/zafarkhaja/semver/VersionTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/VersionTest.java @@ -26,6 +26,7 @@ package com.github.zafarkhaja.semver; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; +import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.*; import static org.junit.Assert.*; /** @@ -307,11 +308,18 @@ public class VersionTest { } @Test - public void shouldCheckIfVersionSatisfiesExpression() { + public void shouldCheckIfVersionSatisfiesExpressionString() { Version v = Version.valueOf("2.0.0-beta"); assertTrue(v.satisfies("~1.0")); assertFalse(v.satisfies(">=2.0 & <3.0")); } + + @Test + public void shouldCheckIfVersionSatisfiesExpression() { + Version v = Version.valueOf("2.0.0-beta"); + assertTrue(v.satisfies(gte("1.0.0").and(lt("2.0.0")))); + assertFalse(v.satisfies(gte("2.0.0").and(lt("3.0.0")))); + } } public static class EqualsMethodTest { diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/CompositeExpressionTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/CompositeExpressionTest.java new file mode 100644 index 0000000..7597919 --- /dev/null +++ b/src/test/java/com/github/zafarkhaja/semver/expr/CompositeExpressionTest.java @@ -0,0 +1,113 @@ +/* + * The MIT License + * + * Copyright 2012-2014 Zafar Khaja . + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.zafarkhaja.semver.expr; + +import org.junit.Test; +import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.*; +import static org.junit.Assert.*; + +/** + * + * @author Zafar Khaja + */ +public class CompositeExpressionTest { + + @Test + public void shouldSupportEqualExpression() { + assertTrue(eq("1.0.0").interpret("1.0.0")); + assertFalse(eq("1.0.0").interpret("2.0.0")); + } + + @Test + public void shouldSupportNotEqualExpression() { + assertTrue(neq("1.0.0").interpret("2.0.0")); + } + + @Test + public void shouldSupportGreaterExpression() { + assertTrue(gt("1.0.0").interpret("2.0.0")); + assertFalse(gt("2.0.0").interpret("1.0.0")); + } + + @Test + public void shouldSupportGreaterOrEqualExpression() { + assertTrue(gte("1.0.0").interpret("1.0.0")); + assertTrue(gte("1.0.0").interpret("2.0.0")); + assertFalse(gte("2.0.0").interpret("1.0.0")); + } + + @Test + public void shouldSupportLessExpression() { + assertTrue(lt("2.0.0").interpret("1.0.0")); + assertFalse(lt("1.0.0").interpret("2.0.0")); + } + + @Test + public void shouldSupportLessOrEqualExpression() { + assertTrue(lte("1.0.0").interpret("1.0.0")); + assertTrue(lte("2.0.0").interpret("1.0.0")); + assertFalse(lte("1.0.0").interpret("2.0.0")); + } + + @Test + public void shouldSupportNotExpression() { + assertTrue(not(eq("1.0.0")).interpret("2.0.0")); + assertFalse(not(eq("1.0.0")).interpret("1.0.0")); + } + + @Test + public void shouldSupportAndExpression() { + assertTrue(gt("1.0.0").and(lt("2.0.0")).interpret("1.5.0")); + assertFalse(gt("1.0.0").and(lt("2.0.0")).interpret("2.5.0")); + } + + @Test + public void shouldSupportOrExpression() { + assertTrue(lt("1.0.0").or(gt("1.0.0")).interpret("1.5.0")); + assertFalse(gt("1.0.0").or(gt("2.0.0")).interpret("0.5.0")); + } + + @Test + public void shouldSupportComplexExpressions() { + /* ((>=1.0.1 & <2) | (>=3.0 & <4)) & ((1-1.5) & (~1.5)) */ + CompositeExpression expr = + gte("1.0.1").and( + lt("2.0.0").or( + gte("3.0.0").and( + lt("4.0.0").and( + gte("1.0.0").and( + lte("1.5.0").and( + gte("1.5.0").and( + lt("2.0.0") + ) + ) + ) + ) + ) + ) + ); + assertTrue(expr.interpret("1.5.0")); + assertFalse(expr.interpret("2.5.0")); + } +}