Fork me on GitHub

ZIO Tutorial

This tutorial demonstrates using ZIO.

To focus on ZIO, this tutorial uses a simple application that retrieves a message from database via ZIO effect.

Tutorial Source

ZIO

The ZIO effect to retrieve a message from the database is the following:

object MessageService {

  def getMessage(id: Int): ZIO[MessageRepository, Throwable, Message] =
    ZIO.serviceWithZIO[MessageRepository](repository => ZIO.attempt(repository findById id orElseThrow(() => new NoSuchElementException(s"No message by id $id"))))

}

The effect is tested with the following code:

class MessageServiceTest extends AnyFlatSpec {

  it should "retrieve Message" in {
    val retrieve = for {
      m <- MessageService.getMessage(1)
    } yield m
    val message = Unsafe.unsafe { implicit unsafe =>
      runtime(1, "Hello World").unsafe.run(retrieve).getOrThrowFiberFailure()
    }
    assert("Hello World" == message.getContent)
  }

  def runtime(id: Int, content: String): Runtime[MessageRepository] =
    Runtime(ZEnvironment[MessageRepository](new TestMessageRepository(id, content)), FiberRefs.empty, RuntimeFlags.default)
}

Note: the test is not comprehensive, however, demonstrates testing the ZIO effect.

Servicing Request with ZIO

To integrate ZIO into a First-Class Procedure add the following to the pom.xml:

<dependency>
    <groupId>net.officefloor.scala</groupId>
    <artifactId>officescala_zio</artifactId>
</dependency>

This will setup the configured procedures to handle the returned ZIO from functions.

As ZIO environments provide dependency injection, the OfficeFloor injected dependencies need to be mapped onto the required ZIO environment. The following function demonstrates the mapping:

  def service(request: ZioRequest, repository: MessageRepository): ZIO[Any, Throwable, Message] = {

    // Service Logic
    val zio = for {
      m <- MessageService.getMessage(request.id)
      // possible further logic
    } yield m

    // Provide environment from dependency injection
    zio.provide(ZLayer.succeed(repository))
  }

This function is configured into a procedure. The ZIO handling by OfficeFloor then:

  1. Identifies a ZIO is returned from the function
  2. Confirms the ZIO does not require a custom environment. As above, environment needs to be provided from injected dependencies
  3. Unsafely runs the returned ZIO
  4. Provides the success as parameter to next procedure. Or throws any exception to be handled by configured OfficeFloor exception handlers

This allows ZIO to be used for writing modular functions of the application. The REST endpoint configuration then composes these functions to service the request:

service:
  class: net.officefloor.tutorial.ziohttpserver.ServiceLogic
  method: service
  next: send

send:
  class: net.officefloor.tutorial.ziohttpserver.ServiceLogic
  method: send

The next configured procedure sends the message response:

def send(@Parameter message: Message, response: ObjectResponse[ZioResponse]): Unit =
  response.send(new ZioResponse(message.getContent))

Repository injected from Spring

To complete the code of the application, a repository implementation is required. Given the repository needs to be dependency injected, the implementation of the repository is via Spring:

@Repository
public interface MessageRepository extends CrudRepository<Message, Integer> {
}

See the Transaction Tutorial for further information on configuring Spring repositories.

Testing

The following test demonstrates using ZIO to service a HTTP request:

class ZioHttpServerTest extends AnyFlatSpec with WoofRules {

  it should "get message" in {
    withMockWoofServer { server =>
      val request = mockRequest("/")
        .method(httpMethod("POST"))
        .header("Content-Type", "application/json")
        .entity(jsonEntity(new ZioRequest(1)))
      val response = server.send(request)
      response.assertResponse(200, jsonEntity(new ZioResponse("Hi via ZIO")))
    }
  }

}

Next

The next tutorial covers simple caching.