Refactor VersionParser to improve error handling
This commit is contained in:
parent
82dd3c2968
commit
d36d961555
|
@ -139,6 +139,8 @@ public class Version implements Comparable<Version> {
|
||||||
* Builds a {@code Version} object.
|
* Builds a {@code Version} object.
|
||||||
*
|
*
|
||||||
* @return a newly built {@code Version} instance
|
* @return a newly built {@code Version} instance
|
||||||
|
* @throws ParseException when invalid version string is provided
|
||||||
|
* @throws UnexpectedCharacterException is a special case of {@code ParseException}
|
||||||
*/
|
*/
|
||||||
public Version build() {
|
public Version build() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
@ -254,6 +256,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param version the version string to parse
|
* @param version the version string to parse
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version valueOf(String version) {
|
public static Version valueOf(String version) {
|
||||||
return VersionParser.parseValidSemVer(version);
|
return VersionParser.parseValidSemVer(version);
|
||||||
|
@ -328,6 +332,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param preRelease the pre-release version to append
|
* @param preRelease the pre-release version to append
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version incrementMajorVersion(String preRelease) {
|
public Version incrementMajorVersion(String preRelease) {
|
||||||
return new Version(
|
return new Version(
|
||||||
|
@ -351,6 +357,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param preRelease the pre-release version to append
|
* @param preRelease the pre-release version to append
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version incrementMinorVersion(String preRelease) {
|
public Version incrementMinorVersion(String preRelease) {
|
||||||
return new Version(
|
return new Version(
|
||||||
|
@ -374,6 +382,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param preRelease the pre-release version to append
|
* @param preRelease the pre-release version to append
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version incrementPatchVersion(String preRelease) {
|
public Version incrementPatchVersion(String preRelease) {
|
||||||
return new Version(
|
return new Version(
|
||||||
|
@ -406,6 +416,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param preRelease the pre-release version to set
|
* @param preRelease the pre-release version to set
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version setPreReleaseVersion(String preRelease) {
|
public Version setPreReleaseVersion(String preRelease) {
|
||||||
return new Version(normal, VersionParser.parsePreRelease(preRelease));
|
return new Version(normal, VersionParser.parsePreRelease(preRelease));
|
||||||
|
@ -417,6 +429,8 @@ public class Version implements Comparable<Version> {
|
||||||
* @param build the build metadata to set
|
* @param build the build metadata to set
|
||||||
* @return a new instance of the {@code Version} class
|
* @return a new instance of the {@code Version} class
|
||||||
* @throws IllegalArgumentException if the input string is {@code NULL} or empty
|
* @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 Version setBuildMetadata(String build) {
|
public Version setBuildMetadata(String build) {
|
||||||
return new Version(normal, preRelease, VersionParser.parseBuild(build));
|
return new Version(normal, preRelease, VersionParser.parseBuild(build));
|
||||||
|
|
|
@ -257,15 +257,19 @@ class VersionParser implements Parser<Version> {
|
||||||
private Version parseValidSemVer() {
|
private Version parseValidSemVer() {
|
||||||
NormalVersion normal = parseVersionCore();
|
NormalVersion normal = parseVersionCore();
|
||||||
MetadataVersion preRelease = MetadataVersion.NULL;
|
MetadataVersion preRelease = MetadataVersion.NULL;
|
||||||
if (chars.positiveLookahead(HYPHEN)) {
|
|
||||||
chars.consume();
|
|
||||||
preRelease = parsePreRelease();
|
|
||||||
}
|
|
||||||
MetadataVersion build = MetadataVersion.NULL;
|
MetadataVersion build = MetadataVersion.NULL;
|
||||||
if (chars.positiveLookahead(PLUS)) {
|
|
||||||
chars.consume();
|
Character next = consumeNextCharacter(HYPHEN, PLUS, EOL);
|
||||||
|
if (HYPHEN.isMatchedBy(next)) {
|
||||||
|
preRelease = parsePreRelease();
|
||||||
|
next = consumeNextCharacter(PLUS, EOL);
|
||||||
|
if (PLUS.isMatchedBy(next)) {
|
||||||
|
build = parseBuild();
|
||||||
|
}
|
||||||
|
} else if (PLUS.isMatchedBy(next)) {
|
||||||
build = parseBuild();
|
build = parseBuild();
|
||||||
}
|
}
|
||||||
|
consumeNextCharacter(EOL);
|
||||||
return new Version(normal, preRelease, build);
|
return new Version(normal, preRelease, build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,9 +286,9 @@ class VersionParser implements Parser<Version> {
|
||||||
*/
|
*/
|
||||||
private NormalVersion parseVersionCore() {
|
private NormalVersion parseVersionCore() {
|
||||||
int major = Integer.parseInt(numericIdentifier());
|
int major = Integer.parseInt(numericIdentifier());
|
||||||
chars.consume(DOT);
|
consumeNextCharacter(DOT);
|
||||||
int minor = Integer.parseInt(numericIdentifier());
|
int minor = Integer.parseInt(numericIdentifier());
|
||||||
chars.consume(DOT);
|
consumeNextCharacter(DOT);
|
||||||
int patch = Integer.parseInt(numericIdentifier());
|
int patch = Integer.parseInt(numericIdentifier());
|
||||||
return new NormalVersion(major, minor, patch);
|
return new NormalVersion(major, minor, patch);
|
||||||
}
|
}
|
||||||
|
@ -305,14 +309,16 @@ class VersionParser implements Parser<Version> {
|
||||||
* @throws ParseException if the pre-release version has empty identifier(s)
|
* @throws ParseException if the pre-release version has empty identifier(s)
|
||||||
*/
|
*/
|
||||||
private MetadataVersion parsePreRelease() {
|
private MetadataVersion parsePreRelease() {
|
||||||
|
ensureValidLookahead(DIGIT, LETTER, HYPHEN);
|
||||||
List<String> idents = new ArrayList<String>();
|
List<String> idents = new ArrayList<String>();
|
||||||
do {
|
do {
|
||||||
checkForEmptyIdentifier();
|
|
||||||
idents.add(preReleaseIdentifier());
|
idents.add(preReleaseIdentifier());
|
||||||
if (chars.positiveLookahead(DOT)) {
|
if (chars.positiveLookahead(DOT)) {
|
||||||
chars.consume(DOT);
|
consumeNextCharacter(DOT);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} while (!chars.positiveLookahead(PLUS, EOL));
|
break;
|
||||||
|
} while (true);
|
||||||
return new MetadataVersion(idents.toArray(new String[idents.size()]));
|
return new MetadataVersion(idents.toArray(new String[idents.size()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,6 +335,7 @@ class VersionParser implements Parser<Version> {
|
||||||
* @return a single pre-release identifier
|
* @return a single pre-release identifier
|
||||||
*/
|
*/
|
||||||
private String preReleaseIdentifier() {
|
private String preReleaseIdentifier() {
|
||||||
|
checkForEmptyIdentifier();
|
||||||
CharType boundary = nearestCharType(DOT, PLUS, EOL);
|
CharType boundary = nearestCharType(DOT, PLUS, EOL);
|
||||||
if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
|
if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
|
||||||
return alphanumericIdentifier();
|
return alphanumericIdentifier();
|
||||||
|
@ -353,14 +360,16 @@ class VersionParser implements Parser<Version> {
|
||||||
* @throws ParseException if the build metadata has empty identifier(s)
|
* @throws ParseException if the build metadata has empty identifier(s)
|
||||||
*/
|
*/
|
||||||
private MetadataVersion parseBuild() {
|
private MetadataVersion parseBuild() {
|
||||||
|
ensureValidLookahead(DIGIT, LETTER, HYPHEN);
|
||||||
List<String> idents = new ArrayList<String>();
|
List<String> idents = new ArrayList<String>();
|
||||||
do {
|
do {
|
||||||
checkForEmptyIdentifier();
|
|
||||||
idents.add(buildIdentifier());
|
idents.add(buildIdentifier());
|
||||||
if (chars.positiveLookahead(DOT)) {
|
if (chars.positiveLookahead(DOT)) {
|
||||||
chars.consume(DOT);
|
consumeNextCharacter(DOT);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} while (!chars.positiveLookahead(EOL));
|
break;
|
||||||
|
} while (true);
|
||||||
return new MetadataVersion(idents.toArray(new String[idents.size()]));
|
return new MetadataVersion(idents.toArray(new String[idents.size()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,6 +386,7 @@ class VersionParser implements Parser<Version> {
|
||||||
* @return a single build identifier
|
* @return a single build identifier
|
||||||
*/
|
*/
|
||||||
private String buildIdentifier() {
|
private String buildIdentifier() {
|
||||||
|
checkForEmptyIdentifier();
|
||||||
CharType boundary = nearestCharType(DOT, EOL);
|
CharType boundary = nearestCharType(DOT, EOL);
|
||||||
if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
|
if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
|
||||||
return alphanumericIdentifier();
|
return alphanumericIdentifier();
|
||||||
|
@ -421,7 +431,7 @@ class VersionParser implements Parser<Version> {
|
||||||
private String alphanumericIdentifier() {
|
private String alphanumericIdentifier() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
do {
|
do {
|
||||||
sb.append(chars.consume(DIGIT, LETTER, HYPHEN));
|
sb.append(consumeNextCharacter(DIGIT, LETTER, HYPHEN));
|
||||||
} while (chars.positiveLookahead(DIGIT, LETTER, HYPHEN));
|
} while (chars.positiveLookahead(DIGIT, LETTER, HYPHEN));
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
@ -441,7 +451,7 @@ class VersionParser implements Parser<Version> {
|
||||||
private String digits() {
|
private String digits() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
do {
|
do {
|
||||||
sb.append(chars.consume(DIGIT));
|
sb.append(consumeNextCharacter(DIGIT));
|
||||||
} while (chars.positiveLookahead(DIGIT));
|
} while (chars.positiveLookahead(DIGIT));
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
@ -471,7 +481,7 @@ class VersionParser implements Parser<Version> {
|
||||||
private void checkForLeadingZeroes() {
|
private void checkForLeadingZeroes() {
|
||||||
Character la1 = chars.lookahead(1);
|
Character la1 = chars.lookahead(1);
|
||||||
Character la2 = chars.lookahead(2);
|
Character la2 = chars.lookahead(2);
|
||||||
if (la1 == '0' && DIGIT.isMatchedBy(la2)) {
|
if (la1 != null && la1 == '0' && DIGIT.isMatchedBy(la2)) {
|
||||||
throw new ParseException(
|
throw new ParseException(
|
||||||
"Numeric identifier MUST NOT contain leading zeroes"
|
"Numeric identifier MUST NOT contain leading zeroes"
|
||||||
);
|
);
|
||||||
|
@ -485,8 +495,39 @@ class VersionParser implements Parser<Version> {
|
||||||
* metadata have empty identifier(s)
|
* metadata have empty identifier(s)
|
||||||
*/
|
*/
|
||||||
private void checkForEmptyIdentifier() {
|
private void checkForEmptyIdentifier() {
|
||||||
if (DOT.isMatchedBy(chars.lookahead(1))) {
|
Character la = chars.lookahead(1);
|
||||||
throw new ParseException("Identifiers MUST NOT be empty");
|
if (DOT.isMatchedBy(la) || PLUS.isMatchedBy(la) || EOL.isMatchedBy(la)) {
|
||||||
|
throw new ParseException(
|
||||||
|
"Identifiers MUST NOT be empty",
|
||||||
|
new UnexpectedCharacterException(la, DIGIT, LETTER, HYPHEN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to consume the next character in the stream.
|
||||||
|
*
|
||||||
|
* @param expected the expected types of the next character
|
||||||
|
* @return the next character in the stream
|
||||||
|
* @throws UnexpectedCharacterException if the next element is of an unexpected type
|
||||||
|
*/
|
||||||
|
private Character consumeNextCharacter(CharType... expected) {
|
||||||
|
try {
|
||||||
|
return chars.consume(expected);
|
||||||
|
} catch (UnexpectedElementException e) {
|
||||||
|
throw new UnexpectedCharacterException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the next character in the stream is valid.
|
||||||
|
*
|
||||||
|
* @param expected the expected types of the next character
|
||||||
|
* @throws UnexpectedCharacterException if the next element is not valid
|
||||||
|
*/
|
||||||
|
private void ensureValidLookahead(CharType... expected) {
|
||||||
|
if (!chars.positiveLookahead(expected)) {
|
||||||
|
throw new UnexpectedCharacterException(chars.lookahead(1), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
*
|
||||||
|
* Copyright 2014 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;
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.VersionParser.CharType;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
import org.junit.runners.Parameterized.Parameters;
|
||||||
|
import static com.github.zafarkhaja.semver.VersionParser.CharType.*;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Zafar Khaja <zafarkhaja@gmail.com>
|
||||||
|
*/
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class ParserErrorHandlingTest {
|
||||||
|
|
||||||
|
private final String invalidVersion;
|
||||||
|
private final Character unexpected;
|
||||||
|
private final CharType[] expected;
|
||||||
|
|
||||||
|
public ParserErrorHandlingTest(
|
||||||
|
String invalidVersion,
|
||||||
|
Character unexpected,
|
||||||
|
CharType[] expected
|
||||||
|
) {
|
||||||
|
this.invalidVersion = invalidVersion;
|
||||||
|
this.unexpected = unexpected;
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCorrectlyHandleParseErrors() {
|
||||||
|
try {
|
||||||
|
VersionParser.parseValidSemVer(invalidVersion);
|
||||||
|
} catch (UnexpectedCharacterException e) {
|
||||||
|
assertEquals(unexpected, e.getUnexpectedCharacter());
|
||||||
|
assertArrayEquals(expected, e.getExpectedCharTypes());
|
||||||
|
return;
|
||||||
|
} catch (ParseException e) {
|
||||||
|
if (e.getCause() != null) {
|
||||||
|
UnexpectedCharacterException cause = (UnexpectedCharacterException) e.getCause();
|
||||||
|
assertEquals(unexpected, cause.getUnexpectedCharacter());
|
||||||
|
assertArrayEquals(expected, cause.getExpectedCharTypes());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail("Uncaught exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parameters(name = "{0}")
|
||||||
|
public static Collection<Object[]> parameters() {
|
||||||
|
return Arrays.asList(new Object[][] {
|
||||||
|
{ "1", null, new CharType[] { DOT } },
|
||||||
|
{ "1 ", ' ', new CharType[] { DOT } },
|
||||||
|
{ "1.", null, new CharType[] { DIGIT } },
|
||||||
|
{ "1.2", null, new CharType[] { DOT } },
|
||||||
|
{ "1.2.", null, new CharType[] { DIGIT } },
|
||||||
|
{ "a.b.c", 'a', new CharType[] { DIGIT } },
|
||||||
|
{ "1.b.c", 'b', new CharType[] { DIGIT } },
|
||||||
|
{ "1.2.c", 'c', new CharType[] { DIGIT } },
|
||||||
|
{ "!.2.3", '!', new CharType[] { DIGIT } },
|
||||||
|
{ "1.!.3", '!', new CharType[] { DIGIT } },
|
||||||
|
{ "1.2.!", '!', new CharType[] { DIGIT } },
|
||||||
|
{ "v1.2.3", 'v', new CharType[] { DIGIT } },
|
||||||
|
{ "1.2.3-", null, new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2. 3", ' ', new CharType[] { DIGIT } },
|
||||||
|
{ "1.2.3=alpha", '=', new CharType[] { HYPHEN, PLUS, EOL } },
|
||||||
|
{ "1.2.3~beta", '~', new CharType[] { HYPHEN, PLUS, EOL } },
|
||||||
|
{ "1.2.3-be$ta", '$', new CharType[] { PLUS, EOL } },
|
||||||
|
{ "1.2.3+b1+b2", '+', new CharType[] { EOL } },
|
||||||
|
{ "1.2.3-rc!", '!', new CharType[] { PLUS, EOL } },
|
||||||
|
{ "1.2.3-+", '+', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3-@", '@', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3+@", '@', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3-rc1.", null, new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3+20140620.", null, new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3-b.+b", '+', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3-rc..", '.', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
{ "1.2.3-rc+bld..", '.', new CharType[] { DIGIT, LETTER, HYPHEN } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user