-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(example): Provide example application (#27)
- Loading branch information
1 parent
d791df6
commit bea87be
Showing
16 changed files
with
778 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
http { | ||
port = 9000 | ||
} | ||
|
||
elasticsearch { | ||
host = "localhost" | ||
port = 9200 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
|
||
} |
58 changes: 58 additions & 0 deletions
58
modules/example/src/main/scala/example/RepositoriesElasticsearch.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
modules/example/src/main/scala/example/api/HealthCheck.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
modules/example/src/main/scala/example/api/Repositories.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} | ||
|
||
} |
Oops, something went wrong.