diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33a02c..9d8f2d0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ version: 2 updates: - - package-ecosystem: "devcontainers" - directory: "/" - schedule: - interval: weekly + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 688c711..d1b0e0a 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,7 +29,9 @@ jobs: distribution: 'temurin' cache: maven - name: Test with Maven - run: mvn -B test + run: mvn -B -ntp test + - name: Check coverage + run: mvn -B -ntp verify # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e01daa3..a4e99ea 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,21 @@ \ No newline at end of file diff --git a/pom.xml b/pom.xml index ebb128c..7c5ad6f 100644 --- a/pom.xml +++ b/pom.xml @@ -28,15 +28,59 @@ - - + + org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 + maven-surefire-plugin + 3.2.5 true - - - + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + default-prepare-agent + + prepare-agent + + + + default-report + + report + + + + default-check + + check + + + + + SOURCEFILE + + net/marcellperger/mathexpr/util/rs/Result.java + net/marcellperger/mathexpr/util/rs/Option.java + + + + INSTRUCTION + COVEREDRATIO + + 100% + + + + + + + + + + diff --git a/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java b/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java deleted file mode 100644 index 1b4c25a..0000000 --- a/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.marcellperger.mathexpr.parser; - -@SuppressWarnings("unused") // I want all the constructors! -public class ExprParseRtException extends RuntimeException { - public ExprParseRtException() { - super(); - } - - public ExprParseRtException(String message) { - super(message); - } - - public ExprParseRtException(String message, Throwable cause) { - super(message, cause); - } - - public ExprParseRtException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/net/marcellperger/mathexpr/parser/Parser.java b/src/main/java/net/marcellperger/mathexpr/parser/Parser.java index 2c13958..7c9ac2b 100644 --- a/src/main/java/net/marcellperger/mathexpr/parser/Parser.java +++ b/src/main/java/net/marcellperger/mathexpr/parser/Parser.java @@ -12,6 +12,7 @@ import java.util.Comparator; import java.util.List; import java.util.function.Function; +import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,38 +40,23 @@ public MathSymbol parseExpr() throws ExprParseException { // https://regex101.com/r/2EogTA/1 protected static final Pattern DOUBLE_RE = Pattern.compile("^([+-]?)(\\d*\\.\\d+|\\d+\\.?)(?:[eE]([+-]?\\d+))?"); - public @NotNull MathSymbol parseDoubleLiteral_exc() throws ExprParseException { - return switch (parseDoubleLiteral_null()) { - case null -> throw new ExprParseException("Couldn't parse double in expression"); - case MathSymbol sym -> sym; - }; - } - public @Nullable MathSymbol parseDoubleLiteral_null() { + public @NotNull MathSymbol parseDoubleLiteral() throws ExprParseException { discardWhitespace(); - Matcher m = DOUBLE_RE.matcher(strFromHere()); - if (!m.lookingAt()) return null; - String s = m.group(); - idx += s.length(); - double value; + String s = matchNextRegexString(DOUBLE_RE, "Invalid number (double)"); try { - value = Double.parseDouble(s); - } catch (NumberFormatException _exc) { - // Technically this should never happen - assuming I've got that regex right - assert false: "There is a problem with the regex, this should've been rejected earlier"; - return null; + return new BasicDoubleSymbol(Double.parseDouble(s)); + } catch (NumberFormatException exc) { + throw new AssertionError("There is a problem with the regex," + + " this should've been rejected earlier", exc); } - return new BasicDoubleSymbol(value); } - public @Nullable MathSymbol parseParensOrLiteral() throws ExprParseException { + public @NotNull MathSymbol parseParensOrLiteral() throws ExprParseException { discardWhitespace(); - if(peek() == '(') return parseParens(); - // TODO add a better error handling system - don't want to maintain 2 versions of each function - // returning null: +easier to do unions, +no need for verbose try/catch, -no info about errors - return parseDoubleLiteral_null(); + return peek() == '(' ? parseParens() : parseDoubleLiteral(); } - public @Nullable MathSymbol parseParens() throws ExprParseException { + public MathSymbol parseParens() throws ExprParseException { advanceExpectNext_ignoreWs('('); MathSymbol sym = parseExpr(); advanceExpectNext_ignoreWs(')'); @@ -152,8 +138,7 @@ boolean advanceIf(@NotNull Function predicate) { * @param predicate Keep advancing while this returns true * @return Amount of spaces advanced */ - @SuppressWarnings("UnusedReturnValue") - int advanceWhile(@NotNull Function predicate) { + protected int advanceWhile(@NotNull Function predicate) { int n = 0; while (notEof() && advanceIf(predicate)) ++n; return n; @@ -167,19 +152,45 @@ protected void discardWhitespace() { advanceWhile(Character::isWhitespace); } - protected void advanceExpectNext(char expected) { + protected void advanceExpectNext(char expected) throws ExprParseException { char actual = advance(); - if(actual != expected) throw new ExprParseRtException("Expected '%c', got '%c'".formatted(expected, actual)); + if(actual != expected) throw new ExprParseException("Expected '%c', got '%c'".formatted(expected, actual)); } - protected void advanceExpectNext_ignoreWs(char expected) { + protected void advanceExpectNext_ignoreWs(char expected) throws ExprParseException { discardWhitespace(); advanceExpectNext(expected); } + protected MatchResult matchNextRegexResult(@NotNull Pattern pat, ExprParseException exc) throws ExprParseException { + Matcher m = pat.matcher(strFromHere()); + if (!m.lookingAt()) throw exc; + String s = m.group(); + idx += s.length(); + return m.toMatchResult(); + } + protected MatchResult matchNextRegexResult(@NotNull Pattern pat, String exc) throws ExprParseException { + return matchNextRegexResult(pat, new ExprParseException(exc)); + } + protected MatchResult matchNextRegexResult(@NotNull Pattern pat) throws ExprParseException { + return matchNextRegexResult(pat, "Regex should've been matched"); + } + @SuppressWarnings("unused") + protected String matchNextRegexString(@NotNull Pattern pat, ExprParseException exc) throws ExprParseException { + return matchNextRegexResult(pat, exc).group(); + } + @SuppressWarnings("SameParameterValue") + protected String matchNextRegexString(@NotNull Pattern pat, String msg) throws ExprParseException { + return matchNextRegexResult(pat, msg).group(); + } + @SuppressWarnings("unused") + protected String matchNextRegexString(@NotNull Pattern pat) throws ExprParseException { + return matchNextRegexResult(pat).group(); + } + protected boolean matchesNext(@NotNull String expected) { return src.startsWith(expected, /*start*/idx); } - + private @NotNull List<@NotNull String> sortedByLength(@NotNull List<@NotNull String> arr) { return arr.stream().sorted(Comparator.comparingInt(String::length).reversed()).toList(); } diff --git a/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java b/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java new file mode 100644 index 0000000..be5ece3 --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java @@ -0,0 +1,4 @@ +package net.marcellperger.mathexpr.util; + +public @interface JacocoIgnoreNotGenerated { +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java b/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java new file mode 100644 index 0000000..a4f378f --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java @@ -0,0 +1,6 @@ +package net.marcellperger.mathexpr.util; + +@FunctionalInterface +public interface ThrowingSupplier { + T get() throws E; +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/Util.java b/src/main/java/net/marcellperger/mathexpr/util/Util.java index 14f28cb..0e8ffe2 100644 --- a/src/main/java/net/marcellperger/mathexpr/util/Util.java +++ b/src/main/java/net/marcellperger/mathexpr/util/Util.java @@ -7,8 +7,11 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; @SuppressWarnings("unused") public class Util { @@ -56,13 +59,11 @@ protected Util() {} return obj; } - @SuppressWarnings("UnusedReturnValue") @Contract("_ -> param1") public static > @NotNull C requireNonEmpty(@NotNull C collection) { if(collection.isEmpty()) throw new CollectionSizeException("Argument must not be empty"); return collection; } - @SuppressWarnings("UnusedReturnValue") @Contract("null -> fail; _ -> param1") public static > @NotNull C requireNonEmptyNonNull(C collection) { return requireNonEmpty(Objects.requireNonNull(collection)); @@ -72,7 +73,6 @@ protected Util() {} if(collection.isEmpty()) throw new CollectionSizeException(msg); return collection; } - @SuppressWarnings("UnusedReturnValue") @Contract("null, _ -> fail; _, _ -> param1") public static > @NotNull C requireNonEmptyNonNull(C collection, String msg) { return requireNonEmpty(Objects.requireNonNull(collection), msg); @@ -194,4 +194,91 @@ public static T getOnlyItem(@Flow(sourceIsContainer = true) @NotNull Sequence public static @NotNull V getNotNull(@NotNull Map map, K key) { return Objects.requireNonNull(map.get(key)); } + + @Contract(value = "_ -> new", pure = true) + public static @NotNull Iterator singleItemIterator(T value) { + return new SingleItemIterator<>(value); + } + + protected static class SingleItemIterator implements Iterator { + T value; + boolean isAtEnd; + + public SingleItemIterator(T value) { + this.value = value; + isAtEnd = false; + } + + @Override + public boolean hasNext() { + return !isAtEnd; + } + + @Override + public T next() { + if(isAtEnd) throw new NoSuchElementException("End of SingleItemIterator reached"); + return _moveToEndAndPop(); + } + + protected T _moveToEndAndPop() { + isAtEnd = true; + T val = value; + // Don't hold on to our copy - we don't need it anymore + // as iterators cannot go backwards and this lets JVM free it earlier. + value = null; + return val; + } + + @Override + public void forEachRemaining(Consumer action) { + if(isAtEnd) return; + action.accept(_moveToEndAndPop()); + } + } + + @Contract(pure = true) + public static @NotNull Function consumerToFunction(Consumer consumer) { + return v -> { + consumer.accept(v); + return VoidVal.val(); + }; + } + @Contract(pure = true) + public static @NotNull UnaryOperator consumerToIdentityFunc(Consumer consumer) { + return v -> { + consumer.accept(v); + return v; + }; + } + + public static @NotNull Supplier runnableToSupplier(Runnable runnable) { + return () -> { + runnable.run(); + return VoidVal.val(); + }; + } + + // These 2 trick Java into throwing checked exceptions in an unchecked way + @Contract("_ -> fail") + public static T throwAsUnchecked(Throwable exc) { + throwAs(exc); // is inferred from no `throws` clause + // Java doesn't know that this always throws so this lets us + // do `return/throw throwAsUnchecked()` to make Java's flow control analyser happy + throw new AssertionError("Unreachable"); + } + @SuppressWarnings("unchecked") + @Contract("_ -> fail") + public static void throwAs(Throwable exc) throws E { + // We do a little type erasure hack to trick Java: + // - E will be type-erased to Throwable so this will become + // throw (Throwable)exc; + // but exc is already Throwable due to the param type so + // this is like a runtime no-op. + // - But javac will see that we're throwing an E which is allowed! + // - The reason we need a separate method is so that there is a + // generic for javac to type-erase (otherwise JVM would check that + // it's actually that concrete type when it is thrown and this way + // it only checks for the base condition) + throw (E)exc; + } } diff --git a/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java b/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java new file mode 100644 index 0000000..ade1e71 --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java @@ -0,0 +1,28 @@ +package net.marcellperger.mathexpr.util; + +import org.jetbrains.annotations.NotNull; + + +/** The equivalent of Rust's {@code ()} or Python's {@code NoneType}/{@code None} + * or a {@code void} type in Java but can be passed as a type arg + * when e.g. you don't actually want to return anything but {@code Function} interface + * requires it to return instances of a type. Because 'no value' is a type that has + * EXACTLY ONE instance: nothing ({@code None} / {@code ()} / {@code undefined})*/ +public final class VoidVal { + static final @NotNull VoidVal INST = new VoidVal(); + + @SuppressWarnings("ConstantValue") // This is actually called to initialize it to non-null + private VoidVal() { + if(INST != null) throw new AssertionError( + "new VoidVal() should only be called once to create VoidVal.INST"); + } + + public static VoidVal val() { + return INST; + } + + @Override + public boolean equals(Object obj) { + return this == obj; // there can only be one. + } +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java new file mode 100644 index 0000000..aa6140d --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java @@ -0,0 +1,196 @@ +package net.marcellperger.mathexpr.util.rs; + +import net.marcellperger.mathexpr.util.Util; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public sealed interface Option extends Iterable { + // hashCode, equals are automatically implemented for `record`s + record Some(T value) implements Option { + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "Some(" + value + ')'; + } + } + + record None() implements Option { + @Contract(" -> new") + public @NotNull None cast() { + return new None<>(); + } + + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "None"; + } + } + + // Same as new Some/None but return Option + @Contract("_ -> new") + static @NotNull Option newSome(T value) { + return new Some<>(value); + } + + @Contract(" -> new") + static @NotNull Option newNone() { + return new None<>(); + } + + static Option<@NotNull T> ofNullable(@Nullable T value) { + return ofOptional(Optional.ofNullable(value)); + } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + static Option ofOptional(Optional value) { + return value.map(Option::newSome).orElse(newNone()); + } + + default boolean isSome() { + return this instanceof Some; + } + + default boolean isNone() { + return this instanceof None; + } + + default boolean isSomeAnd(Predicate predicate) { + return switch (this) { + case None() -> false; + case Some(T value) -> predicate.test(value); + }; + } + // I don't think we need as_slice because .stream().toList() / .collect() + // and there are already easy ways of Option -> Collection using .map etc. + + default Option map(Function fn) { + return switch (this) { + case Some(T value) -> new Some<>(fn.apply(value)); + case None n -> n.cast(); + }; + } + default U mapOr(U default_, Function fn) { + return mapOrElse(() -> default_, fn); + } + default U mapOrElse(Supplier defaultFn, Function fn) { + return switch (this) { + case Some(T value) -> fn.apply(value); + case None() -> defaultFn.get(); + }; + } + // there is nothing to map from or to (None -> None)! so this is Runnable + // so this is the same as inspectErr. It's here for consistency with Result + default Option mapErr(Runnable noneFn) { + if(isNone()) noneFn.run(); + return this; + } + + // not Rust functions, but provides more logical argument order + // (if/else vs else/if) than mapOrElse + default void ifThenElse_void(Consumer someFn, Runnable noneFn) { + mapOrElse(Util.runnableToSupplier(noneFn), Util.consumerToFunction(someFn)); + } + default U ifThenElse(Function someFn, Supplier noneFn) { + return mapOrElse(noneFn, someFn); + } + + default Option inspect(Consumer fn) { + return map(Util.consumerToIdentityFunc(fn)); + } + // not in Rust but could be useful (same as mapErr) + default Option inspectErr(Runnable noneFn) { + if(isNone()) noneFn.run(); + return this; + } + + // .iter()-esque methods: why does Java have SO MANY - one is enough. + default Stream stream() { + return mapOrElse(Stream::empty, Stream::of); + } + // Iterable automatically implements `spliterator()` for us + @NotNull + @Override + default Iterator iterator() { + return switch (this) { + case Some(T value) -> Util.singleItemIterator(value); + case None() -> Collections.emptyIterator(); + }; + } + @Override + default void forEach(Consumer action) { + map(Util.consumerToFunction(action)); + } + + default T unwrapOr(T default_) { + return unwrapOrElse(() -> default_); + } + default T unwrapOrElse(Supplier ifNone) { + return mapOrElse(ifNone, Function.identity()); + } + // no unwrap_or_default (see Result.java for big explanation) + default T expect(String msg) { + return unwrapOrElse(() -> { + throw new OptionPanicException(msg); + }); + } + default T unwrap() { + return expect("Option.unwrap() got None value"); + } + + default Result okOr(E err) { + return ifThenElse(Result::newOk, () -> Result.newErr(err)); + } + default Result okOrElse(Supplier errSupplier) { + return ifThenElse(Result::newOk, () -> Result.newErr(errSupplier.get())); + } + + default Option and(Option right) { + return andThen((_v) -> right); + } + default Option andThen(Function> fn) { + return switch (this) { + case Some(T value) -> fn.apply(value); + case None n -> n.cast(); + }; + } + + default Option or(Option right) { + return orElse(() -> right); + } + default Option orElse(Supplier> orFn) { + return switch (this) { + case Some s -> s; + case None() -> orFn.get(); + }; + } + + default Option filter(Predicate predicate) { + return switch (this) { + case Some(T value) -> predicate.test(value) ? this : newNone(); + case None n -> n; + }; + } + + default Option xor(Option right) { + return switch (this) { // I wish this implementation was more elegant + case Some left -> right.isNone() ? left : newNone(); + case None() -> right; + }; + } + + // no insert / get_or_insert(_*) / take(_if) / replace + // (as record is immutable in Java) + // no (un)zip(_with) because no tuples in Java (will do if needed tho) + // rest are `impl`s on compound types which Java can't do + // (see Result.java for detailed explanation), can do static method if we need it so much +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java new file mode 100644 index 0000000..b0fb89a --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java @@ -0,0 +1,35 @@ +package net.marcellperger.mathexpr.util.rs; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public class OptionPanicException extends PanicException { + public OptionPanicException() { + } + + public OptionPanicException(String message) { + super(message); + } + + public OptionPanicException(String message, Throwable cause) { + super(message, cause); + } + + public OptionPanicException(Throwable cause) { + super(cause); + } + + @SuppressWarnings("unused") // may be used later + @Contract(value = " -> new", pure = true) + public static @NotNull Builder builder() { + return new Builder(); + } + + public static class Builder extends PanicException.Builder { + @Override + public OptionPanicException build() { + return build(OptionPanicException::new, OptionPanicException::new, + OptionPanicException::new, OptionPanicException::new); + } + } +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java new file mode 100644 index 0000000..e42439a --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java @@ -0,0 +1,66 @@ +package net.marcellperger.mathexpr.util.rs; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class PanicException extends RuntimeException { + public PanicException() { + } + + public PanicException(String message) { + super(message); + } + + public PanicException(String message, Throwable cause) { + super(message, cause); + } + + public PanicException(Throwable cause) { + super(cause); + } + + @SuppressWarnings("unused") // may be used later + @Contract(value = " -> new", pure = true) + public static @NotNull Builder builder() { + return new Builder(); + } + + public static class Builder { + protected @Nullable String msg; + protected @Nullable Throwable cause; + + protected Builder() { + msg = null; + cause = null; + } + + @Contract("_ -> this") + public Builder msg(@Nullable String msg) { + this.msg = msg; + return this; + } + + @Contract("_ -> this") + public Builder cause(@Nullable Throwable cause) { + this.cause = cause; + return this; + } + + public PanicException build() { + return build(PanicException::new, PanicException::new, PanicException::new, PanicException::new); + } + + public T build(Supplier ctor0, Function ctor1str, + Function ctor1exc, + BiFunction ctor2) { + return msg != null + ? cause != null ? ctor2.apply(msg, cause) : ctor1str.apply(msg) + : cause != null ? ctor1exc.apply(cause) : ctor0.get(); + } + } +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java b/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java new file mode 100644 index 0000000..c0d73cb --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java @@ -0,0 +1,256 @@ +package net.marcellperger.mathexpr.util.rs; + +import net.marcellperger.mathexpr.util.JacocoIgnoreNotGenerated; +import net.marcellperger.mathexpr.util.ThrowingSupplier; +import net.marcellperger.mathexpr.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + + +/** + * An implementation of Rust's immutable {@code Result} type that stores either: + *
  • A value of type {@link T} representing success
  • + *
  • An error value of type {@link E} representing an error
+ * No restrictions are placed on any of these types for convenience and flexibility. + * @see Rust's std::result::Result + * @param Type of the {@link Ok} value + * @param Type of the {@link Err} error value + */ +@SuppressWarnings({"unused", "InnerClassOfInterface"}) // should be an abstract class but then the `record Ok/Err` cannot extend it +public sealed interface Result extends Iterable { + // hashCode / equals is automatically implemented for these record types, but we customize toString + record Ok(T value) implements Result { + public Ok cast() { return new Ok<>(value); } + + @Override + public String toString() { + return "Ok(" + value + ')'; + } + } + + record Err(E exc) implements Result { + public Err cast() { return new Err<>(exc); } + + @Override + public String toString() { + return "Err(" + exc + ')'; + } + } + + // same as new Ok/Err but return Option + static Result newOk(T value) { + return new Ok<>(value); + } + static Result newErr(E err) { + return new Err<>(err); + } + + /** Same as fromExc but also fills in the stack trace using + * {@link Throwable#fillInStackTrace()} for more useful error messages */ + static @NotNull Result fromExc(@NotNull E exc) { + exc.fillInStackTrace(); + return newErr(exc); + } + + static Result fromTry(ThrowingSupplier inner, Class catchThis) { + try { + return newOk(inner.get()); + } catch (Throwable exc) { + return _makeErrTypeOrFailUnchecked(exc, catchThis); + } + } + + @NotNull + @JacocoIgnoreNotGenerated + private static Result _makeErrTypeOrFailUnchecked(Throwable exc, Class catchThis) { + try { + return newErr(catchThis.cast(exc)); + } catch (ClassCastException c) { + // We've handled E, so only unchecked exceptions should reach here + // so it's safe to throw them (but Java doesn't know that so we trick it) + return Util.throwAsUnchecked(exc); // Jacoco doesn't realize that the `return` is unreachable + } + } + + default @Nullable Ok ok() { + return switch (this) { + case Ok ok -> ok; + case Err _e -> null; + }; + } + default @Nullable Err err() { + return switch (this) { + case Ok _ok -> null; + case Err err -> err; + }; + } + + default Optional> okOpt() { return Optional.ofNullable(ok()); } + default Optional> errOpt() { return Optional.ofNullable(err()); } + + default Option okOption() { + return mapOr(Option.newNone(), Option::newSome); + } + default Option errOption() { + return ifThenElse(_ok -> Option.newNone(), Option::newSome); + } + + default boolean isOk() { return ok() != null; } + default boolean isErr() { return err() != null; } + + default boolean isOkAnd(Predicate f) { + return switch (this) { + case Ok(T value) -> f.test(value); + case Err e -> false; + }; + } + default boolean isErrAnd(Predicate f) { + return switch (this) { + case Ok o -> false; + case Err(E err) -> f.test(err); + }; + } + + default Result map(Function op) { + return switch (this) { + case Ok(T value) -> new Ok<>(op.apply(value)); + case Err e -> e.cast(); + }; + } + default U mapOr(U default_, Function f) { + return mapOrElse((_e) -> default_, f); + } + default U mapOrElse(Function defaultFn, Function f) { + return switch (this) { + case Ok(T value) -> f.apply(value); + case Err(E err) -> defaultFn.apply(err); + }; + } + default Result mapErr(Function op) { + return switch (this) { + case Ok o -> o.cast(); + case Err(E err) -> new Err<>(op.apply(err)); + }; + } + + // not a Rust function. Similar to mapOrElse but reversed arguments + default void ifThenElse_void(Consumer okFn, Consumer errFn) { + // This _void variant exists (instead of an overload) to avoid Java complaining + // about ambiguous arguments when passing method references + mapOrElse(Util.consumerToFunction(errFn), Util.consumerToFunction(okFn)); + } + default U ifThenElse(Function okFn, Function errFn) { + return mapOrElse(errFn, okFn); + } + + default Result inspect(Consumer f) { + return map(Util.consumerToIdentityFunc(f)); + } + default Result inspectErr(Consumer f) { + return mapErr(Util.consumerToIdentityFunc(f)); + } + + default Result runIfOk(Consumer f) { + return map(Util.consumerToIdentityFunc(f)); + } + default Result runIfErr(Consumer f) { + return mapErr(Util.consumerToIdentityFunc(f)); + } + + // .iter()-esque methods: why does Java have SO MANY - one is enough. + default Stream stream() { + return okOpt().map(Ok::value).stream(); + } + // Iterable automatically implements `spliterator()` for us + @NotNull + @Override + default Iterator iterator() { + return switch (this) { + case Ok(T value) -> Util.singleItemIterator(value); + case Err(E _err) -> Collections.emptyIterator(); + }; + } + @Override + default void forEach(Consumer action) { + map(Util.consumerToFunction(action)); + } + + default T unwrap() { + return expect("unwrap() got Err value"); + } + default T expect(String msg) { + return unwrapOrElse((err) -> { + throw ResultPanicWithValueException.fromMaybeExcValue(err, msg); + }); + } + // We CANNOT do unwrap_or_default because Java: + // - Static methods cannot be overloaded properly and cannot even be part of an interface + // (static methods are very much NOT first-class things in Java - unlike Rust) + // so the language that is the 'standard' Object-Oriented language cannot + // even do polymorphism for static methods! (without resorting to + // runtime non-compile-time-checked reflection) + // - My (very many) attempts to solve it using C++-style CRTP have all failed + // because Java handles generics using type erasure, making 1 general + // type-erased version of a method so any attempts to call methods on the passed subtype + // just results on the method being called on the type declared extended in the ``. + // Another problematic consequence of (I think) type erasure is that a class hierarchy + // cannot implement the same interface with 2 different types so something + // cannot be both Iterable and Iterable. + // IMO, handling generics using monomorphisation (like C++ and Rust) would solve these issues and would + // allow much greater flexibility and could open paths to a SFINAE-type + // system which would solve the first BP. + // - I really don't want to use reflection, because in strongly typed languages, + // my philosophy is "Check as much as possible at compile-time to reduce runtime failure". + default E expectErr(String msg) { + return ifThenElse(ok -> { + throw ResultPanicWithValueException.fromPlainValue(ok, msg); + }, Function.identity()); + } + default E unwrapErr() { + return expectErr("unwrapErr() got Ok value"); + } + // We can't implement into_ok/into_err as Java doesn't have that + // kind of infallible type and I don't think Java can be that rigorous about + // infallibility (Java's type system in proven to be unsound so...) + // or can't even reason about it in the first place. + + default Result and(Result other) { + return andThen((_ok) -> other); + } + default Result andThen(Function> then) { + return switch (this) { + case Ok(T value) -> then.apply(value); // everything normal with `this`, do next + case Err err -> err.cast(); + }; + } + + default Result or(Result other) { + return orElse((_err) -> other); + } + default Result orElse(Function> elseFn) { + return switch (this) { + case Ok ok -> ok.cast(); + case Err(E err) -> elseFn.apply(err); // `this` failed so try `elseFn` - "You're my only hope" + }; + } + + default T unwrapOr(T defaultV) { + return mapOr(defaultV, Function.identity()); + } + default T unwrapOrElse(Function ifErr) { + return mapOrElse(ifErr, Function.identity()); + } + // no unwrap_unchecked/unwrap_err_unchecked for obvious reasons (usually Java != UB/unsafe) + + // Cannot have implementations for Result, E> and similar because Java + // has nothing that allows us to enable methods if certain conditions are met + // and no multiple implementation blocks with possibly-different conditions on the type parameters. +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java new file mode 100644 index 0000000..fcdb4e4 --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java @@ -0,0 +1,35 @@ +package net.marcellperger.mathexpr.util.rs; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public class ResultPanicException extends PanicException { + public ResultPanicException() { + } + + public ResultPanicException(String message) { + super(message); + } + + public ResultPanicException(String message, Throwable cause) { + super(message, cause); + } + + public ResultPanicException(Throwable cause) { + super(cause); + } + + @SuppressWarnings("unused") // may be used later + @Contract(value = " -> new", pure = true) + public static @NotNull Builder builder() { + return new Builder(); + } + + public static class Builder extends PanicException.Builder { + @Override + public ResultPanicException build() { + return build(ResultPanicException::new, ResultPanicException::new, + ResultPanicException::new, ResultPanicException::new); + } + } +} diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java new file mode 100644 index 0000000..d1c62f7 --- /dev/null +++ b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java @@ -0,0 +1,97 @@ +package net.marcellperger.mathexpr.util.rs; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// Can't make `Throwable`s generic because java... +public class ResultPanicWithValueException extends ResultPanicException { + protected @Nullable Object value; + + /// To set the cause, use `.builder()` + public ResultPanicWithValueException() { + } + + public ResultPanicWithValueException(String message) { + super(message); + } + + public ResultPanicWithValueException(String message, Throwable cause) { + super(message, cause); + } + + public ResultPanicWithValueException(String message, Throwable cause, @Nullable Object value) { + super(message, cause); + this.value = value; + } + + public ResultPanicWithValueException(Throwable cause) { + super(cause); + } + + public static @NotNull ResultPanicWithValueException fromMaybeExcValue(@Nullable Object value, String msg) { + return switch (value) { + case null -> new ResultPanicWithValueException(msg); + case Throwable excValue -> new ResultPanicWithValueException(msg, excValue, excValue); + default -> new ResultPanicWithValueException(msg, null, value); + }; + } + + public static @NotNull ResultPanicWithValueException fromPlainValue(@Nullable Object value, String msg) { + return new Builder().msg(msg).value(value).build(); + } + + @Override + public String getMessage() { + return switch (super.getMessage()) { + case null -> value != null ? "Panic value: " + value : ""; + case "" -> value != null ? "Panic value: " + value : ""; + case String msg -> msg + (value == null ? "" : ": " + value); + }; + } + + public @Nullable Object getValue() { + return value; + } + public void setValue(@Nullable Object value) { + this.value = value; + } + + public static class Builder extends ResultPanicException.Builder { + protected @Nullable Object m_value; + + public Builder() { + super(); + m_value = null; + } + + @Override + public Builder msg(@Nullable String msg) { + // I wish there was a better way (e.g. a Self) type - could actually do + // CRTP here but that could get messy for users of the base type + super.msg(msg); + return this; + } + + @Override + public Builder cause(@Nullable Throwable cause) { + super.cause(cause); + return this; + } + + @Contract("_ -> this") + public Builder value(Object value) { + this.m_value = value; + return this; + } + + @Override + public ResultPanicWithValueException build() { + ResultPanicWithValueException ret = build( + ResultPanicWithValueException::new, ResultPanicWithValueException::new, + ResultPanicWithValueException::new, ResultPanicWithValueException::new); + ret.setValue(m_value); + return ret; + } + } +} diff --git a/src/test/java/net/marcellperger/mathexpr/MiniMock.java b/src/test/java/net/marcellperger/mathexpr/MiniMock.java new file mode 100644 index 0000000..d53f34d --- /dev/null +++ b/src/test/java/net/marcellperger/mathexpr/MiniMock.java @@ -0,0 +1,288 @@ +package net.marcellperger.mathexpr; + +import net.marcellperger.mathexpr.util.VoidVal; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.junit.jupiter.api.Assertions; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class MiniMock { + public static class BaseMockedCallable { + List calls; + + protected void handleCall(A args) { + calls.add(args); + } + + public void assertCalledOnce() { + Assertions.assertEquals(1, calls.size(), "Expected function to be called exactly once."); + } + public void assertCalledOnceWith(A args) { + assertCalledOnce(); + Assertions.assertEquals(args, calls.getFirst(), "Expected function to called with these args"); + } + + public void assertNotCalled() { + Assertions.assertEquals(0, calls.size(), "Expected function not to be called."); + } + + public void reset() { + calls.clear(); + } + } + + public static class MockedSupplier extends BaseMockedCallable implements Supplier { + Supplier returnSupplier; + + public MockedSupplier(R returnValue_) { + this(() -> returnValue_); + } + public MockedSupplier(Supplier returnSupplier_) { + calls = new ZST_List(); + returnSupplier = returnSupplier_; + } + + @Override + public R get() { + handleCall(VoidVal.val()); + return returnSupplier.get(); + } + } + + public static class MockedFunction extends BaseMockedCallable implements Function { + Function returnFunc; + + public MockedFunction(R returnValue_) { + this((_v) -> returnValue_); + } + public MockedFunction(Function returnSupplier_) { + calls = new ArrayList<>(); + returnFunc = returnSupplier_; + } + + @Override + public R apply(T arg) { + handleCall(arg); + return returnFunc.apply(arg); + } + } + + public static class MockedPredicate extends BaseMockedCallable implements Predicate { + Predicate returnFunc; + + public MockedPredicate(boolean returnValue_) { + this((_v) -> returnValue_); + } + public MockedPredicate(Predicate returnSupplier_) { + calls = new ArrayList<>(); + returnFunc = returnSupplier_; + } + + @Override + public boolean test(T arg) { + handleCall(arg); + return returnFunc.test(arg); + } + } + + public static class MockedConsumer extends BaseMockedCallable implements Consumer { + public MockedConsumer() { + calls = new ArrayList<>(); + } + + @Override + public void accept(T arg) { + handleCall(arg); + } + } + + public static class MockedRunnable extends BaseMockedCallable implements Runnable { + public MockedRunnable() { + calls = new ZST_List(); + } + + @Override + public void run() { handleCall(VoidVal.val()); } + } + + // oof, this List interface is monstrously big, with very few defaults! + static class ZST_List implements List { + private int m_size = 0; // I wish this could be long but size() requires int + + public ZST_List() {} + public ZST_List(int size) { m_size = size; } + @SuppressWarnings("unused") + @Contract(pure = true) + public ZST_List(@NotNull Collection c) { + m_size = c.size(); + } + + + private void wantInBounds(int index) { + if(index < 0 || index >= m_size) throw new IndexOutOfBoundsException(index); + } + private void wantInBounds_insert(int index) { + if(index < 0 || index > m_size) throw new IndexOutOfBoundsException(index); + } + + @Contract(" -> new") + private @NotNull @Unmodifiable List immutableImpl() { + return Collections.nCopies(m_size, VoidVal.val()); + } + + @Override + public int size() { + return m_size; + } + + @Override + public boolean isEmpty() { + return m_size == 0; + } + + @Override + public boolean contains(Object o) { + return o == VoidVal.val(); // we contain all VoidVal and nothing else + } + + @NotNull + @Override + public Iterator iterator() { + return immutableImpl().iterator(); + } + + @NotNull + @Override + public Object @NotNull [] toArray() { + return immutableImpl().toArray(); + } + + @NotNull + @Override + public T @NotNull [] toArray(@NotNull T @NotNull [] a) { + return immutableImpl().toArray(a); + } + + @Override + public boolean add(VoidVal voidVal) { + ++m_size; + return true; + } + + @Override + public boolean remove(Object o) { + if(m_size <= 0) return false; + --m_size; + return true; + } + + @Override + public boolean containsAll(@NotNull Collection c) { + return m_size != 0 && c.stream().allMatch(o -> o == VoidVal.val()); + } + + @Override + public boolean addAll(@NotNull Collection c) { + if(c.stream().anyMatch(o -> o != VoidVal.val())) throw new ClassCastException("ZST_List can only contain VoidVal"); + m_size += c.size(); + return true; + } + + @Override + public boolean addAll(int index, @NotNull Collection c) { + wantInBounds_insert(index); + return addAll(c); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + boolean removeValues = c.contains(VoidVal.val()); + if(removeValues && m_size != 0) { + m_size = 0; + return true; + } + return false; + } + + @Override + public boolean retainAll(@NotNull Collection c) { + boolean retainValues = c.contains(VoidVal.val()); + if (retainValues || m_size == 0) { + return false; + } + m_size = 0; + return true; + } + + @Override + public void clear() { + m_size = 0; + } + + @Override + public VoidVal get(int index) { + wantInBounds(index); + return VoidVal.val(); + } + + @Override + public VoidVal set(int index, VoidVal element) { + wantInBounds(index); + if(element != VoidVal.val()) throw new ClassCastException("ZST_List can only contain VoidVal"); + return VoidVal.val(); + } + + @Override + public void add(int index, VoidVal element) { + wantInBounds_insert(index); + ++m_size; + } + + @Override + public VoidVal remove(int index) { + wantInBounds(index); + --m_size; + return VoidVal.val(); + } + + @Override + public int indexOf(Object o) { + if(m_size == 0) return -1; + return o == VoidVal.val() ? 0 : -1; + } + + @Override + public int lastIndexOf(Object o) { + if(m_size == 0) return -1; + return o == VoidVal.val() ? m_size - 1 : -1; + } + + // TODO Technically I should make a custom class for this and .iterator() + // as modification IS allowed but I'll do that later. + @NotNull + @Override + public ListIterator listIterator() { + return immutableImpl().listIterator(); + } + @NotNull + @Override + public ListIterator listIterator(int index) { + return immutableImpl().listIterator(index); + } + + @NotNull + @Override + public List subList(int fromIndex, int toIndex) { + // We just return a copy, because, for us, there ARE no + // 'non-structural' changes as only the count matters and there is + // only one possible value for each place so the only way to modify it is + return new ZST_List(toIndex - fromIndex); + } + } +} diff --git a/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java b/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java index b32ae3c..b708b27 100644 --- a/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java +++ b/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java @@ -1,5 +1,4 @@ package net.marcellperger.mathexpr; -@SuppressWarnings("unused") // IntelliJ is dumb - it IS used in tests public record ObjStringPair(MathSymbol obj, String str) { } diff --git a/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java new file mode 100644 index 0000000..10fe2af --- /dev/null +++ b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java @@ -0,0 +1,359 @@ +package net.marcellperger.mathexpr.util.rs; + +import net.marcellperger.mathexpr.MiniMock.*; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class OptionTest { + Option getSome() { + return Option.newSome(314); + } + Option getNone() { + return Option.newNone(); + } + + @Test + void isSome() { + assertTrue(getSome().isSome()); + assertFalse(getNone().isSome()); + } + + @Test + void isNone() { + assertFalse(getSome().isNone()); + assertTrue(getNone().isNone()); + } + + @Test + void isSomeAnd() { + MockedPredicate mfFalse = new MockedPredicate<>(false); + MockedPredicate mfTrue = new MockedPredicate<>(true); + { + assertTrue(getSome().isSomeAnd(mfTrue)); + mfTrue.assertCalledOnceWith(314); + assertFalse(getSome().isSomeAnd(mfFalse)); + mfFalse.assertCalledOnceWith(314); + } + mfTrue.reset(); + mfFalse.reset(); + { + assertFalse(getNone().isSomeAnd(mfTrue)); + mfTrue.assertNotCalled(); + assertFalse(getNone().isSomeAnd(mfFalse)); + mfFalse.assertNotCalled(); + } + } + + @Test + void map() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + assertEquals(Option.newSome(7), Option.newSome(6).map(mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd1.reset(); + assertEquals(Option.newNone(), Option.newNone().map(mfAdd1)); + mfAdd1.assertNotCalled(); + } + + @Test + void mapOr() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + assertEquals(7, Option.newSome(6).mapOr(-1, mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd1.reset(); + assertEquals(-1, Option.newNone().mapOr(-1, mfAdd1)); + mfAdd1.assertNotCalled(); + } + + @Test + void mapOrElse() { + MockedSupplier mSupplier = new MockedSupplier<>(-6); + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + { + assertEquals(7, Option.newSome(6).mapOrElse(mSupplier, mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mSupplier.assertNotCalled(); + } + mfAdd1.reset(); + mSupplier.reset(); + { + assertEquals(-6, Option.newNone().mapOrElse(mSupplier, mfAdd1)); + mfAdd1.assertNotCalled(); + mSupplier.assertCalledOnce(); + } + } + + @Test + void mapErr() { + MockedRunnable mRunnable = new MockedRunnable(); + assertEquals(getNone(), getNone().mapErr(mRunnable)); + mRunnable.assertCalledOnce(); + mRunnable.reset(); + assertEquals(getSome(), getSome().mapErr(mRunnable)); + mRunnable.assertNotCalled(); + } + + @Test + void ifThenElse() { + MockedSupplier mSupplier = new MockedSupplier<>(-6); + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + { + assertEquals(7, Option.newSome(6).ifThenElse(mfAdd1, mSupplier)); + mfAdd1.assertCalledOnceWith(6); + mSupplier.assertNotCalled(); + } + mfAdd1.reset(); + mSupplier.reset(); + { + assertEquals(-6, Option.newNone().ifThenElse(mfAdd1, mSupplier)); + mfAdd1.assertNotCalled(); + mSupplier.assertCalledOnce(); + } + } + + @Test + void ifThenElse_void() { + MockedConsumer mCons = new MockedConsumer<>(); + MockedRunnable mRunnable = new MockedRunnable(); + { + getSome().ifThenElse_void(mCons, mRunnable); + mCons.assertCalledOnceWith(314); + mRunnable.assertNotCalled(); + } + mCons.reset(); + mRunnable.reset(); + { + getNone().ifThenElse_void(mCons, mRunnable); + mCons.assertNotCalled(); + mRunnable.assertCalledOnce(); + } + } + + @Test + void inspect() { + MockedConsumer intCons = new MockedConsumer<>(); + assertEquals(getSome(), getSome().inspect(intCons)); + intCons.assertCalledOnceWith(314); + intCons.reset(); + assertEquals(getNone(), getNone().inspect(intCons)); + intCons.assertNotCalled(); + } + + @Test + void inspectErr() { + MockedRunnable mRunnable = new MockedRunnable(); + assertEquals(getNone(), getNone().inspectErr(mRunnable)); + mRunnable.assertCalledOnce(); + mRunnable.reset(); + assertEquals(getSome(), getSome().inspectErr(mRunnable)); + mRunnable.assertNotCalled(); + } + + @Test + void stream() { + assertEquals(List.of(314), getSome().stream().toList()); + assertEquals(List.of(), getNone().stream().toList()); + } + + @Test + void iterator() { + { + Iterator oks = getSome().iterator(); + assertTrue(oks.hasNext()); + assertEquals(314, oks.next()); + assertFalse(oks.hasNext()); + } + { + List ls = new ArrayList<>(); + getSome().iterator().forEachRemaining(ls::add); + assertEquals(List.of(314), ls); + } + { + Iterator oks = getNone().iterator(); + assertFalse(oks.hasNext()); + } + { + List ls = new ArrayList<>(); + getNone().iterator().forEachRemaining(ls::add); + assertEquals(List.of(), ls); + } + } + + @Test + void forEach() { + MockedConsumer intCons = new MockedConsumer<>(); + { + getSome().forEach(intCons); + intCons.assertCalledOnceWith(314); + } + intCons.reset(); + { + getNone().forEach(intCons); + intCons.assertNotCalled(); + } + } + + @Test + void unwrapOr() { + assertEquals(314, getSome().unwrapOr(-1)); + assertEquals(-1, getNone().unwrapOr(-1)); + } + + @Test + void unwrapOrElse() { + MockedSupplier ms = new MockedSupplier<>(271); + { + assertEquals(314, getSome().unwrapOrElse(ms)); + ms.assertNotCalled(); + } + ms.reset(); + { + assertEquals(271, getNone().unwrapOrElse(ms)); + ms.assertCalledOnce(); + } + } + + @Test + void expect() { + assertEquals(314, assertDoesNotThrow(() -> getSome().expect("EXPECT_MSG"))); + Option n = getNone(); + OptionPanicException exc = assertThrows(OptionPanicException.class, () -> n.expect("EXPECT_MSG")); + assertEquals("EXPECT_MSG", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause for Option.expect()"); + } + + @Test + void unwrap() { + assertEquals(314, assertDoesNotThrow(() -> getSome().unwrap())); + OptionPanicException exc = assertThrows(OptionPanicException.class, getNone()::unwrap); + assertEquals("Option.unwrap() got None value", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause for Option.unwrap()"); + } + + @Test + void okOr() { + assertEquals(Result.newOk(314), getSome().okOr("ERR_ARG")); + assertEquals(Result.newErr("ERR_ARG"), getNone().okOr("ERR_ARG")); + } + + @Test + void okOrElse() { + MockedSupplier ms = new MockedSupplier<>("ERR_FN_RETURN"); + { + assertEquals(Result.newOk(314), getSome().okOrElse(ms)); + ms.assertNotCalled(); + } + ms.reset(); + { + assertEquals(Result.newErr("ERR_FN_RETURN"), getNone().okOrElse(ms)); + ms.assertCalledOnce(); + } + } + + @Test + void and() { + assertEquals(Option.newSome(91L), getSome().and(Option.newSome(91L))); + assertEquals(Option.newNone(), getSome().and(Option.newNone())); + assertEquals(Option.newNone(), getNone().and(Option.newSome(91L))); + assertEquals(Option.newNone(), getNone().and(getNone())); + } + + @Test + void andThen() { + MockedFunction> mfOk = new MockedFunction<>(Option.newSome(91L)); + MockedFunction> mfErr = new MockedFunction<>(Option.newNone()); + { + assertEquals(Option.newSome(91L), getSome().andThen(mfOk)); + mfOk.assertCalledOnceWith(314); + assertEquals(Option.newNone(), getSome().andThen(mfErr)); + mfErr.assertCalledOnceWith(314); + } + mfOk.reset(); + mfErr.reset(); + { + assertEquals(Option.newNone(), getNone().andThen(mfOk)); + mfOk.assertNotCalled(); + assertEquals(Option.newNone(), getNone().andThen(mfErr)); + mfErr.assertNotCalled(); + } + } + + @Test + void filter() { + MockedPredicate mTrue = new MockedPredicate<>(true); + MockedPredicate mFalse = new MockedPredicate<>(false); + { + assertEquals(getSome(), getSome().filter(mTrue)); + mTrue.assertCalledOnceWith(314); + assertEquals(getNone(), getSome().filter(mFalse)); + mFalse.assertCalledOnceWith(314); + } + mTrue.reset(); + mFalse.reset(); + { + assertEquals(getNone(), getNone().filter(mTrue)); + mTrue.assertNotCalled(); + assertEquals(getNone(), getNone().filter(mFalse)); + mFalse.assertNotCalled(); + } + } + + @Test + void or() { + assertEquals(getSome(), getSome().or(Option.newSome(271))); + assertEquals(getSome(), getSome().or(getNone())); + assertEquals(getSome(), getNone().or(getSome())); + assertEquals(getNone(), getNone().or(getNone())); + } + + @Test + void orElse() { + MockedSupplier> mSome = new MockedSupplier<>(Option.newSome(271)); + MockedSupplier> mNone = new MockedSupplier<>(Option.newNone()); + { + assertEquals(getSome(), getSome().orElse(mSome)); + mSome.assertNotCalled(); + assertEquals(getSome(), getSome().orElse(mNone)); + mNone.assertNotCalled(); + } + mSome.reset(); + mNone.reset(); + { + assertEquals(Option.newSome(271), getNone().orElse(mSome)); + mSome.assertCalledOnce(); + assertEquals(Option.newNone(), getNone().orElse(mNone)); + mNone.assertCalledOnce(); + } + } + + @Test + void xor() { + assertEquals(getNone(), getSome().xor(Option.newSome(271))); + assertEquals(getSome(), getSome().xor(getNone())); + assertEquals(getSome(), getNone().xor(getSome())); + assertEquals(getNone(), getNone().xor(getNone())); + } + + @Test + void test_toString() { + assertEquals("Some(314)", getSome().toString()); + assertEquals("None", getNone().toString()); + } + + @Test + void ofOptional() { + assertEquals(Option.newSome(314), Option.ofOptional(Optional.of(314))); + assertEquals(Option.newNone(), Option.ofOptional(Optional.empty())); + } + + @Test + void ofNullable() { + assertEquals(Option.newSome(314), Option.ofNullable(314)); + assertEquals(Option.newNone(), Option.ofNullable(null)); + } +} \ No newline at end of file diff --git a/src/test/java/net/marcellperger/mathexpr/util/rs/ResultTest.java b/src/test/java/net/marcellperger/mathexpr/util/rs/ResultTest.java new file mode 100644 index 0000000..023c88d --- /dev/null +++ b/src/test/java/net/marcellperger/mathexpr/util/rs/ResultTest.java @@ -0,0 +1,537 @@ +package net.marcellperger.mathexpr.util.rs; + +import net.marcellperger.mathexpr.util.rs.Result.Err; +import net.marcellperger.mathexpr.util.rs.Result.Ok; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static net.marcellperger.mathexpr.MiniMock.*; +import static org.junit.jupiter.api.Assertions.*; + +// Some of these tests are somewhat based on Rust's tests for `core::result::Result` +// with some additions for all the methods that aren't tested in Rust's tests +// so that we test all the non-trivial methods +class ResultTest { + Result getOk() { + return Result.newOk(314); + } + Result getErr() { + return Result.newErr("TESTING_ERROR"); + } + + @Test + void isOk() { + assertTrue(getOk().isOk()); + assertFalse(getErr().isOk()); + } + + @Test + void isErr() { + assertFalse(getOk().isErr()); + assertTrue(getErr().isErr()); + } + + @Test + void isOkAnd() { + MockedPredicate mfFalse = new MockedPredicate<>(false); + MockedPredicate mfTrue = new MockedPredicate<>(true); + { + assertTrue(getOk().isOkAnd(mfTrue)); + mfTrue.assertCalledOnceWith(314); + assertFalse(getOk().isOkAnd(mfFalse)); + mfFalse.assertCalledOnceWith(314); + } + mfTrue.reset(); + mfFalse.reset(); + { + assertFalse(getErr().isOkAnd(mfTrue)); + mfTrue.assertNotCalled(); + assertFalse(getErr().isOkAnd(mfFalse)); + mfFalse.assertNotCalled(); + } + } + + @Test + void isErrAnd() { + MockedPredicate mfFalse = new MockedPredicate<>(false); + MockedPredicate mfTrue = new MockedPredicate<>(true); + { + assertTrue(getErr().isErrAnd(mfTrue)); + mfTrue.assertCalledOnceWith("TESTING_ERROR"); + assertFalse(getErr().isErrAnd(mfFalse)); + mfFalse.assertCalledOnceWith("TESTING_ERROR"); + } + mfTrue.reset(); + mfFalse.reset(); + { + assertFalse(getOk().isErrAnd(mfTrue)); + mfTrue.assertNotCalled(); + assertFalse(getOk().isErrAnd(mfFalse)); + mfFalse.assertNotCalled(); + } + } + + @Test + void map() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + assertEquals(Result.newOk(7), Result.newOk(6).map(mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd1.reset(); + assertEquals(Result.newErr(6), Result.newErr(6).map(mfAdd1)); + mfAdd1.assertNotCalled(); + } + + @Test + void mapOr() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + assertEquals(7, Result.newOk(6).mapOr(-1, mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd1.reset(); + assertEquals(-1, Result.newErr(6).mapOr(-1, mfAdd1)); + mfAdd1.assertNotCalled(); + } + + @Test + void mapOrElse() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + MockedFunction mfAdd25 = new MockedFunction<>(i -> i + 25); + { + assertEquals(7, Result.newOk(6).mapOrElse(mfAdd25, mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd25.assertNotCalled(); + } + mfAdd1.reset(); + mfAdd25.reset(); + { + assertEquals(31, Result.newErr(6).mapOrElse(mfAdd25, mfAdd1)); + mfAdd1.assertNotCalled(); + mfAdd25.assertCalledOnceWith(6); + } + } + + @Test + void mapErr() { + MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1); + assertEquals(Result.newErr(7), Result.newErr(6).mapErr(mfAdd1)); + mfAdd1.assertCalledOnceWith(6); + mfAdd1.reset(); + assertEquals(Result.newOk(6), Result.newOk(6).mapErr(mfAdd1)); + mfAdd1.assertNotCalled(); + } + + @Test + void ifThenElse_void() { + MockedConsumer intConsumer = new MockedConsumer<>(); + MockedConsumer strConsumer = new MockedConsumer<>(); + { + getOk().ifThenElse_void(intConsumer, strConsumer); + intConsumer.assertCalledOnceWith(314); + strConsumer.assertNotCalled(); + } + intConsumer.reset(); + strConsumer.reset(); + { + getErr().ifThenElse_void(intConsumer, strConsumer); + intConsumer.assertNotCalled(); + strConsumer.assertCalledOnceWith("TESTING_ERROR"); + } + } + + @Test + void ifThenElse_func() { + MockedFunction intFn = new MockedFunction<>("intFn_return"); + MockedFunction strFn = new MockedFunction<>("strFn_return"); + { + assertEquals("intFn_return", getOk().ifThenElse(intFn, strFn)); + intFn.assertCalledOnceWith(314); + strFn.assertNotCalled(); + } + intFn.reset(); + strFn.reset(); + { + assertEquals("strFn_return", getErr().ifThenElse(intFn, strFn)); + intFn.assertNotCalled(); + strFn.assertCalledOnceWith("TESTING_ERROR"); + } + } + + @Test + void inspect() { + MockedConsumer intCons = new MockedConsumer<>(); + assertEquals(getOk(), getOk().inspect(intCons)); + intCons.assertCalledOnceWith(314); + intCons.reset(); + assertEquals(getErr(), getErr().inspect(intCons)); + intCons.assertNotCalled(); + } + + @Test + void inspectErr() { + MockedConsumer strCons = new MockedConsumer<>(); + assertEquals(getErr(), getErr().inspectErr(strCons)); + strCons.assertCalledOnceWith("TESTING_ERROR"); + strCons.reset(); + assertEquals(getOk(), getOk().inspectErr(strCons)); + strCons.assertNotCalled(); + } + + @Test + void runIfOk() { + MockedConsumer intCons = new MockedConsumer<>(); + assertEquals(getOk(), getOk().runIfOk(intCons)); + intCons.assertCalledOnceWith(314); + intCons.reset(); + assertEquals(getErr(), getErr().runIfOk(intCons)); + intCons.assertNotCalled(); + } + + @Test + void runIfErr() { + MockedConsumer strCons = new MockedConsumer<>(); + assertEquals(getErr(), getErr().runIfErr(strCons)); + strCons.assertCalledOnceWith("TESTING_ERROR"); + strCons.reset(); + assertEquals(getOk(), getOk().runIfErr(strCons)); + strCons.assertNotCalled(); + } + + @Test + void stream() { + assertEquals(List.of(314), getOk().stream().toList()); + assertEquals(List.of(), getErr().stream().toList()); + } + + @Test + void iterator() { + { + Iterator oks = getOk().iterator(); + assertTrue(oks.hasNext()); + assertEquals(314, oks.next()); + assertFalse(oks.hasNext()); + } + { + List ls = new ArrayList<>(); + getOk().iterator().forEachRemaining(ls::add); + assertEquals(List.of(314), ls); + } + { + Iterator oks = getErr().iterator(); + assertFalse(oks.hasNext()); + } + { + List ls = new ArrayList<>(); + getErr().iterator().forEachRemaining(ls::add); + assertEquals(List.of(), ls); + } + } + + @Test + void forEach() { + MockedConsumer intCons = new MockedConsumer<>(); + { + getOk().forEach(intCons); + intCons.assertCalledOnceWith(314); + } + intCons.reset(); + { + getErr().forEach(intCons); + intCons.assertNotCalled(); + } + } + + @Test + void unwrap() { + assertEquals(314, assertDoesNotThrow(() -> getOk().unwrap())); + { + Result err = getErr(); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, err::unwrap); + assertEquals("TESTING_ERROR", exc.getValue()); + assertEquals("unwrap() got Err value: TESTING_ERROR", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause when Err is a string"); + } + { + MyCustomException customExc = new MyCustomException("CUSTOM_ERR_VALUE"); + Result err = Result.newErr(customExc); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, err::unwrap); + assertEquals(customExc, exc.getValue()); + assertEquals("unwrap() got Err value: " + + "net.marcellperger.mathexpr.util.rs.ResultTest$MyCustomException:" + + " CUSTOM_ERR_VALUE", exc.getMessage()); + assertEquals(customExc, exc.getCause()); + } + } + + @SuppressWarnings("unused") + static class MyCustomException extends RuntimeException { + public MyCustomException() { + } + + public MyCustomException(String message) { + super(message); + } + + public MyCustomException(String message, Throwable cause) { + super(message, cause); + } + + public MyCustomException(Throwable cause) { + super(cause); + } + } + + @Test + void expect() { + assertEquals(314, assertDoesNotThrow(() -> getOk().expect("EXPECT_MSG"))); + { + Result err = getErr(); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, () -> err.expect("EXPECT_MSG")); + assertEquals("TESTING_ERROR", exc.getValue()); + assertEquals("EXPECT_MSG: TESTING_ERROR", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause when Err is a string"); + } + { + MyCustomException customExc = new MyCustomException("CUSTOM_ERR_VALUE"); + Result err = Result.newErr(customExc); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, () -> err.expect("EXPECT_MSG")); + assertEquals(customExc, exc.getValue()); + assertEquals("EXPECT_MSG: " + + "net.marcellperger.mathexpr.util.rs.ResultTest$MyCustomException:" + + " CUSTOM_ERR_VALUE", exc.getMessage()); + assertEquals(customExc, exc.getCause()); + } + } + + @Test + void unwrapErr() { + assertEquals("TESTING_ERROR", assertDoesNotThrow(() -> getErr().unwrapErr())); + { + Result err = getOk(); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, err::unwrapErr); + assertEquals(314, exc.getValue()); + assertEquals("unwrapErr() got Ok value: 314", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause for Ok"); + } + { + MyCustomException customExc = new MyCustomException("CUSTOM_ERR_VALUE"); + Result err = Result.newOk(customExc); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, err::unwrapErr); + assertEquals(customExc, exc.getValue()); + assertEquals("unwrapErr() got Ok value: " + + "net.marcellperger.mathexpr.util.rs.ResultTest$MyCustomException:" + + " CUSTOM_ERR_VALUE", exc.getMessage()); + assertNull(exc.getCause(), "Don't set cause for Ok"); + } + } + + @Test + void expectErr() { + assertEquals("TESTING_ERROR", assertDoesNotThrow(() -> getErr().expectErr("EXPECT_ERR_MSG"))); + { + Result err = getOk(); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, () -> err.expectErr("EXPECT_ERR_MSG")); + assertEquals(314, exc.getValue()); + assertEquals("EXPECT_ERR_MSG: 314", exc.getMessage()); + assertNull(exc.getCause(), "Expected no cause for Ok"); + } + { + MyCustomException customExc = new MyCustomException("CUSTOM_ERR_VALUE"); + Result err = Result.newOk(customExc); + ResultPanicWithValueException exc = assertThrows( + ResultPanicWithValueException.class, () -> err.expectErr("EXPECT_ERR_MSG")); + assertEquals(customExc, exc.getValue()); + assertEquals("EXPECT_ERR_MSG: " + + "net.marcellperger.mathexpr.util.rs.ResultTest$MyCustomException:" + + " CUSTOM_ERR_VALUE", exc.getMessage()); + assertNull(exc.getCause(), "Don't set cause for Ok"); + } + } + + @Test + void and() { + assertEquals(Result.newOk(91L), getOk().and(Result.newOk(91L))); + assertEquals(Result.newErr("ERR_RIGHT"), getOk().and(Result.newErr("ERR_RIGHT"))); + assertEquals(Result.newErr("TESTING_ERROR"), getErr().and(Result.newOk(91L))); + assertEquals(Result.newErr("TESTING_ERROR"), getErr().and(Result.newErr("ERR_RIGHT"))); + } + + @Test + void andThen() { + MockedFunction> mfOk = new MockedFunction<>(Result.newOk(91L)); + MockedFunction> mfErr = new MockedFunction<>(Result.newErr("ERR_RIGHT")); + { + assertEquals(Result.newOk(91L), getOk().andThen(mfOk)); + mfOk.assertCalledOnceWith(314); + assertEquals(Result.newErr("ERR_RIGHT"), getOk().andThen(mfErr)); + mfErr.assertCalledOnceWith(314); + } + mfOk.reset(); + mfErr.reset(); + { + assertEquals(Result.newErr("TESTING_ERROR"), getErr().andThen(mfOk)); + mfOk.assertNotCalled(); + assertEquals(Result.newErr("TESTING_ERROR"), getErr().andThen(mfErr)); + mfErr.assertNotCalled(); + } + } + + @Test + void or() { + assertEquals(getOk(), getOk().or(Result.newOk(271))); + assertEquals(getOk(), getOk().or(Result.newErr("ERR_RIGHT"))); + assertEquals(Result.newOk(271), getErr().or(Result.newOk(271))); + assertEquals(Result.newErr("ERR_RIGHT"), getErr().or(Result.newErr("ERR_RIGHT"))); + } + + @Test + void orElse() { + MockedFunction> mfOk = new MockedFunction<>(Result.newOk(271)); + MockedFunction> mfErr = new MockedFunction<>(Result.newErr("ERR_RIGHT")); + { + assertEquals(getOk(), getOk().orElse(mfOk)); + mfOk.assertNotCalled(); + assertEquals(getOk(), getOk().orElse(mfErr)); + mfErr.assertNotCalled(); + } + mfOk.reset(); + mfErr.reset(); + { + assertEquals(Result.newOk(271), getErr().orElse(mfOk)); + mfOk.assertCalledOnceWith("TESTING_ERROR"); + assertEquals(Result.newErr("ERR_RIGHT"), getErr().orElse(mfErr)); + mfErr.assertCalledOnceWith("TESTING_ERROR"); + } + } + + @Test + void unwrapOr() { + assertEquals(314, getOk().unwrapOr(-1)); + assertEquals(-1, getErr().unwrapOr(-1)); + } + + @Test + void unwrapOrElse() { + MockedFunction mf = new MockedFunction<>(271); + { + assertEquals(314, getOk().unwrapOrElse(mf)); + mf.assertNotCalled(); + } + mf.reset(); + { + assertEquals(271, getErr().unwrapOrElse(mf)); + mf.assertCalledOnceWith("TESTING_ERROR"); + } + } + + @Test + void test_toString() { + assertEquals("Ok(314)", getOk().toString()); + assertEquals("Err(TESTING_ERROR)", getErr().toString()); + } + + @Test + void okOpt() { + assertEquals(Optional.of(new Ok<>(314)), getOk().okOpt()); + assertEquals(Optional.empty(), getErr().okOpt()); + } + + @Test + void errOpt() { + assertEquals(Optional.empty(), getOk().errOpt()); + assertEquals(Optional.of(new Err<>("TESTING_ERROR")), getErr().errOpt()); + } + + @Test + void okOption() { + assertEquals(Option.newSome(314), getOk().okOption()); + assertEquals(Option.newNone(), getErr().okOption()); + } + + @Test + void errOption() { + assertEquals(Option.newNone(), getOk().errOption()); + assertEquals(Option.newSome("TESTING_ERROR"), getErr().errOption()); + } + + @SuppressWarnings("CommentedOutCode") + @Test + void fromTry() { + assertEquals(Result.newOk(314), Result.fromTry(() -> 314, CustomException.class)); + CustomException ce = new CustomException("Unchecked (expected) exception"); + CheckedCustomException cce = new CheckedCustomException("Checked (expected) exception"); + assertEquals(Result.newErr(cce), Result.fromTry(() -> {throw cce;}, CheckedCustomException.class)); + assertEquals(Result.newErr(ce), Result.fromTry(() -> {throw ce;}, CustomException.class)); + UnexpectedCustomException uce = new UnexpectedCustomException("Unexpected unchecked exc"); + assertThrows(UnexpectedCustomException.class, () -> Result.fromTry(() -> {throw uce;}, CustomException.class)); + // This doesn't compile so GOOD! (How do I write a test that something DOESN'T compile???) + // UnexpectedCheckedCustomException ucc = new UnexpectedCheckedCustomException("Unexpected checked exc"); + // assertThrows(UnexpectedCheckedCustomException.class, () -> Result.fromTry(() -> {throw ucc;}, CheckedCustomException.class)); + } + + static class UnexpectedCustomException extends RuntimeException { + public UnexpectedCustomException(String message) { + super(message); + } + } + static class CustomException extends RuntimeException { + public CustomException(String message) { + super(message); + } + } + static class CheckedCustomException extends Exception { + public CheckedCustomException(String message) { + super(message); + } + } + @SuppressWarnings("unused") // used in the does-not-compile test + static class UnexpectedCheckedCustomException extends Exception { + public UnexpectedCheckedCustomException(String message) { + super(message); + } + } + + @Test + void fromExc() { + CheckedCustomException cce = new CheckedCustomException("Message"); + Result result = inner0(cce); + CheckedCustomException cceOut = result.expectErr("fromExc() didn't return Err value"); + assertEquals(cce, cceOut); + + StackTraceElement currFrame = getCurrentStack(); + List st = Arrays.stream(cceOut.getStackTrace()) + .filter(f -> f.getClassName().equals(currFrame.getClassName())) + .filter(f -> Objects.equals(f.getFileName(), currFrame.getFileName())) + .filter(f -> Objects.equals(f.getModuleName(), currFrame.getModuleName())) + .filter(f -> Objects.equals(f.getModuleVersion(), currFrame.getModuleVersion())) + .filter(f -> Objects.equals(f.getClassLoaderName(), currFrame.getClassLoaderName())) + .toList(); + List i0 = st.stream() + .filter(f -> f.getMethodName().equals("inner0")) + .toList(); + assertEquals(1, i0.size(), "Expected exactly 1 inner0 frame"); + List i1 = st.stream() + .filter(f -> f.getMethodName().equals("inner0")) + .toList(); + assertEquals(1, i1.size(), "Expected exactly 1 inner1 frame"); + } + + private StackTraceElement getCurrentStack() { + return StackWalker.getInstance() + .walk(s -> /*Skip this methods*/s.skip(1).findFirst()) + .orElseThrow().toStackTraceElement(); + } + + private Result inner0(CheckedCustomException exc) { + return inner1(exc); + } + private Result inner1(CheckedCustomException exc) { + return Result.fromExc(exc); + } +}