Skip to content

Commit

Permalink
(example): Support getAll and search queries (#45)
Browse files Browse the repository at this point in the history
Co-authored-by: Dragutin Marjanović <[email protected]>
  • Loading branch information
dbulaja98 and drmarjanovic authored Jan 27, 2023
1 parent c0ac7cf commit 5bd757f
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 27 deletions.
2 changes: 1 addition & 1 deletion modules/example/src/main/scala/example/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object Main extends ZIOAppDefault {

val populate: RIO[SttpBackend[Task, Any] with ElasticExecutor, Unit] =
(for {
repositories <- RepoFetcher.fetchAllByOrganization("zio")
repositories <- RepoFetcher.fetchAllByOrganization(organization)
_ <- ZIO.logInfo("Adding GitHub repositories...")
_ <- RepositoriesElasticsearch.createAll(repositories)
} yield ()).provideSome(RepositoriesElasticsearch.live)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
package example

import zio._
import zio.elasticsearch.{DeletionOutcome, DocumentId, ElasticExecutor, ElasticRequest, Routing}
import zio.elasticsearch.ElasticQuery.matchAll
import zio.elasticsearch.{
CreationOutcome,
DeletionOutcome,
DocumentId,
ElasticExecutor,
ElasticQuery,
ElasticRequest,
Routing
}
import zio.prelude.Newtype.unsafeWrap

final case class RepositoriesElasticsearch(executor: ElasticExecutor) {

def findAll(): Task[List[GitHubRepo]] =
executor.execute(ElasticRequest.search[GitHubRepo](Index, matchAll()))

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

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

def createAll(repositories: List[GitHubRepo]): Task[Unit] =
for {
reqs <- ZIO.collectAllPar {
repositories.map { repository =>
routingOf(repository.organization).map(
ElasticRequest
.create[GitHubRepo](Index, unsafeWrap(DocumentId)(repository.id), repository)
.routing(_)
)
}
}
bulkReq = ElasticRequest.bulk(reqs: _*)
routing <- routingOf(organization)
reqs = repositories.map { repository =>
ElasticRequest.create[GitHubRepo](Index, unsafeWrap(DocumentId)(repository.id), repository)
}
bulkReq = ElasticRequest.bulk(reqs: _*).routing(routing)
_ <- executor.execute(bulkReq)
} yield ()

Expand All @@ -49,17 +56,23 @@ final case class RepositoriesElasticsearch(executor: ElasticExecutor) {
res <- executor.execute(req)
} yield res

def search(query: ElasticQuery[_]): Task[List[GitHubRepo]] =
executor.execute(ElasticRequest.search[GitHubRepo](Index, query))

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

}

object RepositoriesElasticsearch {

def findAll(): RIO[RepositoriesElasticsearch, List[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.findAll())

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

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

def createAll(repositories: List[GitHubRepo]): RIO[RepositoriesElasticsearch, Unit] =
Expand All @@ -71,6 +84,9 @@ object RepositoriesElasticsearch {
def remove(organization: String, id: String): RIO[RepositoriesElasticsearch, DeletionOutcome] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.remove(organization, id))

def search(query: ElasticQuery[_]): RIO[RepositoriesElasticsearch, List[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.search(query))

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

import zio.schema.{DeriveSchema, Schema}

import java.time.LocalDateTime

sealed trait Criteria

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

final case class IntCriteria(field: IntFilter, operator: FilterOperator, value: Int) extends Criteria

final case class DateCriteria(field: DateFilter, operator: FilterOperator, value: LocalDateTime) extends Criteria

final case class CompoundCriteria(
operator: CompoundOperator,
filters: List[Criteria]
) extends Criteria

sealed trait FilterOperator

object FilterOperator {
case object GreaterThan extends FilterOperator
case object LessThan extends FilterOperator
case object EqualTo extends FilterOperator
}

sealed trait CompoundOperator

object CompoundOperator {
case object And extends CompoundOperator
case object Or extends CompoundOperator
}

sealed trait IntFilter

object IntFilter {
case object Stars extends IntFilter {
override def toString: String = "stars"
}

case object Forks extends IntFilter {
override def toString: String = "forks"
}
}

sealed trait DateFilter

case object LastCommitAt extends DateFilter {
override def toString: String = "lastCommitAt"
}
60 changes: 52 additions & 8 deletions modules/example/src/main/scala/example/api/Repositories.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ package example.api

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

import CompoundOperator._
import FilterOperator._

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))
RepositoriesElasticsearch.findAll().map(repositories => Response.json(repositories.toJson)).orDie

case Method.GET -> BasePath / organization / id =>
RepositoriesElasticsearch
Expand All @@ -36,12 +40,28 @@ object Repositories {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(repo) =>
RepositoriesElasticsearch.create(repo).map { id =>
Response.json(repo.copy(id = DocumentId.unwrap(id)).toJson).setStatus(Created)
RepositoriesElasticsearch.create(repo).map {
case CreationOutcome.Created =>
Response.json(repo.toJson).setStatus(Created)
case CreationOutcome.AlreadyExists =>
Response.json("A repository with a given ID already exists.").setStatus(BadRequest)
}
}
.orDie

case req @ Method.POST -> BasePath / "search" =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[Criteria](Criteria.schema, _))
.flatMap {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(queryBody) =>
RepositoriesElasticsearch
.search(createElasticQuery(queryBody))
.map(repositories => Response.json(repositories.toJson))
}
.orDie

case req @ Method.PUT -> BasePath / id =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[GitHubRepo](GitHubRepo.schema, _))
Expand All @@ -58,10 +78,7 @@ object Repositories {
)
case Right(repo) =>
(RepositoriesElasticsearch
.upsert(id, repo.copy(id = id)) *> RepositoriesElasticsearch.findById(
repo.organization,
id
)).map {
.upsert(id, repo.copy(id = 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)
}
Expand All @@ -80,4 +97,31 @@ object Repositories {
.orDie
}

private def createElasticQuery(query: Criteria): ElasticQuery[_] =
query match {
case IntCriteria(field, operator, value) =>
operator match {
case GreaterThan =>
ElasticQuery.range(field.toString).gt(value)
case LessThan =>
ElasticQuery.range(field.toString).lt(value)
case EqualTo =>
ElasticQuery.matches(field.toString, value)
}
case DateCriteria(field, operator, value) =>
operator match {
case GreaterThan =>
ElasticQuery.range(field.toString).gt(value.toString)
case LessThan =>
ElasticQuery.range(field.toString).lt(value.toString)
case EqualTo =>
ElasticQuery.matches(field.toString, value.toString)
}
case CompoundCriteria(operator, filters) =>
operator match {
case And => boolQuery().must(filters.map(createElasticQuery): _*)
case Or => boolQuery().should(filters.map(createElasticQuery): _*)
}
}

}
3 changes: 2 additions & 1 deletion modules/example/src/main/scala/example/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import zio.elasticsearch.IndexName

package object example {
val Index: IndexName = IndexName("repositories")
final val Index: IndexName = IndexName("repositories")
final val organization: String = "zio"
}
51 changes: 48 additions & 3 deletions modules/example/zio-elasticsearch-example.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"scrul-detector\",\n \"url\": \"https://github.com/lambdaworks/scurl-detector\",\n \"description\": \"Scala library that detects and extracts URLs from text.\",\n \"lastCommitAt\": \"2022-12-01T14:27:11.436\",\n \"stars\": 14,\n \"forks\": 1\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"scrul-detector\",\n \"url\": \"https://github.com/lambdaworks/scurl-detector\",\n \"description\": \"Scala library that detects and extracts URLs from text.\",\n \"lastCommitAt\": \"2022-12-01T14:27:11.436\",\n \"stars\": 14,\n \"forks\": 1\n}",
"options": {
"raw": {
"language": "json"
Expand Down Expand Up @@ -142,7 +142,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"scountries\",\n \"url\": \"https://github.com/lambdaworks/scountries\",\n \"description\": \"Scala library that provides an enumeration of ISO 3166 codes for countries, along with their subdivisions.\",\n \"lastCommitAt\": \"2022-12-08T19:10:46.016\",\n \"stars\": 16,\n \"forks\": 1\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"scountries\",\n \"url\": \"https://github.com/lambdaworks/scountries\",\n \"description\": \"Scala library that provides an enumeration of ISO 3166 codes for countries, along with their subdivisions.\",\n \"lastCommitAt\": \"2022-12-08T19:10:46.016\",\n \"stars\": 16,\n \"forks\": 1\n}",
"options": {
"raw": {
"language": "json"
Expand All @@ -164,6 +164,26 @@
},
"response": []
},
{
"name": "Retrieving all repositories",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:{{HTTP_PORT}}/api/repositories",
"protocol": "http",
"host": [
"localhost"
],
"port": "{{HTTP_PORT}}",
"path": [
"api",
"repositories"
]
}
},
"response": []
},
{
"name": "Retrieving an existing repository",
"request": {
Expand Down Expand Up @@ -281,7 +301,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"zio-elasticsearch\",\n \"url\": \"https://github.com/lambdaworks/zio-elasticsearch\",\n \"description\": \"ZIO Elasticsearch is a type-safe, testable and streaming-friendly ZIO native Elasticsearch client.\",\n \"lastCommitAt\": \"2022-12-27T15:58:30.996\",\n \"stars\": 21,\n \"forks\": 5\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"zio-elasticsearch\",\n \"url\": \"https://github.com/lambdaworks/zio-elasticsearch\",\n \"description\": \"ZIO Elasticsearch is a type-safe, testable and streaming-friendly ZIO native Elasticsearch client.\",\n \"lastCommitAt\": \"2022-12-27T15:58:30.996\",\n \"stars\": 21,\n \"forks\": 5\n}",
"options": {
"raw": {
"language": "json"
Expand Down Expand Up @@ -363,6 +383,31 @@
}
},
"response": []
},
{
"name": "Search for repositories with 4 forks and with latest commit after 2019-12-04",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"CompoundCriteria\": {\n \"operator\": {\"And\": {}},\n \"filters\": [\n {\n \"IntCriteria\": {\n \"field\": {\"Forks\": {}},\n \"operator\": {\"EqualTo\": {}},\n \"value\": 4\n }\n },\n {\n \"DateCriteria\": {\n \"field\": {\"LastCommitAt\": {}},\n \"operator\": {\"GreaterThan\": {}},\n \"value\": \"2019-12-04T18:38:04\"\n }\n }\n ]\n }\n}\n"
},
"url": {
"raw": "http://localhost:{{HTTP_PORT}}/api/repositories/search",
"protocol": "http",
"host": [
"localhost"
],
"port": "{{HTTP_PORT}}",
"path": [
"api",
"repositories",
"search"
]
}
},
"response": []
}
],
"event": [
Expand Down

0 comments on commit 5bd757f

Please sign in to comment.