Skip to content

Latest commit

 

History

History
218 lines (134 loc) · 6.14 KB

README.md

File metadata and controls

218 lines (134 loc) · 6.14 KB

e-scala

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.

Installation

If you use SBT, add following to your build.sbt:

libraryDependencies += "dev.akif" %% "e-scala" % "@VERSION@"

If you use Maven, add following to your pom.xml:

<dependencies>
    <dependency>
        <groupId>dev.akif</groupId>
        <artifactId>e-scala_3</artifactId>
        <version>@VERSION@</version>
    </dependency>
</dependencies>

If you use Gradle, add following to your project's build.gradle:

dependencies
{
    implementation('dev.akif:e-scala_3:@VERSION@')
}

Contents

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.*

1. E

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

1.1. Creating an E

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)

1.2. Accessing Data in E

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

1.3. Converting E

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]

2. EOr

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.

2.1. Creating an EOr

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) }

2.2. Accessing Content of an EOr

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

2.3. Working With EOr

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)

3. Codec, Decoder and Encoder

e-scala provides definitions for implementing decoding/encoding mechanism for E and EOr.

  • Decoder[I, O] is for building an O (output) from an I (input) while handling the decoding failures with E
  • Encoder[I, O] is for building an O (output) from an I (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 same T (target) type