diff --git a/src/main/java/com/garbagemule/MobArena/things/ParserUtil.java b/src/main/java/com/garbagemule/MobArena/things/ParserUtil.java new file mode 100644 index 0000000..13f0c3f --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ParserUtil.java @@ -0,0 +1,95 @@ +package com.garbagemule.MobArena.things; + +import java.util.ArrayList; +import java.util.List; + +class ParserUtil { + + static String extractBetween(String s, char left, char right) { + int start = s.indexOf(left); + if (start < 0) { + throw new IllegalArgumentException("Missing start symbol " + left); + } + + int end = s.lastIndexOf(right); + if (end < 0) { + throw new IllegalArgumentException("Missing end symbol " + right); + } + + return s.substring(start + 1, end).trim(); + } + + static List split(String s) { + List result = new ArrayList<>(); + int start = 0; + int parens = 0; + int brackets = 0; + int curlies = 0; + int angles = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == ',') { + if (parens == 0 && brackets == 0 && curlies == 0 && angles == 0) { + String part = s.substring(start, i).trim(); + if (!part.isEmpty()) { + result.add(part); + } + start = i + 1; + } + } else if (c == '(') { + parens++; + } else if (c == ')') { + parens--; + if (parens < 0) { + throw new IllegalArgumentException("Unmatched right parenthesis )"); + } + } else if (c == '[') { + brackets++; + } else if (c == ']') { + brackets--; + if (brackets < 0) { + throw new IllegalArgumentException("Unmatched right square bracket ]"); + } + } else if (c == '{') { + curlies++; + } else if (c == '}') { + curlies--; + if (curlies < 0) { + throw new IllegalArgumentException("Unmatched right curly brace }"); + } + } else if (c == '<') { + angles++; + } else if (c == '>') { + angles--; + if (angles < 0) { + throw new IllegalArgumentException("Unmatched right angle bracket >"); + } + } + } + if (parens > 0) { + throw new IllegalArgumentException("Unmatched left parenthesis ("); + } + if (brackets > 0) { + throw new IllegalArgumentException("Unmatched left square bracket ["); + } + if (curlies > 0) { + throw new IllegalArgumentException("Unmatched left curly brace {"); + } + if (angles > 0) { + throw new IllegalArgumentException("Unmatched left angle bracket <"); + } + if (start == 0) { + String part = s.trim(); + if (!part.isEmpty()) { + result.add(part); + } + } else { + String part = s.substring(start).trim(); + if (!part.isEmpty()) { + result.add(part); + } + } + return result; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/RandomThingPickerParser.java b/src/main/java/com/garbagemule/MobArena/things/RandomThingPickerParser.java new file mode 100644 index 0000000..ca43981 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/RandomThingPickerParser.java @@ -0,0 +1,43 @@ +package com.garbagemule.MobArena.things; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +public class RandomThingPickerParser implements ThingPickerParser { + + private final ThingPickerParser parser; + private final Random random; + + public RandomThingPickerParser( + ThingPickerParser parser, + Random random + ) { + this.parser = parser; + this.random = random; + } + + @Override + public ThingPicker parse(String s) { + if (!(s.startsWith("random(") && s.endsWith(")"))) { + return null; + } + + String inner = ParserUtil.extractBetween(s, '(', ')'); + List pickers = ParserUtil.split(inner) + .stream() + .map(String::trim) + .map(parser::parse) + .collect(Collectors.toList()); + + if (pickers.isEmpty()) { + throw new IllegalArgumentException("Nothing to pick from: " + s); + } + if (pickers.size() == 1) { + return pickers.get(0); + } + + return new RandomThingPicker(pickers, random); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ThingGroupPickerParser.java b/src/main/java/com/garbagemule/MobArena/things/ThingGroupPickerParser.java new file mode 100644 index 0000000..e27730e --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ThingGroupPickerParser.java @@ -0,0 +1,37 @@ +package com.garbagemule.MobArena.things; + +import java.util.List; +import java.util.stream.Collectors; + +public class ThingGroupPickerParser implements ThingPickerParser { + + private final ThingPickerParser parser; + + public ThingGroupPickerParser(ThingPickerParser parser) { + this.parser = parser; + } + + @Override + public ThingPicker parse(String s) { + if (!(s.startsWith("all(") && s.endsWith(")"))) { + return null; + } + + String inner = ParserUtil.extractBetween(s, '(', ')'); + List pickers = ParserUtil.split(inner) + .stream() + .map(String::trim) + .map(parser::parse) + .collect(Collectors.toList()); + + if (pickers.isEmpty()) { + throw new IllegalArgumentException("Nothing to group: " + s); + } + if (pickers.size() == 1) { + return pickers.get(0); + } + + return new ThingGroupPicker(pickers); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ThingPickerParser.java b/src/main/java/com/garbagemule/MobArena/things/ThingPickerParser.java new file mode 100644 index 0000000..e1e40fd --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ThingPickerParser.java @@ -0,0 +1,14 @@ +package com.garbagemule.MobArena.things; + +public interface ThingPickerParser { + + /** + * Parse the given string, returning a {@link ThingPicker} instance on + * success, otherwise null. + * + * @param s a string to parse + * @return an instance of {@link ThingPicker}, or null + */ + ThingPicker parse(String s); + +} diff --git a/src/test/java/com/garbagemule/MobArena/things/ParserUtilTest.java b/src/test/java/com/garbagemule/MobArena/things/ParserUtilTest.java new file mode 100644 index 0000000..882af8d --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/ParserUtilTest.java @@ -0,0 +1,224 @@ +package com.garbagemule.MobArena.things; + +import org.hamcrest.collection.IsEmptyCollection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +public class ParserUtilTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void extractBetweenThrowsIfNoLeftSymbol() { + String input = "a, b, c)"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.extractBetween(input, '(', ')'); + } + + @Test + public void extractBetweenThrowsIfNoRightSymbol() { + String input = "(a, b, c"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.extractBetween(input, '(', ')'); + } + + @Test + public void extractBetweenStripsPrefixAndSuffix() { + String inner = "a, b, c"; + String input = "hello [" + inner + "] world"; + + String result = ParserUtil.extractBetween(input, '[', ']'); + + assertThat(result, equalTo(inner)); + } + + @Test + public void extractBetweenSkipsNestedGroups() { + String inner = "a, b, c"; + String wrapped = "<" + inner + ">"; + String input = "<" + wrapped + ">"; + + String result = ParserUtil.extractBetween(input, '<', '>'); + + assertThat(result, equalTo(wrapped)); + } + + @Test + public void extractBetweenThingGroup1() { + String inner = "random(a, b), random(c, d)"; + String input = "all(" + inner + ")"; + + String result = ParserUtil.extractBetween(input, '(', ')'); + + assertThat(result, equalTo(inner)); + } + + @Test + public void splitThrowsOnUnmatchedLeftParen() { + String input = "(a, b), (c, d"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedRightParen() { + String input = "(a, b), c, d)"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedLeftBracket() { + String input = "[a, b], [c, d"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedRightBracket() { + String input = "[a, b], c, d]"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedLeftBrace() { + String input = "{a, b}, {c, d"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedRightBrace() { + String input = "{a, b}, c, d}"; + exception.expect(IllegalArgumentException.class); + + ParserUtil.split(input); + } + + @Test + public void splitThrowsOnUnmatchedLeftAngle() { + String input = ", result = ParserUtil.split(input); + + assertThat(result, empty()); + } + + @Test + public void splitReturnsInputIfNoCommas() { + String input = "abc"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Collections.singletonList(input))); + } + + @Test + public void splitWorksOnBareLists() { + String input = "abc, de, f"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", "de", "f"))); + } + + @Test + public void splitOmitsEmptyParts() { + String input = "abc, , f, "; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", "f"))); + } + + @Test + public void splitSkipsParentheses() { + String input = "abc, (de, f)"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", "(de, f)"))); + } + + @Test + public void splitSkipsSquareBrackets() { + String input = "abc, [de, f]"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", "[de, f]"))); + } + + @Test + public void splitSkipsCurlyBraces() { + String input = "abc, {de, f}"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", "{de, f}"))); + } + + @Test + public void splitSkipsAngleBrackets() { + String input = "abc, "; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList("abc", ""))); + } + + @Test + public void splitThingGroup1() { + String input = "all(random(a, b), random(c, d))"; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Collections.singletonList(input))); + } + + @Test + public void splitThingGroup2() { + String group1 = "all(random(a, b), random(c, d))"; + String group2 = "random(all(e, f), all(g, h))"; + String input = group1 + ", " + group2; + + List result = ParserUtil.split(input); + + assertThat(result, equalTo(Arrays.asList(group1, group2))); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/things/RandomThingPickerParserTest.java b/src/test/java/com/garbagemule/MobArena/things/RandomThingPickerParserTest.java new file mode 100644 index 0000000..0e73727 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/RandomThingPickerParserTest.java @@ -0,0 +1,99 @@ +package com.garbagemule.MobArena.things; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Random; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RandomThingPickerParserTest { + + private RandomThingPickerParser subject; + private ThingPickerParser parser; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setup() { + parser = mock(ThingPickerParser.class); + subject = new RandomThingPickerParser(parser, new Random()); + } + + @Test + public void returnsNullIfRandomIsMissing() { + String input = "(a, b, c)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void returnsNullIfParenthesesAreMissing() { + String input = "random[a, b, c]"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void returnsNullIfNotRandom() { + String input = "all(a, b, c)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void invokesUnderlyingParserForEachItem() { + String input = "random(a, b, c)"; + + subject.parse(input); + + verify(parser, times(1)).parse("a"); + verify(parser, times(1)).parse("b"); + verify(parser, times(1)).parse("c"); + } + + @Test + public void returnsRandomThingPickerMultipleThings() { + String input = "random(a, b)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, instanceOf(RandomThingPicker.class)); + } + + @Test + public void returnsOnlyPickerInsteadOfWrapping() { + String input = "random(a)"; + ThingPicker picker = mock(ThingPicker.class); + when(parser.parse("a")).thenReturn(picker); + + ThingPicker result = subject.parse(input); + + assertThat(result, is(picker)); + } + + @Test + public void throwsIfZeroThings() { + String input = "random()"; + exception.expect(IllegalArgumentException.class); + + subject.parse(input); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/things/ThingGroupPickerParserTest.java b/src/test/java/com/garbagemule/MobArena/things/ThingGroupPickerParserTest.java new file mode 100644 index 0000000..c0c78c7 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/ThingGroupPickerParserTest.java @@ -0,0 +1,97 @@ +package com.garbagemule.MobArena.things; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ThingGroupPickerParserTest { + + private ThingGroupPickerParser subject; + private ThingPickerParser parser; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setup() { + parser = mock(ThingPickerParser.class); + subject = new ThingGroupPickerParser(parser); + } + + @Test + public void returnsNullIfAllIsMissing() { + String input = "(a, b, c)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void returnsNullIfParenthesesAreMissing() { + String input = "all[a, b, c]"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void returnsNullIfNotAll() { + String input = "random(a, b, c)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void invokesUnderlyingParserForEachItem() { + String input = "all(a, b, c)"; + + subject.parse(input); + + verify(parser, times(1)).parse("a"); + verify(parser, times(1)).parse("b"); + verify(parser, times(1)).parse("c"); + } + + @Test + public void returnsRandomThingPickerMultipleThings() { + String input = "all(a, b)"; + + ThingPicker result = subject.parse(input); + + assertThat(result, instanceOf(ThingGroupPicker.class)); + } + + @Test + public void returnsSingleThingPickerIfOnlyOneThing() { + String input = "all(a)"; + ThingPicker picker = mock(ThingPicker.class); + when(parser.parse("a")).thenReturn(picker); + + ThingPicker result = subject.parse(input); + + assertThat(result, is(picker)); + } + + @Test + public void throwsIfZeroThings() { + String input = "all()"; + exception.expect(IllegalArgumentException.class); + + subject.parse(input); + } + +}