Skip to content

Commit

Permalink
Merge pull request #37 from Tradeshift/anyOf
Browse files Browse the repository at this point in the history
Introduce "allOf", as antonym to the existing "alternatively"
  • Loading branch information
jypma authored Oct 10, 2016
2 parents feea74d + 06cb815 commit 09c2dec
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public Try<T> apply(JSONEvent evt) {

@Override
public String toString() {
return "" + field + innerProtocol;
return "" + field + "(" + innerProtocol + ")";
}
};
}
Expand Down Expand Up @@ -142,7 +142,7 @@ public Seq<JSONEvent> reset() {

@Override
public String toString() {
return "" + field + innerProtocol;
return "" + field + "(" + innerProtocol + ")";
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,46 @@ public class ValueProtocol<T> implements Protocol<JSONEvent, T> {
// 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> INTEGER = of("(signed 32-bit integer)",
evt -> Try.of(() -> Integer.parseInt(evt.getValueAsString())),
public static final ValueProtocol<Integer> 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> LONG = of("(signed 64-bit integer)",
evt -> Try.of(() -> Long.parseLong(evt.getValueAsString())),
public static final ValueProtocol<Long> 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> BIGDECIMAL = of("(arbitrary precision decimal)",
evt -> Try.of(() -> new BigDecimal(evt.getValueAsString())),
public static final ValueProtocol<BigDecimal> 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> BIGINTEGER = of("(arbitrary precision integer)",
evt -> Try.of(() -> new BigInteger(evt.getValueAsString())),
public static final ValueProtocol<BigInteger> 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> BOOLEAN = of("(boolean)",
v -> Try.of(() -> v.getValueAsString().equals("true")),
public static final ValueProtocol<Boolean> 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> STRING = of("(string)",
evt -> Try.success(evt.getValueAsString()),
static final ValueProtocol<String> STRING = of("string",
evt -> evt.getValueAsString(),
s -> new JSONEvent.StringValue(s));

private static final Logger log = LoggerFactory.getLogger(ValueProtocol.class);

private final Function1<Value, Try<T>> tryRead;
private final Function1<Value, T> tryRead;
private final Function1<T,Value> write;
private final String description;

public static <T> ValueProtocol<T> of(String description, Function1<Value, Try<T>> tryRead, Function1<T, Value> write) {
public static <T> ValueProtocol<T> of(String description, Function1<Value, T> tryRead, Function1<T, Value> write) {
return new ValueProtocol<>(description, tryRead, write);
}

protected ValueProtocol(String description, Function1<Value, Try<T>> tryRead, Function1<T, Value> write) {
protected ValueProtocol(String description, Function1<Value, T> tryRead, Function1<T, Value> write) {
this.description = description;
this.tryRead = tryRead;
this.write = write;
Expand All @@ -87,7 +87,15 @@ public Try<T> apply(JSONEvent evt) {
}

if (level == 0 && evt instanceof JSONEvent.Value) {
Try<T> result = tryRead.apply(JSONEvent.Value.class.cast(evt));
Try<T> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
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;

import javaslang.collection.Seq;
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<E,T> implements ReadProtocol<E,T> {
private static final Logger log = LoggerFactory.getLogger(AlternativesProtocol.class);
public class AnyOfProtocol<E,T> implements ReadProtocol<E,T> {
private static final Logger log = LoggerFactory.getLogger(AnyOfProtocol.class);

private final Seq<ReadProtocol<E,T>> alternatives;

public AlternativesProtocol(Seq<ReadProtocol<E,T>> alternatives) {
public AnyOfProtocol(Seq<ReadProtocol<E,T>> alternatives) {
this.alternatives = alternatives;
}

Expand All @@ -28,26 +30,18 @@ public Reader<E,T> reader() {
return new Reader<E,T>() {
@Override
public Try<T> reset() {
Try<T> result = none();
for (Reader<E,T> reader: readers) {
Try<T> 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<T> apply(E evt) {
return perform(r -> r.apply(evt));
}

private Try<T> perform(Function<Reader<E,T>, Try<T>> f) {
Try<T> result = none();
for (Reader<E,T> reader: readers) {
Try<T> readerResult = reader.apply(evt);
Try<T> readerResult = f.apply(reader);
log.debug("reader {} said {}", reader, readerResult);
if (!isNone(readerResult)) {
if (isNone(result) || (result.isFailure() && readerResult.isSuccess())) {
Expand All @@ -59,7 +53,6 @@ public Try<T> apply(E evt) {
}
return result;
}

};
}

Expand All @@ -68,7 +61,7 @@ public Try<T> apply(E evt) {
*/
public static <E,T> Protocol<E,T> readWrite(Seq<Protocol<E,T>> alternatives) {
Protocol<E,T> write = alternatives.head();
AlternativesProtocol<E,T> read = new AlternativesProtocol<E,T>(Seq.narrow(alternatives));
AnyOfProtocol<E,T> read = new AnyOfProtocol<>(Seq.narrow(alternatives));
return new Protocol<E,T>() {
@Override
public Writer<E,T> writer() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<E,T> implements ReadProtocol<E,Seq<T>> {
private final Seq<ReadProtocol<E,T>> alternatives;

public CombinedProtocol(Seq<ReadProtocol<E,T>> alternatives) {
this.alternatives = alternatives;
}

@Override
public Reader<E,Seq<T>> reader() {
Seq<Reader<E,T>> readers = alternatives.map(p -> p.reader());
return new Reader<E,Seq<T>>() {
@Override
public Try<Seq<T>> reset() {
return perform(r -> r.reset());
}

@Override
public Try<Seq<T>> apply(E evt) {
return perform(r -> r.apply(evt));
}

private Try<Seq<T>> perform(Function<Reader<E,T>, Try<T>> f) {
Try<Seq<T>> result = none();
for (Reader<E,T> reader: readers) {
Try<T> 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;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,34 @@ public Writer<T, T> writer() {
*/
@SafeVarargs
@SuppressWarnings("varargs")
public static <E,T> ReadProtocol<E,T> alternatively(ReadProtocol<E,T> first, ReadProtocol<E,T> second, ReadProtocol<E,T>... others) {
return new AlternativesProtocol<>(Vector.of(first, second).appendAll(Arrays.asList(others)));
public static <E,T> ReadProtocol<E,T> anyOf(ReadProtocol<E,T> first, ReadProtocol<E,T> second, ReadProtocol<E,T>... 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 <E,T> Protocol<E,T> alternatively(Protocol<E,T> first, Protocol<E,T> second, Protocol<E,T>... others) {
return AlternativesProtocol.readWrite(Vector.of(first, second).appendAll(Arrays.asList(others)));
public static <E,T> Protocol<E,T> anyOf(Protocol<E,T> first, Protocol<E,T> second, Protocol<E,T>... 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 <E,T> ReadProtocol<E,Seq<T>> combine(ReadProtocol<E,T> first, ReadProtocol<E,T> second, ReadProtocol<E,T>... others) {
return new CombinedProtocol<>(Vector.of(first, second).appendAll(Arrays.asList(others)));
}

// ----------------------- Collections -----------------------------

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -273,7 +273,7 @@ public class JSONProtocolSpec {{

describe("a JSONProtocol with several alternatives", () -> {

ReadProtocol<JSONEvent, DTO2> proto = alternatively(
ReadProtocol<JSONEvent, DTO2> proto = anyOf(
object(
option(field("i", integerValue)),
i -> new DTO2(Option.none(), i)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<JSONEvent, Seq<Integer>> 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<JSONEvent, Seq<Object>> 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<JSONEvent, Seq<Integer>> protocol = combine(
integerValue,
integerValue.map(i -> i * 2)
);

assertThatThrownBy(() -> jackson.parse("\"hello\"", protocol.reader())).hasMessageContaining("Expecting signed 32-bit integer");
});
});
}}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -206,7 +205,7 @@ public class XMLProtocolSpec {{
dto -> dto.getL()
);

Protocol<XMLEvent,DTO1> proto = alternatively(
Protocol<XMLEvent,DTO1> proto = anyOf(
protoV1.having(attribute("version"), "1"),
protoV2.having(attribute("version"), "2")
);
Expand Down Expand Up @@ -259,7 +258,7 @@ public class XMLProtocolSpec {{
dto -> dto.getL()
);

Protocol<XMLEvent,DTO1> proto = alternatively(
Protocol<XMLEvent,DTO1> proto = anyOf(
protoV2.having(
attribute("version"), "2"
),
Expand Down

0 comments on commit 09c2dec

Please sign in to comment.