Create SemVer Expression parser

This commit is contained in:
Zafar Khaja 2013-11-10 23:47:51 +04:00
parent 2dc8bd8930
commit 388f94915f
5 changed files with 516 additions and 27 deletions

View File

@ -1,12 +1,10 @@
Java SemVer v0.7.0 (SemVer 2) [![Build Status](https://travis-ci.org/zafarkhaja/java-semver.png)](https://travis-ci.org/zafarkhaja/java-semver)
==================
=============================
Java SemVer is a Java implementation of the Semantic Versioning Specification
(http://semver.org/).
Versioning
----------
### Versioning ###
Java SemVer is versioned according to the SemVer Specification.
**NOTE**: The current release of the Java SemVer library has a major version of
@ -19,13 +17,13 @@ Usage
Below are some common use cases for the Java SemVer library.
### Creating Versions ###
Java SemVer library is composed of one small package which contains a few
classes. All the classes but one are package-private and are not accessible
outside the package. The only public class is `Version` which acts as a
_facade_ for the client code. By design, the `Version` class is made immutable
by making its constructor package-private, so that it can not be subclassed or
directly instantiated. Instead of public constructor, the `Version` class
provides a _static factory method_, `Version.valueOf(String value)`.
The main class of the Java SemVer library is `Version` which implements the
Facade design pattern. By design, the `Version` class is made immutable by
making its constructors package-private, so that it can not be subclassed or
directly instantiated. Instead of public constructors, the `Version` class
provides few _static factory methods_.
One of the methods is the `Version.valueOf` method.
```java
import com.github.zafarkhaja.semver.Version;
@ -43,6 +41,17 @@ String build = v.getBuildMetadata(); // "build.1"
String str = v.toString(); // "1.0.0-rc.1+build.1"
```
The other static factory method is `Version.forIntegers` which is also
overloaded to allow fewer arguments.
```java
import com.github.zafarkhaja.semver.Version;
Version v1 = Version.forIntegers(1);
Version v2 = Version.forIntegers(1, 2);
Version v3 = Version.forIntegers(1, 2, 3);
```
Another way to create a `Version` is to use a _builder_ class `Version.Builder`.
```java
@ -76,15 +85,15 @@ import com.github.zafarkhaja.semver.Version;
Version v1 = Version.valueOf("1.2.3");
// Incrementing major version
// Incrementing the major version
Version v2 = v1.incrementMajorVersion(); // "2.0.0"
Version v2 = v1.incrementMajorVersion("alpha"); // "2.0.0-alpha"
// Incrementing minor version
// Incrementing the minor version
Version v3 = v1.incrementMinorVersion(); // "1.3.0"
Version v3 = v1.incrementMinorVersion("alpha"); // "1.3.0-alpha"
// Incrementing patch version
// Incrementing the patch version
Version v4 = v1.incrementPatchVersion(); // "1.2.4"
Version v4 = v1.incrementPatchVersion("alpha"); // "1.2.4-alpha"
@ -92,30 +101,32 @@ Version v4 = v1.incrementPatchVersion("alpha"); // "1.2.4-alpha"
String str = v1.toString(); // "1.2.3"
```
There are also incrementor methods for pre-release version and build metadata.
There are also incrementor methods for the pre-release version and the build
metadata.
```java
import com.github.zafarkhaja.semver.Version;
// Incrementing pre-release version
// Incrementing the pre-release version
Version v1 = Version.valueOf("1.2.3-rc"); // considered as "rc.0"
Version v2 = v1.incrementPreReleaseVersion(); // "1.2.3-rc.1"
Version v3 = v2.incrementPreReleaseVersion(); // "1.2.3-rc.2"
// Incrementing build metadata
// Incrementing the build metadata
Version v1 = Version.valueOf("1.2.3-rc+build"); // considered as "build.0"
Version v2 = v1.incrementBuildMetadata(); // "1.2.3-rc+build.1"
Version v3 = v2.incrementBuildMetadata(); // "1.2.3-rc+build.2"
```
When incrementing normal or pre-release versions build metadata is always dropped.
When incrementing the normal or pre-release versions the build metadata is
always dropped.
```java
import com.github.zafarkhaja.semver.Version;
Version v1 = Version.valueOf("1.2.3-beta+build");
// Incrementing normal version
// Incrementing the normal version
Version v2 = v1.incrementMajorVersion(); // "2.0.0"
Version v2 = v1.incrementMajorVersion("alpha"); // "2.0.0-alpha"
@ -125,7 +136,7 @@ Version v3 = v1.incrementMinorVersion("alpha"); // "1.3.0-alpha"
Version v4 = v1.incrementPatchVersion(); // "1.2.4"
Version v4 = v1.incrementPatchVersion("alpha"); // "1.2.4-alpha"
// Incrementing pre-release version
// Incrementing the pre-release version
Version v2 = v1.incrementPreReleaseVersion(); // "1.2.3-beta.1"
```
**NOTE**: The discussion page https://github.com/mojombo/semver/issues/60 might
@ -134,8 +145,8 @@ incrementor methods.
### Comparing Versions ###
Comparing versions with Java SemVer is easy. The `Version` class implements the
`Comparable` interface, it also overrides the `Object.equals(Object obj)` method
and provides some more methods for convenient comparing.
`Comparable` interface, it also overrides the `Object.equals` method and provides
some more methods for convenient comparing.
```java
import com.github.zafarkhaja.semver.Version;
@ -152,7 +163,7 @@ boolean result = v1.lessThan(v2); // true
boolean result = v1.lessThanOrEqualTo(v2); // true
```
When determining version precedence build metadata is ignored (SemVer p.10).
When determining version precedence the build metadata is ignored (SemVer p.10).
```java
import com.github.zafarkhaja.semver.Version;
@ -164,9 +175,9 @@ int result = v1.compareTo(v2); // = 0
boolean result = v1.equals(v2); // true
```
Sometimes, however, you might want to compare versions with build metadata in
mind. For such cases Java SemVer provides a _comparator_ `Version.BUILD_AWARE_ORDER`
and a convenience method `Version.compareWithBuildsTo(Version other)`.
Sometimes, however, you might want to compare versions with the build metadata
in mind. For such cases Java SemVer provides a _comparator_ `Version.BUILD_AWARE_ORDER`
and a convenience method `Version.compareWithBuildsTo`.
```java
import com.github.zafarkhaja.semver.Version;
@ -181,9 +192,34 @@ boolean result = v1.equals(v2); // false
```
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)".
The entry point for the API is the `Version.satisfies` method.
```java
import com.github.zafarkhaja.semver.Version;
Version v = Version.valueOf("1.0.0-beta");
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.
* 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`
* Negation operator - `!(1.*)` which is equivalent to `<1.0.0 & >=2.0.0`
* Short notation - `1` which is equivalent to `=1.0.0`
* Parenthesized expression - `~1.3 | (1.4.* & !=1.4.5) | ~2`
TODO
----
* [Implement ranges](https://github.com/zafarkhaja/java-semver/issues/1)
* [Write doc comments for all API classes and methods](https://github.com/zafarkhaja/java-semver/issues/2)

View File

@ -23,6 +23,8 @@
*/
package com.github.zafarkhaja.semver;
import com.github.zafarkhaja.semver.expr.Expression;
import com.github.zafarkhaja.semver.expr.ExpressionParser;
import java.util.Comparator;
/**
@ -116,6 +118,23 @@ public class Version implements Comparable<Version> {
return VersionParser.parseValidSemVer(version);
}
public static Version forIntegers(int major) {
return new Version(new NormalVersion(major, 0, 0));
}
public static Version forIntegers(int major, int minor) {
return new Version(new NormalVersion(major, minor, 0));
}
public static Version forIntegers(int major, int minor, int patch) {
return new Version(new NormalVersion(major, minor, patch));
}
public boolean satisfies(String expr) {
Parser<Expression> parser = ExpressionParser.newInstance();
return parser.parse(expr).interpret(this);
}
public Version incrementMajorVersion() {
return new Version(normal.incrementMajor());
}

View File

@ -0,0 +1,222 @@
/*
* The MIT License
*
* Copyright 2013 Zafar Khaja <zafarkhaja@gmail.com>.
*
* 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.Parser;
import com.github.zafarkhaja.semver.Version;
import com.github.zafarkhaja.semver.expr.Lexer.Token;
import com.github.zafarkhaja.semver.util.Stream;
import com.github.zafarkhaja.semver.util.Stream.ElementType;
import java.util.EnumSet;
import java.util.Iterator;
import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*;
/**
*
* @author Zafar Khaja <zafarkhaja@gmail.com>
*/
public class ExpressionParser implements Parser<Expression> {
private final Lexer lexer;
private Stream<Token> tokens;
ExpressionParser(Lexer lexer) {
this.lexer = lexer;
}
public static Parser<Expression> newInstance() {
return new ExpressionParser(new Lexer());
}
@Override
public Expression parse(String input) {
tokens = lexer.tokenize(input);
return parseSemVerExpression();
}
private Expression parseSemVerExpression() {
Expression expr;
if (tokens.positiveLookahead(NOT)) {
tokens.consume();
tokens.consume(LEFT_PAREN);
expr = new Not(parseSemVerExpression());
tokens.consume(RIGHT_PAREN);
} else if (tokens.positiveLookahead(LEFT_PAREN)) {
tokens.consume(LEFT_PAREN);
expr = parseSemVerExpression();
tokens.consume(RIGHT_PAREN);
} else {
expr = parseExpression();
}
return parseBooleanExpression(expr);
}
private Expression parseBooleanExpression(Expression expr) {
if (tokens.positiveLookahead(AND)) {
tokens.consume();
expr = new And(expr, parseSemVerExpression());
} else if (tokens.positiveLookahead(OR)) {
tokens.consume();
expr = new Or(expr, parseSemVerExpression());
}
return expr;
}
private Expression parseExpression() {
if (tokens.positiveLookahead(TILDE)) {
return parseTildeExpression();
} else if (isVersionExpression()) {
return parseVersionExpression();
} else if (isRangeExpression()) {
return parseRangeExpression();
}
return parseComparisonExpression();
}
private Expression parseComparisonExpression() {
Token token = tokens.lookahead();
Expression expr;
switch (token.type) {
case EQUAL:
tokens.consume();
expr = new Equal(parseVersion());
break;
case NOT_EQUAL:
tokens.consume();
expr = new NotEqual(parseVersion());
break;
case GREATER:
tokens.consume();
expr = new Greater(parseVersion());
break;
case GREATER_EQUAL:
tokens.consume();
expr = new GreaterOrEqual(parseVersion());
break;
case LESS:
tokens.consume();
expr = new Less(parseVersion());
break;
case LESS_EQUAL:
tokens.consume();
expr = new LessOrEqual(parseVersion());
break;
default:
expr = new Equal(parseVersion());
}
return expr;
}
private Expression parseTildeExpression() {
tokens.consume(TILDE);
int major = intOf(tokens.consume(NUMERIC).lexeme);
if (!tokens.positiveLookahead(DOT)) {
return new GreaterOrEqual(versionOf(major, 0, 0));
}
tokens.consume(DOT);
int minor = intOf(tokens.consume(NUMERIC).lexeme);
if (!tokens.positiveLookahead(DOT)) {
return new And(
new GreaterOrEqual(versionOf(major, minor, 0)),
new Less(versionOf(major + 1, 0, 0))
);
}
tokens.consume(DOT);
int patch = intOf(tokens.consume(NUMERIC).lexeme);
return new And(
new GreaterOrEqual(versionOf(major, minor, patch)),
new Less(versionOf(major, minor + 1, 0))
);
}
private boolean isVersionExpression() {
return isVersionFollowedBy(STAR);
}
private Expression parseVersionExpression() {
int major = intOf(tokens.consume(NUMERIC).lexeme);
tokens.consume(DOT);
if (tokens.positiveLookahead(STAR)) {
tokens.consume();
return new And(
new GreaterOrEqual(versionOf(major, 0, 0)),
new Less(versionOf(major + 1, 0, 0))
);
}
int minor = intOf(tokens.consume(NUMERIC).lexeme);
tokens.consume(DOT);
tokens.consume(STAR);
return new And(
new GreaterOrEqual(versionOf(major, minor, 0)),
new Less(versionOf(major, minor + 1, 0))
);
}
private boolean isRangeExpression() {
return isVersionFollowedBy(HYPHEN);
}
private Expression parseRangeExpression() {
Expression ge = new GreaterOrEqual(parseVersion());
tokens.consume(HYPHEN);
Expression le = new LessOrEqual(parseVersion());
return new And(ge, le);
}
private Version parseVersion() {
int major = intOf(tokens.consume(NUMERIC).lexeme);
int minor = 0;
if (tokens.positiveLookahead(DOT)) {
tokens.consume();
minor = intOf(tokens.consume(NUMERIC).lexeme);
}
int patch = 0;
if (tokens.positiveLookahead(DOT)) {
tokens.consume();
patch = intOf(tokens.consume(NUMERIC).lexeme);
}
return versionOf(major, minor, patch);
}
private boolean isVersionFollowedBy(ElementType<Token> type) {
EnumSet<Token.Type> expected = EnumSet.of(NUMERIC, DOT);
Iterator<Token> it = tokens.iterator();
Token lookahead = null;
while (it.hasNext()) {
lookahead = it.next();
if (!expected.contains(lookahead.type)) {
break;
}
}
return type.isMatchedBy(lookahead);
}
private Version versionOf(int major, int minor, int patch) {
return Version.forIntegers(major, minor, patch);
}
private int intOf(String value) {
return Integer.parseInt(value);
}
}

View File

@ -305,6 +305,13 @@ public class VersionTest {
assertTrue(0 == v1.compareTo(v2));
assertTrue(0 > v1.compareWithBuildsTo(v2));
}
@Test
public void shouldCheckIfVersionSatisfiesExpression() {
Version v = Version.valueOf("2.0.0-beta");
assertTrue(v.satisfies("~1.0"));
assertFalse(v.satisfies(">=2.0 & <3.0"));
}
}
public static class EqualsMethodTest {

View File

@ -0,0 +1,205 @@
/*
* The MIT License
*
* Copyright 2013 Zafar Khaja <zafarkhaja@gmail.com>.
*
* 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.Version;
import com.github.zafarkhaja.semver.util.UnexpectedElementTypeException;
import org.junit.Test;
import static org.junit.Assert.*;
/**
*
* @author Zafar Khaja <zafarkhaja@gmail.com>
*/
public class ExpressionParserTest {
@Test
public void shouldParseEqualComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression eq = parser.parse("=1.0.0");
assertTrue(eq.interpret(Version.valueOf("1.0.0")));
}
@Test
public void shouldParseEqualComparisonExpressionIfOnlyVersionGiven() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression eq = parser.parse("1.0.0");
assertTrue(eq.interpret(Version.valueOf("1.0.0")));
}
@Test
public void shouldParseNotEqualComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression ne = parser.parse("!=1.0.0");
assertTrue(ne.interpret(Version.valueOf("1.2.3")));
}
@Test
public void shouldParseGreaterComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression gt = parser.parse(">1.0.0");
assertTrue(gt.interpret(Version.valueOf("1.2.3")));
}
@Test
public void shouldParseGreaterOrEqualComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression ge = parser.parse(">=1.0.0");
assertTrue(ge.interpret(Version.valueOf("1.0.0")));
assertTrue(ge.interpret(Version.valueOf("1.2.3")));
}
@Test
public void shouldParseLessComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression lt = parser.parse("<1.2.3");
assertTrue(lt.interpret(Version.valueOf("1.0.0")));
}
@Test
public void shouldParseLessOrEqualComparisonExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression le = parser.parse("<=1.2.3");
assertTrue(le.interpret(Version.valueOf("1.0.0")));
assertTrue(le.interpret(Version.valueOf("1.2.3")));
}
@Test
public void shouldParseTildeExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr1 = parser.parse("~1");
assertTrue(expr1.interpret(Version.valueOf("1.2.3")));
assertTrue(expr1.interpret(Version.valueOf("3.2.1")));
Expression expr2 = parser.parse("~1.2");
assertTrue(expr2.interpret(Version.valueOf("1.2.3")));
assertFalse(expr2.interpret(Version.valueOf("2.0.0")));
Expression expr3 = parser.parse("~1.2.3");
assertTrue(expr3.interpret(Version.valueOf("1.2.3")));
assertFalse(expr3.interpret(Version.valueOf("1.3.0")));
}
@Test
public void shouldParseShortFormOfVersion() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr1 = parser.parse("1");
assertTrue(expr1.interpret(Version.valueOf("1.0.0")));
Expression expr2 = parser.parse("2.0");
assertTrue(expr2.interpret(Version.valueOf("2.0.0")));
}
@Test
public void shouldParseVersionExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr1 = parser.parse("1.*");
assertTrue(expr1.interpret(Version.valueOf("1.2.3")));
assertFalse(expr1.interpret(Version.valueOf("3.2.1")));
Expression expr2 = parser.parse("1.2.*");
assertTrue(expr2.interpret(Version.valueOf("1.2.3")));
assertFalse(expr2.interpret(Version.valueOf("1.3.2")));
}
@Test
public void shouldParseRangeExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression range = parser.parse("1.0.0 - 2.0.0");
assertTrue(range.interpret(Version.valueOf("1.2.3")));
assertFalse(range.interpret(Version.valueOf("3.2.1")));
}
@Test
public void shouldParseAndBooleanExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression and = parser.parse(">=1.0.0 & <2.0.0");
assertTrue(and.interpret(Version.valueOf("1.2.3")));
assertFalse(and.interpret(Version.valueOf("3.2.1")));
}
@Test
public void shouldParseOrBooleanExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression or = parser.parse("1.* | =2.0.0");
assertTrue(or.interpret(Version.valueOf("1.2.3")));
assertFalse(or.interpret(Version.valueOf("2.1.0")));
}
@Test
public void shouldParseParenthesizedExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr = parser.parse("(1)");
assertTrue(expr.interpret(Version.valueOf("1.0.0")));
assertFalse(expr.interpret(Version.valueOf("2.0.0")));
}
@Test
public void shouldParseExpressionWithMultipleParentheses() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr = parser.parse("((1))");
assertTrue(expr.interpret(Version.valueOf("1.0.0")));
assertFalse(expr.interpret(Version.valueOf("2.0.0")));
}
@Test
public void shouldParseNotExpression() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression not1 = parser.parse("!(1)");
assertTrue(not1.interpret(Version.valueOf("2.0.0")));
assertFalse(not1.interpret(Version.valueOf("1.0.0")));
Expression not2 = parser.parse("0.* & !(>=1 & <2)");
assertTrue(not2.interpret(Version.valueOf("0.5.0")));
assertFalse(not2.interpret(Version.valueOf("1.0.1")));
Expression not3 = parser.parse("!(>=1 & <2) & >=2");
assertTrue(not3.interpret(Version.valueOf("2.0.0")));
assertFalse(not3.interpret(Version.valueOf("1.2.3")));
}
@Test
public void shouldRespectPrecedenceWhenUsedWithParentheses() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr1 = parser.parse("(~1.0 & <2.0) | >2.0");
assertTrue(expr1.interpret(Version.valueOf("2.5.0")));
Expression expr2 = parser.parse("~1.0 & (<2.0 | >2.0)");
assertFalse(expr2.interpret(Version.valueOf("2.5.0")));
}
@Test
public void shouldParseComplexExpressions() {
ExpressionParser parser = new ExpressionParser(new Lexer());
Expression expr = parser.parse(
"((>=1.0.1 & <2) | (>=3.0 & <4)) & ((1-1.5) & (~1.5))"
);
assertTrue(expr.interpret(Version.valueOf("1.5.0")));
assertFalse(expr.interpret(Version.valueOf("2.5.0")));
}
@Test
public void shouldRaiseErrorIfClosingParenthesisIsMissing() {
ExpressionParser parser = new ExpressionParser(new Lexer());
try {
parser.parse("((>=1.0.1 & < 2)");
} catch (UnexpectedElementTypeException e) {
return;
}
fail("Should raise error if closing parenthesis is missing");
}
}