From 06cb8156621faa29bc332eb7d24d11ea5daf32f8 Mon Sep 17 00:00:00 2001 From: Jan Ypma Date: Mon, 10 Oct 2016 13:56:45 +0200 Subject: [PATCH] Introduce "combine", and rename "alternatively" to "anyOf" Also touched up some debugging strings so error messages are nice and informative. --- .../reaktive/json/FieldProtocol.java | 4 +- .../reaktive/json/ValueProtocol.java | 40 +++++++------ ...ativesProtocol.java => AnyOfProtocol.java} | 33 +++++------ .../reaktive/marshal/CombinedProtocol.java | 57 +++++++++++++++++++ .../tradeshift/reaktive/marshal/Protocol.java | 22 +++++-- .../reaktive/json/JSONProtocolSpec.java | 4 +- .../marshal/CombinedProtocolSpec.java | 51 +++++++++++++++++ .../reaktive/xml/XMLProtocolSpec.java | 7 +-- 8 files changed, 170 insertions(+), 48 deletions(-) rename ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/{AlternativesProtocol.java => AnyOfProtocol.java} (65%) create mode 100644 ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/CombinedProtocol.java create mode 100644 ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/marshal/CombinedProtocolSpec.java diff --git a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/FieldProtocol.java b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/FieldProtocol.java index 9287f70e..2f425e0c 100644 --- a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/FieldProtocol.java +++ b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/FieldProtocol.java @@ -81,7 +81,7 @@ public Try apply(JSONEvent evt) { @Override public String toString() { - return "" + field + innerProtocol; + return "" + field + "(" + innerProtocol + ")"; } }; } @@ -142,7 +142,7 @@ public Seq reset() { @Override public String toString() { - return "" + field + innerProtocol; + return "" + field + "(" + innerProtocol + ")"; } }; } diff --git a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/ValueProtocol.java b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/ValueProtocol.java index f69dd4e7..3d40076e 100644 --- a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/ValueProtocol.java +++ b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/json/ValueProtocol.java @@ -22,46 +22,46 @@ public class ValueProtocol implements Protocol { // For everything that marshals to strings, use stringValue.as(...) /** A Java integer represented as a JSON number (on reading, JSON string is also allowed) */ - public static final ValueProtocol INTEGER = of("(signed 32-bit integer)", - evt -> Try.of(() -> Integer.parseInt(evt.getValueAsString())), + public static final ValueProtocol INTEGER = of("signed 32-bit integer", + evt -> Integer.parseInt(evt.getValueAsString()), i -> new JSONEvent.NumericValue(String.valueOf(i))); /** A Java long represented as a JSON number (on reading, JSON string is also allowed) */ - public static final ValueProtocol LONG = of("(signed 64-bit integer)", - evt -> Try.of(() -> Long.parseLong(evt.getValueAsString())), + public static final ValueProtocol LONG = of("signed 64-bit integer", + evt -> Long.parseLong(evt.getValueAsString()), l -> new JSONEvent.NumericValue(String.valueOf(l))); /** A Java big decimal represented as a JSON number (on reading, JSON string is also allowed) */ - public static final ValueProtocol BIGDECIMAL = of("(arbitrary precision decimal)", - evt -> Try.of(() -> new BigDecimal(evt.getValueAsString())), + public static final ValueProtocol BIGDECIMAL = of("arbitrary precision decimal", + evt -> new BigDecimal(evt.getValueAsString()), d -> new JSONEvent.NumericValue(String.valueOf(d))); /** A Java big integer represented as a JSON number (on reading, JSON string is also allowed) */ - public static final ValueProtocol BIGINTEGER = of("(arbitrary precision integer)", - evt -> Try.of(() -> new BigInteger(evt.getValueAsString())), + public static final ValueProtocol BIGINTEGER = of("arbitrary precision integer", + evt -> new BigInteger(evt.getValueAsString()), d -> new JSONEvent.NumericValue(String.valueOf(d))); /** A Java boolean represented a JSON boolean (on reading, a JSON string of "true" or "false" is also allowed) */ - public static final ValueProtocol BOOLEAN = of("(boolean)", - v -> Try.of(() -> v.getValueAsString().equals("true")), + public static final ValueProtocol BOOLEAN = of("boolean", + v -> v.getValueAsString().equals("true"), b -> b ? JSONEvent.TRUE : JSONEvent.FALSE); /** A Java String. Internal implementation, @see {@link StringValueProtocol} */ - static final ValueProtocol STRING = of("(string)", - evt -> Try.success(evt.getValueAsString()), + static final ValueProtocol STRING = of("string", + evt -> evt.getValueAsString(), s -> new JSONEvent.StringValue(s)); private static final Logger log = LoggerFactory.getLogger(ValueProtocol.class); - private final Function1> tryRead; + private final Function1 tryRead; private final Function1 write; private final String description; - public static ValueProtocol of(String description, Function1> tryRead, Function1 write) { + public static ValueProtocol of(String description, Function1 tryRead, Function1 write) { return new ValueProtocol<>(description, tryRead, write); } - protected ValueProtocol(String description, Function1> tryRead, Function1 write) { + protected ValueProtocol(String description, Function1 tryRead, Function1 write) { this.description = description; this.tryRead = tryRead; this.write = write; @@ -87,7 +87,15 @@ public Try apply(JSONEvent evt) { } if (level == 0 && evt instanceof JSONEvent.Value) { - Try result = tryRead.apply(JSONEvent.Value.class.cast(evt)); + Try result = Try.of(() -> { + try { + return tryRead.apply(JSONEvent.Value.class.cast(evt)); + } catch (IllegalArgumentException x) { + String msg = (x.getMessage() == null) ? "" : ": " + x.getMessage(); + throw new IllegalArgumentException ("Expecting " + description + msg); + } + }); + log.info("Read {}", result); return result; } else { diff --git a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AlternativesProtocol.java b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AnyOfProtocol.java similarity index 65% rename from ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AlternativesProtocol.java rename to ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AnyOfProtocol.java index 1c2b34f2..6e50340f 100644 --- a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AlternativesProtocol.java +++ b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/AnyOfProtocol.java @@ -3,6 +3,8 @@ import static com.tradeshift.reaktive.marshal.ReadProtocol.isNone; import static com.tradeshift.reaktive.marshal.ReadProtocol.none; +import java.util.function.Function; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,15 +12,15 @@ import javaslang.control.Try; /** - * Forwards read events to multiple alternative protocols, emitting whenever any of the alternatives emit. If multiple + * Forwards read events to multiple alternative protocols, emitting whenever any of the alternatives emit. If multiple * alternatives emit for the same event, the first one wins. */ -public class AlternativesProtocol implements ReadProtocol { - private static final Logger log = LoggerFactory.getLogger(AlternativesProtocol.class); +public class AnyOfProtocol implements ReadProtocol { + private static final Logger log = LoggerFactory.getLogger(AnyOfProtocol.class); private final Seq> alternatives; - public AlternativesProtocol(Seq> alternatives) { + public AnyOfProtocol(Seq> alternatives) { this.alternatives = alternatives; } @@ -28,26 +30,18 @@ public Reader reader() { return new Reader() { @Override public Try reset() { - Try result = none(); - for (Reader reader: readers) { - Try readerResult = reader.reset(); - log.debug("reset: reader {} said {}", reader, readerResult); - if (!isNone(readerResult)) { - if (isNone(result) || (result.isFailure() && readerResult.isSuccess())) { - result = readerResult; - } else if (readerResult.isFailure() && result.isFailure()) { - result = Try.failure(new IllegalArgumentException(result.failed().get().getMessage() + ", alternatively " + readerResult.failed().get().getMessage())); - } - } - } - return result; + return perform(r -> r.reset()); } @Override public Try apply(E evt) { + return perform(r -> r.apply(evt)); + } + + private Try perform(Function, Try> f) { Try result = none(); for (Reader reader: readers) { - Try readerResult = reader.apply(evt); + Try readerResult = f.apply(reader); log.debug("reader {} said {}", reader, readerResult); if (!isNone(readerResult)) { if (isNone(result) || (result.isFailure() && readerResult.isSuccess())) { @@ -59,7 +53,6 @@ public Try apply(E evt) { } return result; } - }; } @@ -68,7 +61,7 @@ public Try apply(E evt) { */ public static Protocol readWrite(Seq> alternatives) { Protocol write = alternatives.head(); - AlternativesProtocol read = new AlternativesProtocol(Seq.narrow(alternatives)); + AnyOfProtocol read = new AnyOfProtocol<>(Seq.narrow(alternatives)); return new Protocol() { @Override public Writer writer() { diff --git a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/CombinedProtocol.java b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/CombinedProtocol.java new file mode 100644 index 00000000..746296f6 --- /dev/null +++ b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/CombinedProtocol.java @@ -0,0 +1,57 @@ +package com.tradeshift.reaktive.marshal; + +import static com.tradeshift.reaktive.marshal.ReadProtocol.isNone; +import static com.tradeshift.reaktive.marshal.ReadProtocol.none; + +import java.util.function.Function; + +import javaslang.collection.Seq; +import javaslang.collection.Vector; +import javaslang.control.Try; + +/** + * Forwards read events to multiple alternative protocols, emitting whenever any of the alternatives emit. + * If multiple alternatives emit for the same event, all results are emitted. + * If at least one alternative emits for an event, any errors on other alternatives are ignored. + * If all alternatives yield errors for an event, the errors are concatenated and escalated. + */ +public class CombinedProtocol implements ReadProtocol> { + private final Seq> alternatives; + + public CombinedProtocol(Seq> alternatives) { + this.alternatives = alternatives; + } + + @Override + public Reader> reader() { + Seq> readers = alternatives.map(p -> p.reader()); + return new Reader>() { + @Override + public Try> reset() { + return perform(r -> r.reset()); + } + + @Override + public Try> apply(E evt) { + return perform(r -> r.apply(evt)); + } + + private Try> perform(Function, Try> f) { + Try> result = none(); + for (Reader reader: readers) { + Try readerResult = f.apply(reader); + if (!isNone(readerResult)) { + if (isNone(result) || (result.isFailure() && readerResult.isSuccess())) { + result = readerResult.map(Vector::of); + } else if (!result.isFailure() && readerResult.isSuccess()) { + result = result.map(seq -> seq.append(readerResult.get())); + } else if (readerResult.isFailure() && result.isFailure()) { + result = Try.failure(new IllegalArgumentException(result.failed().get().getMessage() + ", alternatively " + readerResult.failed().get().getMessage())); + } + } + } + return result; + } + }; + } +} diff --git a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/Protocol.java b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/Protocol.java index 9c4955ed..9f09b2ce 100644 --- a/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/Protocol.java +++ b/ts-reaktive-marshal/src/main/java/com/tradeshift/reaktive/marshal/Protocol.java @@ -46,20 +46,34 @@ public Writer writer() { */ @SafeVarargs @SuppressWarnings("varargs") - public static ReadProtocol alternatively(ReadProtocol first, ReadProtocol second, ReadProtocol... others) { - return new AlternativesProtocol<>(Vector.of(first, second).appendAll(Arrays.asList(others))); + public static ReadProtocol anyOf(ReadProtocol first, ReadProtocol second, ReadProtocol... others) { + return new AnyOfProtocol<>(Vector.of(first, second).appendAll(Arrays.asList(others))); } /** * Forwards read events to multiple alternative protocols, emitting whenever any of the alternatives emit. If multiple * alternatives emit for the same event, the first one wins. + * + * Always picks the first alternative during writing. */ @SafeVarargs @SuppressWarnings("varargs") - public static Protocol alternatively(Protocol first, Protocol second, Protocol... others) { - return AlternativesProtocol.readWrite(Vector.of(first, second).appendAll(Arrays.asList(others))); + public static Protocol anyOf(Protocol first, Protocol second, Protocol... others) { + return AnyOfProtocol.readWrite(Vector.of(first, second).appendAll(Arrays.asList(others))); } + /** + * Forwards read events to multiple alternative protocols, emitting whenever any of the alternatives emit. + * If multiple alternatives emit for the same event, all results are emitted. + * If at least one alternative emits for an event, any errors on other alternatives are ignored. + * If all alternatives yield errors for an event, the errors are concatenated and escalated. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public static ReadProtocol> combine(ReadProtocol first, ReadProtocol second, ReadProtocol... others) { + return new CombinedProtocol<>(Vector.of(first, second).appendAll(Arrays.asList(others))); + } + // ----------------------- Collections ----------------------------- /** diff --git a/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/json/JSONProtocolSpec.java b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/json/JSONProtocolSpec.java index 26797d9c..4d33df6b 100644 --- a/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/json/JSONProtocolSpec.java +++ b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/json/JSONProtocolSpec.java @@ -11,7 +11,7 @@ import static com.tradeshift.reaktive.json.JSONProtocol.longValue; import static com.tradeshift.reaktive.json.JSONProtocol.object; import static com.tradeshift.reaktive.json.JSONProtocol.stringValue; -import static com.tradeshift.reaktive.marshal.Protocol.alternatively; +import static com.tradeshift.reaktive.marshal.Protocol.anyOf; import static com.tradeshift.reaktive.marshal.Protocol.foldLeft; import static com.tradeshift.reaktive.marshal.Protocol.hashMap; import static com.tradeshift.reaktive.marshal.Protocol.option; @@ -273,7 +273,7 @@ public class JSONProtocolSpec {{ describe("a JSONProtocol with several alternatives", () -> { - ReadProtocol proto = alternatively( + ReadProtocol proto = anyOf( object( option(field("i", integerValue)), i -> new DTO2(Option.none(), i) diff --git a/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/marshal/CombinedProtocolSpec.java b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/marshal/CombinedProtocolSpec.java new file mode 100644 index 00000000..10f5dcb8 --- /dev/null +++ b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/marshal/CombinedProtocolSpec.java @@ -0,0 +1,51 @@ +package com.tradeshift.reaktive.marshal; + +import static com.tradeshift.reaktive.json.JSONProtocol.integerValue; +import static com.tradeshift.reaktive.json.JSONProtocol.stringValue; +import static com.tradeshift.reaktive.marshal.Protocol.combine; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.forgerock.cuppa.Cuppa.describe; +import static org.forgerock.cuppa.Cuppa.it; + +import org.forgerock.cuppa.junit.CuppaRunner; +import org.junit.runner.RunWith; + +import com.tradeshift.reaktive.json.JSONEvent; +import com.tradeshift.reaktive.json.jackson.Jackson; + +import javaslang.collection.Seq; + +@RunWith(CuppaRunner.class) +public class CombinedProtocolSpec {{ + describe("Protocol.allOf", () -> { + Jackson jackson = new Jackson(); + + it("should emit multiple results if multiple readers emit on the same event", () -> { + ReadProtocol> protocol = combine( + integerValue, + integerValue.map(i -> i * 2) + ); + + assertThat(jackson.parse("42", protocol.reader()).findFirst().get()).containsExactly(42, 84); + }); + + it("should emit an event if one reader emits and another yields an error", () -> { + ReadProtocol> protocol = combine( + stringValue.map(s -> (Object) s), + integerValue.map(i -> (Object) i) + ); + + assertThat(jackson.parse("\"hello\"", protocol.reader()).findFirst().get()).containsExactly("hello"); + }); + + it("should yield an error if all readers yield errors", () -> { + ReadProtocol> protocol = combine( + integerValue, + integerValue.map(i -> i * 2) + ); + + assertThatThrownBy(() -> jackson.parse("\"hello\"", protocol.reader())).hasMessageContaining("Expecting signed 32-bit integer"); + }); + }); +}} diff --git a/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/xml/XMLProtocolSpec.java b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/xml/XMLProtocolSpec.java index 42685092..36b49a9f 100644 --- a/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/xml/XMLProtocolSpec.java +++ b/ts-reaktive-marshal/src/test/java/com/tradeshift/reaktive/xml/XMLProtocolSpec.java @@ -1,7 +1,6 @@ package com.tradeshift.reaktive.xml; - -import static com.tradeshift.reaktive.marshal.Protocol.alternatively; +import static com.tradeshift.reaktive.marshal.Protocol.anyOf; import static com.tradeshift.reaktive.marshal.Protocol.arrayList; import static com.tradeshift.reaktive.marshal.Protocol.forEach; import static com.tradeshift.reaktive.marshal.Protocol.hashMap; @@ -206,7 +205,7 @@ public class XMLProtocolSpec {{ dto -> dto.getL() ); - Protocol proto = alternatively( + Protocol proto = anyOf( protoV1.having(attribute("version"), "1"), protoV2.having(attribute("version"), "2") ); @@ -259,7 +258,7 @@ public class XMLProtocolSpec {{ dto -> dto.getL() ); - Protocol proto = alternatively( + Protocol proto = anyOf( protoV2.having( attribute("version"), "2" ),