This is the main implementation of e in Scala. 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 SBT, add following to your build.sbt
:
libraryDependencies += "dev.akif" %% "e-scala" % "3.0.1"
If you use Maven, add following to your pom.xml
:
<dependencies>
<dependency>
<groupId>dev.akif</groupId>
<artifactId>e-scala_3</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
If you use Gradle, add following to your project's build.gradle
:
dependencies
{
implementation('dev.akif:e-scala_3:3.0.1')
}
Below are some details and examples of e-scala's content. For more, please check corresponding automated tests and e-scala's documentation.
To get started, add following import which will cover all your needs:
import e.scala.*
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 | Option[Int] |
A numeric code identifying the error | None |
name | Option[String] |
A name identifying the error, usually enum-like | None |
message | Option[String] |
A message about the error, usually human-readable | None |
causes | List[E] |
Underlying cause(s) of the error, if any | List.empty |
data | Map[String, String] |
Arbitrary data related to the error as a key-value map | Map.empty |
time | Option[Long] |
Time when this error occurred as milliseconds since Epoch | None |
An instance of E can be created by
- directly creating an instance
- modifying an existing instance
- using static constructor methods
import e.scala.*
E.empty
val notSoEmpty = E(Some(1), Some("error-name"), Some("Error Message"))
E.name("test-error").message("Test")
val unexpectedError = E(message = Some("Unexpected Error"), code = Some(-1)).now
val errorWithDataAndCause = unexpectedError.data("action" -> "test").cause(notSoEmpty)
Since E is a case class, you can directly access its fields. There are additional methods as well.
import e.scala.*
val databaseError = E.name("database").code(1)
val error = E(message = Some("Cannot get user!"), name = Some("Unknown")).cause(databaseError)
error.message
error.code orElse (error.causes.headOption.flatMap(_.code))
error.hasData
You can convert your E into an Exception or an EOr.
import e.scala.*
val error = E.name("test").message("Test")
error.toException
error.toEOr[Int]
EOr[A] (which can be aliased with a syntactic sugar as: E or A
) is a container that can either be a Failure
containing an E or Success
containing a value of type A
. It is semantically a combination of Scala's Either
and Try
but it is specialized for E in error case.
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 extension methods
import e.scala.*
EOr[Boolean](E.code(0))
EOr("hello")
EOr.Failure(E.code(1))
EOr.Success("test")
E.message("test").toEOr[Int]
"hello".toEOr
Some(true).toEOr(E.code(2))
Option.empty[String].toEOr(E.code(3))
EOr.fromEither[Int, String](Right("hello")) { left => E.code(left) }
EOr.fromEither[Int, String](Left(4)) { left => E.code(left) }
The error or the value inside an EOr can be accessed safely.
import e.scala.*
val eor1 = E.message("test").toEOr[Int]
val eor2 = "hello".toEOr
eor1.hasValue
eor1.error
eor2.hasError
eor2.value
There are many methods in EOr for modifying, composing, handling the error etc.
import e.scala.*
val eor1 = E.message("test").toEOr[Int]
val eor2 = "hello".toEOr
eor2.map(_.toUpperCase)
eor1.mapError(_.code(1))
eor2.flatMap(s => eor1)
eor2.filter(_.length < 3)
eor1.getOrElse(0)
eor1.fold(e => e.code, i => Some(i)).getOrElse(0)
case class Person(name: String, age: Int)
def makePerson(name: String, age: Int): EOr[Person] =
// You can do for-comprehensions too!
for
n <- EOr(name) if name.nonEmpty
a <- EOr(age).filter(_ > 0, invalidAge => E.name("invalid-age").data("value" -> invalidAge)) // custom error for filtering
yield
Person(n, a)
makePerson("", 5)
makePerson("Akif", -1)
makePerson("Akif", 29)
e-scala 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