ZIO Tutorial

This tutorial demonstrates using ZIO within a Spring Boot application.

The application retrieves a message from an H2 database via a Spring Data JPA repository and returns it in an HTTP response.

Tutorial Source

Maven dependency

Add ZIO support alongside the Spring Boot starter:

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

Application class

Register DefaultScalaModule so that Jackson can serialise and deserialise Scala case classes used for the request and response:

@SpringBootApplication
class Application {

  @Bean
  def scalaJacksonModule(): JacksonModule = DefaultScalaModule
}

object Application extends App {
  SpringApplication.run(classOf[Application], args: _*)
}

ZIO service

The ZIO effect uses ZIO.serviceWithZIO to retrieve a message from the injected MessageRepository environment:

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

This is tested without any HTTP or Spring infrastructure:

class MessageServiceTest {

  @Test
  def retrieveMessage(): Unit = {
    val retrieve = for {
      m <- MessageService.getMessage(1)
    } yield m
    val message = Unsafe.unsafe { implicit unsafe =>
      runtime(1, "Hello World").unsafe.run(retrieve).getOrThrowFiberFailure()
    }
    assertEquals("Hello World", message.getContent)
  }

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

Service logic

The service method receives the @RequestBody and the Spring-injected MessageRepository. It wires the repository into the ZIO environment and returns ZIO[Any, Throwable, Message]. OfficeFloor (via officescala_zio) detects the ZIO return type, runs it, and passes the result as the @Parameter to the send method:

def service(@RequestBody request: ZioRequest, repository: MessageRepository): ZIO[Any, Throwable, Message] = {
  val zio = for {
    m <- MessageService.getMessage(request.id)
  } yield m
  zio.provide(ZLayer.succeed(repository))
}
def send(@Parameter message: Message, response: ObjectResponse[ZioResponse]): Unit =
  response.send(ZioResponse(message.getContent))

REST endpoint

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

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

Repository

The repository is a standard Spring Data JPA interface injected by Spring Boot:

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

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	private String content;

	public Message() {
	}

	public Message(Integer id, String content) {
		this.id = id;
		this.content = content;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getContent() {
		return this.content;
	}

	public void setContent(String content) {
		this.content = content;
	}
}

Request and Response

case class ZioRequest(id: Int)
case class ZioResponse(message: String)

Database

Spring Boot auto-initialises the embedded H2 database from data.sql on the classpath (Hibernate creates the schema from the JPA entity):

INSERT INTO message (id, content) VALUES (1, 'Hi via ZIO');
INSERT INTO message (id, content) VALUES (2, 'Hello World');
INSERT INTO message (id, content) VALUES (3, 'I can do ZIO');

Testing

@SpringBootTest
@AutoConfigureMockMvc
class ZioHttpServerTest {

  @Autowired var mvc: MockMvc = _
  @Autowired var mapper: ObjectMapper = _

  @Test
  def getMessage(): Unit = {
    mvc.perform(post("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(mapper.writeValueAsString(ZioRequest(1)))
        .accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk)
      .andExpect(content().json(mapper.writeValueAsString(ZioResponse("Hi via ZIO"))))
  }
}

Next

Return to the tutorials index to explore the full range of OfficeFloor capabilities.