Skip to content

Commit

Permalink
(example): Provide example application (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
drmarjanovic authored Dec 23, 2022
1 parent d791df6 commit bea87be
Show file tree
Hide file tree
Showing 16 changed files with 778 additions and 6 deletions.
20 changes: 15 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,19 @@ lazy val root =
lazy val library =
project
.in(file("modules/library"))
.disablePlugins(RevolverPlugin)
.settings(stdSettings("zio-elasticsearch"))
.settings(scalacOptions += "-language:higherKinds")
.configs(IntegrationTest)
.settings(
Defaults.itSettings,
libraryDependencies ++= List(
"com.softwaremill.sttp.client3" %% "zio" % "3.8.3",
"com.softwaremill.sttp.client3" %% "zio-json" % "3.8.3",
"com.softwaremill.sttp.client3" %% "zio" % "3.8.5",
"com.softwaremill.sttp.client3" %% "zio-json" % "3.8.5",
"dev.zio" %% "zio-json" % "0.3.0",
"dev.zio" %% "zio-prelude" % "1.0.0-RC16",
"dev.zio" %% "zio-schema" % "0.3.1",
"dev.zio" %% "zio-schema-json" % "0.3.1",
"dev.zio" %% "zio-schema" % "0.4.1",
"dev.zio" %% "zio-schema-json" % "0.4.1",
"org.apache.commons" % "commons-lang3" % "3.12.0",
"dev.zio" %% "zio-test" % "2.0.5" % Tests,
"dev.zio" %% "zio-test-sbt" % "2.0.5" % Tests
Expand All @@ -51,9 +52,17 @@ lazy val example =
project
.in(file("modules/example"))
.settings(stdSettings("example"))
.settings(scalacOptions += "-language:higherKinds")
.settings(
libraryDependencies ++= List(
"dev.zio" %% "zio" % "2.0.5"
"dev.zio" %% "zio" % "2.0.5",
"dev.zio" %% "zio-config" % "3.0.6",
"dev.zio" %% "zio-config-magnolia" % "3.0.6",
"dev.zio" %% "zio-config-typesafe" % "3.0.6",
"dev.zio" %% "zio-http" % "0.0.3",
"dev.zio" %% "zio-json" % "0.3.0",
"dev.zio" %% "zio-schema" % "0.4.1",
"dev.zio" %% "zio-schema-json" % "0.4.1"
)
)
.dependsOn(library)
Expand All @@ -65,6 +74,7 @@ lazy val docs =
project
.in(file("modules/docs"))
.enablePlugins(MdocPlugin, DocusaurusPlugin, ScalaUnidocPlugin)
.disablePlugins(RevolverPlugin)
.dependsOn(library)
.settings(
publish / skip := true,
Expand Down
22 changes: 22 additions & 0 deletions modules/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ZIO Elasticsearch Example Application

This application represents an example of usage `zio-elasticsearch` library for **Elasticsearch 7.x**.

### Running

- Run the Elasticsearch 7.x service (one can be found as part of the `docker-compose.yml` file in the root of this
repository)
- Start the application by running the following command:
```shell
./sbt "~example/reStart"
```
- Check whether the application is running [here](http://localhost:9000/health)
- Explore endpoints using Postman collection (`zio-elasticsearch-example.postman_collection.json`)

### Description

On the application startup - a **"repositories"** index will be deleted, and immediately re-created with the mapping
definition given in the `resources/mapping.json` file.

After successfully starting the application, you can test the exposed ZIO Elasticsearch library's API through exposed HTTP
endpoints.
8 changes: 8 additions & 0 deletions modules/example/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
http {
port = 9000
}

elasticsearch {
host = "localhost"
port = 9200
}
38 changes: 38 additions & 0 deletions modules/example/src/main/resources/mapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"settings": {
"index": {
"number_of_shards": 1
}
},
"mappings": {
"_routing": {
"required": true
},
"properties": {
"id": {
"type": "keyword"
},
"organization": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"url": {
"type": "keyword"
},
"description": {
"type": "text"
},
"lastCommitAt": {
"type": "date"
},
"stars": {
"type": "integer"
},
"forks": {
"type": "integer"
}
}
}
}
23 changes: 23 additions & 0 deletions modules/example/src/main/scala/example/GitHubRepo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package example

import zio.json.{DeriveJsonEncoder, JsonEncoder}
import zio.schema.{DeriveSchema, Schema}

import java.time.LocalDateTime

final case class GitHubRepo(
id: Option[String],
organization: String,
name: String,
url: String,
description: Option[String],
lastCommitAt: LocalDateTime,
stars: Int,
forks: Int
)

object GitHubRepo {
implicit val schema: Schema[GitHubRepo] = DeriveSchema.gen[GitHubRepo]

implicit val encoder: JsonEncoder[GitHubRepo] = DeriveJsonEncoder.gen[GitHubRepo]
}
58 changes: 58 additions & 0 deletions modules/example/src/main/scala/example/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package example

import example.api.{HealthCheck, Repositories}
import example.config.{AppConfig, ElasticsearchConfig, HttpConfig}
import sttp.client3.httpclient.zio.HttpClientZioBackend
import zio._
import zio.config.getConfig
import zio.elasticsearch.{ElasticConfig, ElasticExecutor, ElasticRequest}
import zio.http.{Server, ServerConfig}

import scala.io.Source

object Main extends ZIOAppDefault {

override def run: Task[ExitCode] = {
val elasticConfigLive = ZLayer(getConfig[ElasticsearchConfig].map(es => ElasticConfig(es.host, es.port)))

(prepare *> runServer).provide(
AppConfig.live,
elasticConfigLive,
ElasticExecutor.live,
HttpClientZioBackend.layer()
)
}

private[this] def prepare: RIO[ElasticExecutor, Unit] = {
val deleteIndex: RIO[ElasticExecutor, Unit] =
for {
_ <- ZIO.logInfo(s"Deleting index '$Index'...")
_ <- ElasticRequest.deleteIndex(Index).execute
} yield ()

val createIndex: RIO[ElasticExecutor, Unit] =
for {
_ <- ZIO.logInfo(s"Creating index '$Index'...")
mapping <- ZIO.attempt(Source.fromURL(getClass.getResource("/mapping.json")).mkString)
_ <- ElasticRequest.createIndex(Index, Some(mapping)).execute
} yield ()

deleteIndex *> createIndex
}

private[this] def runServer: RIO[HttpConfig with ElasticExecutor, ExitCode] = {
val serverConfigLive = ZLayer(getConfig[HttpConfig].map(http => ServerConfig.default.port(http.port)))

(for {
http <- getConfig[HttpConfig]
_ <- ZIO.logInfo(s"Starting an HTTP service on port: ${http.port}")
routes = HealthCheck.Route ++ Repositories.Routes
_ <- Server.serve(routes)
} yield ExitCode.success).provideSome(
RepositoriesElasticsearch.live,
Server.live,
serverConfigLive
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package example

import zio._
import zio.elasticsearch.ElasticError.DocumentRetrievingError.DocumentNotFound
import zio.elasticsearch.{DocumentId, ElasticExecutor, ElasticRequest, Routing}

final case class RepositoriesElasticsearch(executor: ElasticExecutor) {

def findById(organization: String, id: String): Task[Option[GitHubRepo]] =
for {
routing <- routingOf(organization)
req = ElasticRequest.getById[GitHubRepo](Index, DocumentId(id)).routing(routing)
res <- executor.execute(req)
} yield res.toOption

def create(repository: GitHubRepo): Task[Option[DocumentId]] =
for {
routing <- routingOf(repository.organization)
req = ElasticRequest.create(Index, repository).routing(routing)
res <- executor.execute(req)
} yield res

def upsert(id: String, repository: GitHubRepo): Task[Unit] =
for {
routing <- routingOf(repository.organization)
req = ElasticRequest.upsert(Index, DocumentId(id), repository).routing(routing)
_ <- executor.execute(req)
} yield ()

def remove(organization: String, id: String): Task[Either[DocumentNotFound.type, Unit]] =
for {
routing <- routingOf(organization)
req = ElasticRequest.deleteById(Index, DocumentId(id)).routing(routing)
res <- executor.execute(req)
} yield res

private def routingOf(value: String): IO[IllegalArgumentException, Routing.Type] =
Routing.make(value).toZIO.mapError(e => new IllegalArgumentException(e))

}

object RepositoriesElasticsearch {

def findById(organization: String, id: String): RIO[RepositoriesElasticsearch, Option[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.findById(organization, id))

def create(repository: GitHubRepo): RIO[RepositoriesElasticsearch, Option[DocumentId]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.create(repository))

def upsert(id: String, repository: GitHubRepo): RIO[RepositoriesElasticsearch, Unit] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.upsert(id, repository))

def remove(organization: String, id: String): RIO[RepositoriesElasticsearch, Either[DocumentNotFound.type, Unit]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.remove(organization, id))

lazy val live: URLayer[ElasticExecutor, RepositoriesElasticsearch] =
ZLayer.fromFunction(RepositoriesElasticsearch(_))
}
13 changes: 13 additions & 0 deletions modules/example/src/main/scala/example/api/HealthCheck.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package example.api

import zio.http._
import zio.http.model.Method
import zio.json.ast.Json._

object HealthCheck {

final val Route: Http[Any, Nothing, Any, Response] = Http.collect { case Method.GET -> !! / "health" =>
Response.json(Obj("name" -> Str("zio-elasticsearch-example"), "status" -> Str("up")).toJson)
}

}
86 changes: 86 additions & 0 deletions modules/example/src/main/scala/example/api/Repositories.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package example.api

import example.{GitHubRepo, RepositoriesElasticsearch}
import zio.ZIO
import zio.elasticsearch.DocumentId
import zio.http._
import zio.http.model.Method
import zio.http.model.Status._
import zio.json.EncoderOps
import zio.schema.codec.JsonCodec

object Repositories {

private final val BasePath = !! / "api" / "repositories"

final val Routes: Http[RepositoriesElasticsearch, Nothing, Request, Response] =
Http.collectZIO[Request] {
case Method.GET -> BasePath =>
ZIO.succeed(Response.text("TODO: Get a list of repositories").setStatus(NotImplemented))

case Method.GET -> BasePath / organization / id =>
RepositoriesElasticsearch
.findById(organization, id)
.map {
case Some(r) =>
Response.json(r.toJson)
case None =>
Response.json(ErrorResponse.fromReasons(s"Repository $id does not exist.").toJson).setStatus(NotFound)
}
.orDie

case req @ Method.POST -> BasePath =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[GitHubRepo](GitHubRepo.schema, _))
.flatMap {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(repo) =>
RepositoriesElasticsearch.create(repo).map {
case Some(id) =>
Response.json(repo.copy(id = Some(DocumentId.unwrap(id))).toJson).setStatus(Created)
case None =>
Response.json(ErrorResponse.fromReasons("Failed to create repository.").toJson).setStatus(BadRequest)
}
}
.orDie

case req @ Method.PUT -> BasePath / id =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[GitHubRepo](GitHubRepo.schema, _))
.flatMap {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(repo) if repo.id.exists(_ != id) =>
ZIO.succeed(
Response
.json(
ErrorResponse.fromReasons("The ID provided in the path does not match the ID from the body.").toJson
)
.setStatus(BadRequest)
)
case Right(repo) =>
(RepositoriesElasticsearch
.upsert(id, repo.copy(id = Some(id))) *> RepositoriesElasticsearch.findById(
repo.organization,
id
)).map {
case Some(updated) => Response.json(updated.toJson)
case None => Response.json(ErrorResponse.fromReasons("Operation failed.").toJson).setStatus(BadRequest)
}
}
.orDie

case Method.DELETE -> BasePath / organization / id =>
RepositoriesElasticsearch
.remove(organization, id)
.map {
case Right(_) =>
Response.status(NoContent)
case Left(_) =>
Response.json(ErrorResponse.fromReasons(s"Repository $id does not exist.").toJson).setStatus(NotFound)
}
.orDie
}

}
23 changes: 23 additions & 0 deletions modules/example/src/main/scala/example/api/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package example

import zio.Chunk
import zio.json._

package object api {

final case class ErrorResponseData(body: Chunk[String])

object ErrorResponseData {
implicit val encoder: JsonEncoder[ErrorResponseData] = DeriveJsonEncoder.gen[ErrorResponseData]
}

final case class ErrorResponse(errors: ErrorResponseData)

object ErrorResponse {
implicit val encoder: JsonEncoder[ErrorResponse] = DeriveJsonEncoder.gen[ErrorResponse]

def fromReasons(reasons: String*): ErrorResponse =
new ErrorResponse(ErrorResponseData(Chunk.fromIterable(reasons)))
}

}
Loading

0 comments on commit bea87be

Please sign in to comment.