diff --git a/src/main/java/com/github/zafarkhaja/semver/ParserException.java b/src/main/java/com/github/zafarkhaja/semver/ParserException.java index 469c060..e839b20 100644 --- a/src/main/java/com/github/zafarkhaja/semver/ParserException.java +++ b/src/main/java/com/github/zafarkhaja/semver/ParserException.java @@ -29,11 +29,11 @@ package com.github.zafarkhaja.semver; */ public class ParserException extends RuntimeException { - ParserException(String message) { + public ParserException(String message) { super(message); } - ParserException() { + public ParserException() { } } diff --git a/src/main/java/com/github/zafarkhaja/semver/VersionParser.java b/src/main/java/com/github/zafarkhaja/semver/VersionParser.java index ef28f61..a79a0d0 100644 --- a/src/main/java/com/github/zafarkhaja/semver/VersionParser.java +++ b/src/main/java/com/github/zafarkhaja/semver/VersionParser.java @@ -23,7 +23,7 @@ */ package com.github.zafarkhaja.semver; -import com.github.zafarkhaja.semver.VersionParser.CharStream; +import com.github.zafarkhaja.semver.util.Stream; import java.util.ArrayList; import java.util.List; import static com.github.zafarkhaja.semver.VersionParser.Char.*; @@ -34,127 +34,70 @@ import static com.github.zafarkhaja.semver.VersionParser.Char.*; */ class VersionParser implements Parser { - static class CharStream { - - static interface CharType { - boolean isMatchedBy(char chr); - } - - private final char[] data; - - private int offset = 0; - - static final char EOL = (char) -1; - - CharStream(String input) { - data = input.toCharArray(); - } - - char consume() { - if (offset + 1 <= data.length) { - return data[offset++]; - } - return EOL; - } - - char consume(CharType... expected) { - char la = lookahead(1); - for (CharType charType : expected) { - if (charType.isMatchedBy(la)) { - return consume(); - } - } - throw new UnexpectedCharacterException(la, expected); - } - - char lookahead() { - return lookahead(1); - } - - char lookahead(int pos) { - int idx = offset + pos - 1; - if (idx < data.length) { - return data[idx]; - } - return EOL; - } - - boolean positiveLookahead(CharType... expected) { - char la = lookahead(1); - for (CharType charType : expected) { - if (charType.isMatchedBy(la)) { - return true; - } - } - return false; - } - - boolean positiveLookaheadBefore(CharType before, CharType... expected) { - char la; - for (int i = 1; i <= data.length; i++) { - la = lookahead(i); - if (before.isMatchedBy(la)) { - break; - } - for (CharType charType : expected) { - if (charType.isMatchedBy(la)) { - return true; - } - } - } - return false; - } - - char[] toArray() { - return data.clone(); - } - } - - static enum Char implements CharStream.CharType { + static enum Char implements Stream.ElementType { DIGIT { @Override - public boolean isMatchedBy(char chr) { + public boolean isMatchedBy(Character chr) { + if (chr == null) { + return false; + } return chr >= '0' && chr <= '9'; } }, LETTER { @Override - public boolean isMatchedBy(char chr) { + public boolean isMatchedBy(Character chr) { + if (chr == null) { + return false; + } return (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z'); } }, DOT { @Override - public boolean isMatchedBy(char chr) { + public boolean isMatchedBy(Character chr) { + if (chr == null) { + return false; + } return chr == '.'; } }, HYPHEN { @Override - public boolean isMatchedBy(char chr) { + public boolean isMatchedBy(Character chr) { + if (chr == null) { + return false; + } return chr == '-'; } }, PLUS { @Override - public boolean isMatchedBy(char chr) { + public boolean isMatchedBy(Character chr) { + if (chr == null) { + return false; + } return chr == '+'; } }, EOL { @Override - public boolean isMatchedBy(char chr) { - return chr == CharStream.EOL; + public boolean isMatchedBy(Character chr) { + return chr == null; } }; } - private final CharStream chars; + private final Stream chars; VersionParser(String input) { - chars = new CharStream(input); + Character[] elements = new Character[input.length()]; + for (int i = 0; i < input.length(); i++) { + elements[i] = Character.valueOf(input.charAt(i)); + } + chars = new Stream(elements); } @Override @@ -298,8 +241,8 @@ class VersionParser implements Parser { } private void checkForLeadingZeroes() { - char la1 = chars.lookahead(1); - char la2 = chars.lookahead(2); + Character la1 = chars.lookahead(1); + Character la2 = chars.lookahead(2); if (la1 == '0' && DIGIT.isMatchedBy(la2)) { throw new GrammarException( "Numeric identifier MUST NOT contain leading zeroes" diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java b/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java new file mode 100644 index 0000000..c6bff31 --- /dev/null +++ b/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java @@ -0,0 +1,145 @@ +/* + * The MIT License + * + * Copyright 2013 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.util.Stream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author Zafar Khaja + */ +class Lexer { + + static class Token { + + enum Type implements Stream.ElementType { + + NUMERIC("0|[1-9][0-9]*"), + DOT("\\."), + HYPHEN("-"), + EQUAL("="), + NOT_EQUAL("!="), + GREATER(">(?!=)"), + GREATER_EQUAL(">="), + LESS("<(?!=)"), + LESS_EQUAL("<="), + TILDE("~"), + STAR("\\*"), + AND("&"), + OR("\\|"), + NOT("!(?!=)"), + LEFT_PAREN("\\("), + RIGHT_PAREN("\\)"), + WHITESPACE("\\s+"), + EOL("?!") { + @Override + public boolean isMatchedBy(Token token) { + return token == null; + } + }; + + final Pattern pattern; + + private Type(String regexp) { + pattern = Pattern.compile("^(" + regexp + ")"); + } + + @Override + public String toString() { + return name() + "(" + pattern + ")"; + } + + @Override + public boolean isMatchedBy(Token token) { + if (token == null) { + return false; + } + return this == token.type; + } + } + + final Type type; + final String lexeme; + + Token(Type type, String lexeme) { + this.type = type; + this.lexeme = (lexeme == null) ? "" : lexeme; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Token)) { + return false; + } + Token token = (Token) other; + return type.equals(token.type) && lexeme.equals(token.lexeme); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 71 * hash + type.hashCode(); + hash = 71 * hash + lexeme.hashCode(); + return hash; + } + + @Override + public String toString() { + return type.name() + "(" + lexeme + ")"; + } + } + + Lexer() { + + } + + Stream tokenize(String input) { + List tokens = new ArrayList(); + while (!input.isEmpty()) { + boolean matched = false; + for (Token.Type tokenType : Token.Type.values()) { + Matcher matcher = tokenType.pattern.matcher(input); + if (matcher.find()) { + matched = true; + input = matcher.replaceFirst(""); + if (tokenType != Token.Type.WHITESPACE) { + tokens.add(new Token(tokenType, matcher.group())); + } + break; + } + } + if (!matched) { + throw new LexerException(input); + } + } + return new Stream(tokens.toArray(new Token[tokens.size()])); + } +} diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/LexerException.java b/src/main/java/com/github/zafarkhaja/semver/expr/LexerException.java new file mode 100644 index 0000000..2927c56 --- /dev/null +++ b/src/main/java/com/github/zafarkhaja/semver/expr/LexerException.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright 2013 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; + +/** + * + * @author Zafar Khaja + */ +public class LexerException extends RuntimeException { + + private final String expr; + + LexerException(String expr) { + this.expr = expr; + } + + @Override + public String toString() { + return "Illegal character near '" + expr + "'"; + } +} diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java b/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java new file mode 100644 index 0000000..67ebd55 --- /dev/null +++ b/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java @@ -0,0 +1,52 @@ +/* + * The MIT License + * + * Copyright 2013 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.ParserException; +import com.github.zafarkhaja.semver.expr.Lexer.*; +import java.util.Arrays; + +/** + * + * @author Zafar Khaja + */ +public class UnexpectedTokenException extends ParserException { + + private final Token unexpected; + private final Token.Type[] expected; + + UnexpectedTokenException(Token token, Token.Type... expected) { + unexpected = token; + this.expected = expected; + } + + @Override + public String toString() { + String message = "Unexpected token '" + unexpected + "'"; + if (expected.length > 0) { + message += ", expecting '" + Arrays.toString(expected) + "'"; + } + return message; + } +} diff --git a/src/main/java/com/github/zafarkhaja/semver/util/Stream.java b/src/main/java/com/github/zafarkhaja/semver/util/Stream.java new file mode 100644 index 0000000..50dddf7 --- /dev/null +++ b/src/main/java/com/github/zafarkhaja/semver/util/Stream.java @@ -0,0 +1,148 @@ +/* + * The MIT License + * + * Copyright 2013 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.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * + * @author Zafar Khaja + */ +public class Stream implements Iterable { + + public static interface ElementType { + boolean isMatchedBy(E element); + } + + private final E[] elements; + + private int offset = 0; + + public Stream(E[] elements) { + this.elements = elements.clone(); + } + + public E consume() { + if (offset >= elements.length) { + return null; + } + return elements[offset++]; + } + + public E consume(ElementType... expected) { + E lookahead = lookahead(1); + for (ElementType type : expected) { + if (type.isMatchedBy(lookahead)) { + return consume(); + } + } + throw new UnexpectedElementTypeException(lookahead, expected); + } + + public E lookahead() { + return lookahead(1); + } + + public E lookahead(int position) { + int idx = offset + position - 1; + if (idx < elements.length) { + return elements[idx]; + } + return null; + } + + public boolean positiveLookahead(ElementType... expected) { + for (ElementType type : expected) { + if (type.isMatchedBy(lookahead(1))) { + return true; + } + } + return false; + } + + public boolean positiveLookaheadBefore( + ElementType before, + ElementType... expected + ) { + E lookahead; + for (int i = 1; i <= elements.length; i++) { + lookahead = lookahead(i); + if (before.isMatchedBy(lookahead)) { + break; + } + for (ElementType type : expected) { + if (type.isMatchedBy(lookahead)) { + return true; + } + } + } + return false; + } + + public boolean positiveLookaheadUntil( + int until, + ElementType... expected + ) { + for (int i = 1; i <= until; i++) { + for (ElementType type : expected) { + if (type.isMatchedBy(lookahead(i))) { + return true; + } + } + } + return false; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private int index = offset; + + @Override + public boolean hasNext() { + return index < elements.length; + } + + @Override + public E next() { + if (index >= elements.length) { + throw new NoSuchElementException(); + } + return elements[index++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public E[] toArray() { + return Arrays.copyOfRange(elements, offset, elements.length); + } +} diff --git a/src/main/java/com/github/zafarkhaja/semver/UnexpectedCharacterException.java b/src/main/java/com/github/zafarkhaja/semver/util/UnexpectedElementTypeException.java similarity index 77% rename from src/main/java/com/github/zafarkhaja/semver/UnexpectedCharacterException.java rename to src/main/java/com/github/zafarkhaja/semver/util/UnexpectedElementTypeException.java index f52fda0..7eb5bcb 100644 --- a/src/main/java/com/github/zafarkhaja/semver/UnexpectedCharacterException.java +++ b/src/main/java/com/github/zafarkhaja/semver/util/UnexpectedElementTypeException.java @@ -21,28 +21,28 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.github.zafarkhaja.semver; +package com.github.zafarkhaja.semver.util; -import com.github.zafarkhaja.semver.VersionParser.CharStream.CharType; +import com.github.zafarkhaja.semver.util.Stream.ElementType; import java.util.Arrays; /** * * @author Zafar Khaja */ -public class UnexpectedCharacterException extends ParserException { +public class UnexpectedElementTypeException extends RuntimeException { - private final char unexpected; - private final CharType[] expected; + private final Object unexpected; + private final ElementType[] expected; - UnexpectedCharacterException(char chr, CharType... expected) { - unexpected = chr; + UnexpectedElementTypeException(Object element, ElementType... expected) { + unexpected = element; this.expected = expected; } @Override public String toString() { - String message = "Unexpected character '" + unexpected + "'"; + String message = "Unexpected element '" + unexpected + "'"; if (expected.length > 0) { message += ", expecting '" + Arrays.toString(expected) + "'"; } diff --git a/src/test/java/com/github/zafarkhaja/semver/VersionParserCharStreamTest.java b/src/test/java/com/github/zafarkhaja/semver/VersionParserCharStreamTest.java deleted file mode 100644 index 1c3bf77..0000000 --- a/src/test/java/com/github/zafarkhaja/semver/VersionParserCharStreamTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * The MIT License - * - * Copyright 2013 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; - -import com.github.zafarkhaja.semver.VersionParser.Char; -import com.github.zafarkhaja.semver.VersionParser.CharStream; -import org.junit.Test; -import static org.junit.Assert.*; - -/** - * - * @author Zafar Khaja - */ -public class VersionParserCharStreamTest { - - @Test - public void shouldBeBackedByCharArray() { - String input = "abc"; - CharStream chars = new CharStream(input); - assertArrayEquals(input.toCharArray(), chars.toArray()); - } - - @Test - public void shouldNotReturnRealCharArray() { - CharStream chars = new CharStream("abc"); - char[] charArray = chars.toArray(); - charArray[0] = 'z'; - assertEquals('z', charArray[0]); - assertEquals('a', chars.lookahead()); - } - - @Test - public void shouldConsumeCharactersOneByOne() { - CharStream chars = new CharStream("abc"); - assertEquals('a', chars.consume()); - assertEquals('b', chars.consume()); - assertEquals('c', chars.consume()); - } - - @Test - public void shouldReturnEolWhenNothingLeftToConsume() { - CharStream chars = new CharStream("abc"); - assertEquals('a', chars.consume()); - assertEquals('b', chars.consume()); - assertEquals('c', chars.consume()); - assertEquals(CharStream.EOL, chars.consume()); - } - - @Test - public void shouldRaiseErrorWhenUnexpectedCharTypeConsumed() { - CharStream chars = new CharStream("abc"); - try { - chars.consume(Char.DIGIT); - } catch (UnexpectedCharacterException e) { - return; - } - fail("Should raise error when unexpected character type is consumed"); - } - - @Test - public void shouldLookaheadWithoutConsuming() { - CharStream chars = new CharStream("abc"); - assertEquals('a', chars.lookahead()); - assertEquals('a', chars.lookahead()); - } - - @Test - public void shouldLookaheadArbitraryNumberOfCharacters() { - CharStream chars = new CharStream("abc"); - assertEquals('a', chars.lookahead(1)); - assertEquals('b', chars.lookahead(2)); - assertEquals('c', chars.lookahead(3)); - } - - @Test - public void shouldReturnEolWhenNothingLeftToLookahead() { - CharStream chars = new CharStream("abc"); - assertEquals('a', chars.consume()); - assertEquals('b', chars.consume()); - assertEquals('c', chars.consume()); - assertEquals(CharStream.EOL, chars.lookahead()); - } - - @Test - public void shouldCheckIfLookaheadIsOfExpectedTypes() { - CharStream chars = new CharStream("abc"); - assertTrue(chars.positiveLookahead(Char.LETTER)); - assertFalse(chars.positiveLookahead(Char.DIGIT, Char.PLUS)); - } - - @Test - public void shouldCheckIfCharOfExpectedTypesExistBeforeGivenType() { - CharStream chars = new CharStream("1.0.0"); - assertTrue(chars.positiveLookaheadBefore(Char.EOL, Char.DOT)); - assertFalse(chars.positiveLookaheadBefore(Char.EOL, Char.LETTER)); - } -} diff --git a/src/test/java/com/github/zafarkhaja/semver/VersionParserCharTest.java b/src/test/java/com/github/zafarkhaja/semver/VersionParserCharTest.java index 3d4741c..d6ccd1b 100644 --- a/src/test/java/com/github/zafarkhaja/semver/VersionParserCharTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/VersionParserCharTest.java @@ -23,9 +23,8 @@ */ package com.github.zafarkhaja.semver; -import com.github.zafarkhaja.semver.VersionParser.Char; -import com.github.zafarkhaja.semver.VersionParser.CharStream; import org.junit.Test; +import static com.github.zafarkhaja.semver.VersionParser.Char.*; import static org.junit.Assert.*; /** @@ -36,49 +35,49 @@ public class VersionParserCharTest { @Test public void shouldBeMatchedByDigit() { - assertTrue(Char.DIGIT.isMatchedBy('0')); - assertTrue(Char.DIGIT.isMatchedBy('9')); - assertFalse(Char.DIGIT.isMatchedBy('a')); - assertFalse(Char.DIGIT.isMatchedBy('A')); + assertTrue(DIGIT.isMatchedBy('0')); + assertTrue(DIGIT.isMatchedBy('9')); + assertFalse(DIGIT.isMatchedBy('a')); + assertFalse(DIGIT.isMatchedBy('A')); } @Test public void shouldBeMatchedByLetter() { - assertTrue(Char.LETTER.isMatchedBy('a')); - assertTrue(Char.LETTER.isMatchedBy('A')); - assertFalse(Char.LETTER.isMatchedBy('0')); - assertFalse(Char.LETTER.isMatchedBy('9')); + assertTrue(LETTER.isMatchedBy('a')); + assertTrue(LETTER.isMatchedBy('A')); + assertFalse(LETTER.isMatchedBy('0')); + assertFalse(LETTER.isMatchedBy('9')); } @Test public void shouldBeMatchedByDot() { - assertTrue(Char.DOT.isMatchedBy('.')); - assertFalse(Char.DOT.isMatchedBy('-')); - assertFalse(Char.DOT.isMatchedBy('0')); - assertFalse(Char.DOT.isMatchedBy('9')); + assertTrue(DOT.isMatchedBy('.')); + assertFalse(DOT.isMatchedBy('-')); + assertFalse(DOT.isMatchedBy('0')); + assertFalse(DOT.isMatchedBy('9')); } @Test public void shouldBeMatchedByHyphen() { - assertTrue(Char.HYPHEN.isMatchedBy('-')); - assertFalse(Char.HYPHEN.isMatchedBy('+')); - assertFalse(Char.HYPHEN.isMatchedBy('a')); - assertFalse(Char.HYPHEN.isMatchedBy('0')); + assertTrue(HYPHEN.isMatchedBy('-')); + assertFalse(HYPHEN.isMatchedBy('+')); + assertFalse(HYPHEN.isMatchedBy('a')); + assertFalse(HYPHEN.isMatchedBy('0')); } @Test public void shouldBeMatchedByPlus() { - assertTrue(Char.PLUS.isMatchedBy('+')); - assertFalse(Char.PLUS.isMatchedBy('-')); - assertFalse(Char.PLUS.isMatchedBy('a')); - assertFalse(Char.PLUS.isMatchedBy('0')); + assertTrue(PLUS.isMatchedBy('+')); + assertFalse(PLUS.isMatchedBy('-')); + assertFalse(PLUS.isMatchedBy('a')); + assertFalse(PLUS.isMatchedBy('0')); } @Test public void shouldBeMatchedByEol() { - assertTrue(Char.EOL.isMatchedBy(CharStream.EOL)); - assertFalse(Char.EOL.isMatchedBy('-')); - assertFalse(Char.EOL.isMatchedBy('a')); - assertFalse(Char.EOL.isMatchedBy('0')); + assertTrue(EOL.isMatchedBy(null)); + assertFalse(EOL.isMatchedBy('-')); + assertFalse(EOL.isMatchedBy('a')); + assertFalse(EOL.isMatchedBy('0')); } } diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java new file mode 100644 index 0000000..1da42c7 --- /dev/null +++ b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright 2013 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.expr.Lexer.*; +import com.github.zafarkhaja.semver.util.Stream; +import org.junit.Test; +import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*; +import static org.junit.Assert.*; + +/** + * + * @author Zafar Khaja + */ +public class LexerTest { + + @Test + public void shouldTokenizeVersionString() { + Token[] expected = { + new Token(GREATER, ">"), + new Token(NUMERIC, "1"), + new Token(DOT, "."), + new Token(NUMERIC, "0"), + new Token(DOT, "."), + new Token(NUMERIC, "0"), + }; + Lexer lexer = new Lexer(); + Stream stream = lexer.tokenize(">1.0.0"); + assertArrayEquals(expected, stream.toArray()); + } + + @Test + public void shouldSkipWhitespaces() { + Token[] expected = { + new Token(GREATER, ">"), + new Token(NUMERIC, "1"), + }; + Lexer lexer = new Lexer(); + Stream stream = lexer.tokenize("> 1"); + assertArrayEquals(expected, stream.toArray()); + } + + @Test + public void shouldRaiseErrorOnIllegalCharacter() { + Lexer lexer = new Lexer(); + try { + lexer.tokenize("@1.0.0"); + } catch (LexerException e) { + return; + } + fail("Should raise error on illegal character"); + } +} diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java new file mode 100644 index 0000000..36c4729 --- /dev/null +++ b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java @@ -0,0 +1,113 @@ +/* + * The MIT License + * + * Copyright 2013 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.expr.Lexer.Token; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*; +import static org.junit.Assert.*; + +/** + * + * @author Zafar Khaja + */ +@RunWith(Enclosed.class) +public class LexerTokenTest { + + public static class EqualsMethodTest { + + @Test + public void shouldBeReflexive() { + Token token = new Token(NUMERIC, "1"); + assertTrue(token.equals(token)); + } + + @Test + public void shouldBeSymmetric() { + Token t1 = new Token(EQUAL, "="); + Token t2 = new Token(EQUAL, "="); + assertTrue(t1.equals(t2)); + assertTrue(t2.equals(t1)); + } + + @Test + public void shouldBeTransitive() { + Token t1 = new Token(GREATER, ">"); + Token t2 = new Token(GREATER, ">"); + Token t3 = new Token(GREATER, ">"); + assertTrue(t1.equals(t2)); + assertTrue(t2.equals(t3)); + assertTrue(t1.equals(t3)); + } + + @Test + public void shouldBeConsistent() { + Token t1 = new Token(HYPHEN, "-"); + Token t2 = new Token(HYPHEN, "-"); + assertTrue(t1.equals(t2)); + assertTrue(t1.equals(t2)); + assertTrue(t1.equals(t2)); + } + + @Test + public void shouldReturnFalseIfOtherVersionIsOfDifferentType() { + Token t1 = new Token(DOT, "."); + assertFalse(t1.equals(new String("."))); + } + + @Test + public void shouldReturnFalseIfOtherVersionIsNull() { + Token t1 = new Token(AND, "&"); + Token t2 = null; + assertFalse(t1.equals(t2)); + } + + @Test + public void shouldReturnFalseIfTypesAreDifferent() { + Token t1 = new Token(EQUAL, "="); + Token t2 = new Token(NOT_EQUAL, "!="); + assertFalse(t1.equals(t2)); + } + + @Test + public void shouldReturnFalseIfLexemesAreDifferent() { + Token t1 = new Token(NUMERIC, "1"); + Token t2 = new Token(NUMERIC, "2"); + assertFalse(t1.equals(t2)); + } + } + + public static class HashCodeMethodTest { + + @Test + public void shouldReturnSameHashCodeIfTokensAreEqual() { + Token t1 = new Token(NUMERIC, "1"); + Token t2 = new Token(NUMERIC, "1"); + assertTrue(t1.equals(t2)); + assertEquals(t1.hashCode(), t2.hashCode()); + } + } +} diff --git a/src/test/java/com/github/zafarkhaja/semver/util/StreamTest.java b/src/test/java/com/github/zafarkhaja/semver/util/StreamTest.java new file mode 100644 index 0000000..2ea8642 --- /dev/null +++ b/src/test/java/com/github/zafarkhaja/semver/util/StreamTest.java @@ -0,0 +1,206 @@ +/* + * The MIT License + * + * Copyright 2013 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.util; + +import com.github.zafarkhaja.semver.util.Stream.ElementType; +import java.util.Iterator; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Zafar Khaja + */ +public class StreamTest { + + @Test + public void shouldBeBackedByArray() { + Character[] input = {'a', 'b', 'c'}; + Stream stream = new Stream(input); + assertArrayEquals(input, stream.toArray()); + } + + @Test + public void shouldImplementIterable() { + Character[] input = {'a', 'b', 'c'}; + Stream stream = new Stream(input); + Iterator it = stream.iterator(); + for (Character chr : input) { + assertEquals(chr, it.next()); + } + assertFalse(it.hasNext()); + } + + @Test + public void shouldNotReturnRealElementsArray() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + Character[] charArray = stream.toArray(); + charArray[0] = Character.valueOf('z'); + assertEquals(Character.valueOf('z'), charArray[0]); + assertEquals(Character.valueOf('a'), stream.toArray()[0]); + } + + @Test + public void shouldReturnArrayOfElementsThatAreLeftInStream() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + stream.consume(); + stream.consume(); + assertEquals(1, stream.toArray().length); + assertEquals(Character.valueOf('c'), stream.toArray()[0]); + } + + @Test + public void shouldConsumeElementsOneByOne() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + assertEquals(Character.valueOf('a'), stream.consume()); + assertEquals(Character.valueOf('b'), stream.consume()); + assertEquals(Character.valueOf('c'), stream.consume()); + } + + @Test + public void shouldRaiseErrorWhenUnexpectedElementTypeConsumed() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + try { + stream.consume(new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return false; + } + }); + } catch (UnexpectedElementTypeException e) { + return; + } + fail("Should raise error when unexpected element type is consumed"); + } + + @Test + public void shouldLookaheadWithoutConsuming() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + assertEquals(Character.valueOf('a'), stream.lookahead()); + assertEquals(Character.valueOf('a'), stream.lookahead()); + } + + @Test + public void shouldLookaheadArbitraryNumberOfElements() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + assertEquals(Character.valueOf('a'), stream.lookahead(1)); + assertEquals(Character.valueOf('b'), stream.lookahead(2)); + assertEquals(Character.valueOf('c'), stream.lookahead(3)); + } + + @Test + public void shouldCheckIfLookaheadIsOfExpectedTypes() { + Stream stream = new Stream( + new Character[] {'a', 'b', 'c'} + ); + assertTrue(stream.positiveLookahead( + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == 'a'; + } + } + )); + assertFalse(stream.positiveLookahead( + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == 'c'; + } + } + )); + } + + @Test + public void shouldCheckIfElementOfExpectedTypesExistBeforeGivenType() { + Stream stream = new Stream( + new Character[] {'1', '.', '0', '.', '0'} + ); + assertTrue(stream.positiveLookaheadBefore( + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == '.'; + } + }, + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == '1'; + } + } + )); + assertFalse(stream.positiveLookaheadBefore( + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == '1'; + } + }, + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == '.'; + } + } + )); + } + + @Test + public void shouldCheckIfElementOfExpectedTypesExistUntilGivenPosition() { + Stream stream = new Stream( + new Character[] {'1', '.', '0', '.', '0'} + ); + assertTrue(stream.positiveLookaheadUntil( + 3, + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == '0'; + } + } + )); + assertFalse(stream.positiveLookaheadUntil( + 3, + new ElementType() { + @Override + public boolean isMatchedBy(Character element) { + return element == 'a'; + } + } + )); + } +}