This is the main implementation of e in Java. It contains two main types E and EOr. It also contains definitions of decoding and encoding for these. Implementations of decoding and encoding are provided in separate modules.
If you use Gradle, add following to your project's build.gradle
:
dependencies {
implementation('dev.akif:e-java:3.0.1')
}
If you use Maven, add following to your pom.xml
:
<dependencies>
<dependency>
<groupId>dev.akif</groupId>
<artifactId>e-java</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
If you use SBT, add following to your build.sbt
:
libraryDependencies += "dev.akif" % "e-java" % "3.0.1"
Below are some details and examples of e-java's content. For more, please check corresponding automated tests and e-java's documentation.
To get started, add following import which will cover all your needs:
import e.java.*;
E (short for error) is the main error type used to represent an error. It is an immutable object with a fluent API. It contains following data about the error.
Field | Type | Description | Default Value |
---|---|---|---|
code | Optional<Integer> |
A numeric code identifying the error | Optional.empty() |
name | Optional<String> |
A name identifying the error, usually enum-like | Optional.empty() |
message | Optional<String> |
A message about the error, usually human-readable | Optional.empty() |
causes | List<E> |
Underlying cause(s) of the error, if any | new LinkedList<>() |
data | Map<String, String> |
Arbitrary data related to the error as a key-value map | new LinkedHashMap<>() |
time | Optional<Long> |
Time when this error occurred as milliseconds since Epoch | Optional.empty() |
An instance of E can be created by
- directly creating an instance
- modifying an existing instance
- using static constructor methods
import e.java.*;
E empty = E.empty;
// {}
E notSoEmpty = new E(1, "error-name", "Error Message", null, null, null);
// {"code":1,"name":"error-name","message":"Error Message"}
E e1 = E.fromName("test-error").message("Test");
// {"name":"test-error","message":"Test"}
E unexpectedError = E.fromMessage("Unexpected Error").code(-1).now();
// {"code":-1,"message":"Unexpected Error","time":1595936239845}
E errorWithDataAndCause = unexpectedError.data("action", "test").cause(notSoEmpty);
// {"code":-1,"message":"Unexpected Error","causes":[{"code":1,"name":"error-name","message":"Error Message"}],"data":{"action":"test"},"time":1595936239845}
Since E is a restricted POJO, you can access its fields by accessor methods. There are additional methods as well.
import e.java.*;
E databaseError = E.fromName("database").code(1);
// {"code":1,"name":"database"}
E error = E.fromMessage("Cannot get user!").name("Unknown").cause(databaseError);
// {"name":"Unknown","message":"Cannot get user!","causes":[{"code":1,"name":"database"}]}
Optional<String> message = error.message();
// "Cannot get user!"
Integer code = error.code().orElseGet(
() -> error.causes
.stream()
.findFirst()
.flatMap(E::code)
);
// 1
boolean hasData = error.hasData();
// false
You can convert your E into an Exception or an EOr.
import e.java.*;
E error = E.fromName("test").message("Test");
// {"name":"test","message":"Test"}
EException ee = error.toException();
// {"name":"test","message":"Test"}
EOr<Integer> eor = error.toEOr<Integer>();
// {"name":"test","message":"Test"}
EOr<A> is a container that can either be a Failure
containing an E or Success
containing a value of type A
.
An instance of EOr can be created by
- directly creating an instance of Failure or Success
- modifying an existing instance
- using static constructor methods
- constructing from an E or a value
- converting from other types by utility methods
import e.java.*;
EOr<Boolean> eor1 = new EOr<>(E.fromCode(0));
// {"code":0}
EOr<String> eor2 = new EOr<>("hello");
// hello
EOr<Boolean> eor3 = new EOr.Failure<>(E.fromCode(1));
// {"code":1}
EOr<String> eor4 = new EOr.Success<>("test");
// test
EOr<Integer> eor5 = E.fromMessage("test").toEOr<>();
// {"message":"test"}
EOr<String> eor6 = EOr.from("hello");
// hello
EOr<String> eor7 = EOr.from(E.fromCode(2));
// {"code":2}
EOr<String> eor8 = EOr.fromNullable(null, () -> E.fromCode(3));
// {"code":3}
EOr<String> eor9 = EOr.fromNullable("test", () -> E.fromCode(3));
// test
The error or the value inside an EOr can be accessed safely.
import e.java.*;
EOr<Integer> eor1 = E.fromMessage("test").toEOr<>();
// {"message":"test"}
EOr<String> eor2 = EOr.from("hello");
// eor2: EOr<String> = hello
boolean hasValue = eor1.hasValue();
// false
Optional<E> e = eor1.error();
// {"message":"test"}
boolean hasError = eor2.hasError();
// false
Optional<String> value = eor2.value();
// hello
There are many methods in EOr for modifying, composing, handling the error etc.
import e.java.*;
EOr<Integer> eor1 = E.fromMessage("test").toEOr<>();
// {"message":"test"}
EOr<String> eor2 = EOr.from("hello");
// hello
EOr<String> eor3 = eor2.map(String::toUpperCase);
// res27: EOr<String> = HELLO
EOr<Integer> eor4 = eor1.mapError(e -> e.code(1));
// {"code":1,"message":"test"}
EOr<Integer> eor5 = eor2.flatMap(s -> eor1);
// {"message":"test"}
EOr<String> eor6 = eor2.filter(s -> s.length() < 3);
// {"name":"filtered","message":"Condition does not hold!","data":{"value":"hello"}}
Integer i1 = eor1.getOrElse(0);
// 0
Integer i2 = eor1.fold(e -> e.code().orElse(0), i -> i);
// 0
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override public String toString() {
return "Person(" + name + "," + age + ")";
}
}
EOr<Person> makePerson(String name, int age) {
return new EOr(name)
.filter(n -> n.length() > 0)
.flatMap(n ->
new EOr(age)
.filter(
a -> a > 0,
// custom error for filtering
invalidAge -> E.fromName("invalid-age")
.data("value", invalidAge)
)
.map(a -> new Person(n, a))
);
}
EOr<Person> p1 = makePerson("", 5);
// {"name":"filtered","message":"Condition does not hold!","data":{"value":""}}
EOr<Person> p2 = makePerson("Akif", -1);
// {"name":"invalid-age","data":{"value":"-1"}}
EOr<Person> p3 = makePerson("Akif", 29);
// Person(Akif,29)
e-java provides definitions for implementing decoding/encoding mechanism for E and EOr.
- Decoder<I, O> is for building an
O
(output) from anI
(input) while handling the decoding failures with E - Encoder<I, O> is for building an
O
(output) from anI
(input) - Codec<S, T> is a combination of Decoder and Encoder for an
S
(source) type where output of Encoder and input of Decoder is the sameT
(target) type