Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(api): Restructure errors #36

Merged
merged 9 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package example

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

final case class RepositoriesElasticsearch(executor: ElasticExecutor) {

Expand All @@ -11,9 +10,9 @@ final case class RepositoriesElasticsearch(executor: ElasticExecutor) {
routing <- routingOf(organization)
req = ElasticRequest.getById[GitHubRepo](Index, DocumentId(id)).routing(routing)
res <- executor.execute(req)
} yield res.toOption
} yield res

def create(repository: GitHubRepo): Task[Option[DocumentId]] =
def create(repository: GitHubRepo): Task[DocumentId] =
for {
routing <- routingOf(repository.organization)
req = ElasticRequest.create(Index, repository).routing(routing).refreshTrue
Expand All @@ -27,7 +26,7 @@ final case class RepositoriesElasticsearch(executor: ElasticExecutor) {
_ <- executor.execute(req)
} yield ()

def remove(organization: String, id: String): Task[Either[DocumentNotFound.type, Unit]] =
def remove(organization: String, id: String): Task[DeletionOutcome] =
for {
routing <- routingOf(organization)
req = ElasticRequest.deleteById(Index, DocumentId(id)).routing(routing).refreshFalse
Expand All @@ -44,13 +43,13 @@ 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]] =
def create(repository: GitHubRepo): RIO[RepositoriesElasticsearch, 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]] =
def remove(organization: String, id: String): RIO[RepositoriesElasticsearch, DeletionOutcome] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.remove(organization, id))

lazy val live: URLayer[ElasticExecutor, RepositoriesElasticsearch] =
Expand Down
13 changes: 5 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,7 +2,7 @@ package example.api

import example.{GitHubRepo, RepositoriesElasticsearch}
import zio.ZIO
import zio.elasticsearch.DocumentId
import zio.elasticsearch.{DeletionOutcome, DocumentId}
import zio.http._
import zio.http.model.Method
import zio.http.model.Status._
Expand Down Expand Up @@ -36,11 +36,8 @@ object Repositories {
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)
RepositoriesElasticsearch.create(repo).map { id =>
Response.json(repo.copy(id = Some(DocumentId.unwrap(id))).toJson).setStatus(Created)
}
}
.orDie
Expand Down Expand Up @@ -75,9 +72,9 @@ object Repositories {
RepositoriesElasticsearch
.remove(organization, id)
.map {
case Right(_) =>
case DeletionOutcome.Deleted =>
Response.status(NoContent)
case Left(_) =>
case DeletionOutcome.NotFound =>
Response.json(ErrorResponse.fromReasons(s"Repository $id does not exist.").toJson).setStatus(NotFound)
}
.orDie
Expand Down
167 changes: 78 additions & 89 deletions modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala
Original file line number Diff line number Diff line change
@@ -1,78 +1,56 @@
package zio.elasticsearch

import sttp.client3.httpclient.zio.HttpClientZioBackend
import sttp.client3.{SttpBackend, basicRequest}
import sttp.model.StatusCode.Ok
import zio.elasticsearch.ElasticConfig.Default
import zio.{Task, ZIO}
import zio.elasticsearch.ElasticError.DocumentRetrievingError.{DecoderError, DocumentNotFound}
import zio.test.Assertion.{equalTo, isFalse, isLeft, isRight, isTrue, isUnit}
import zio.elasticsearch.CreationOutcome.{AlreadyExists, Created}
import zio.elasticsearch.DeletionOutcome.{Deleted, NotFound}
import zio.test.Assertion._
import zio.test.TestAspect.nondeterministic
import zio.test._

object HttpExecutorSpec extends IntegrationSpec {

override def spec: Spec[TestEnvironment, Any] =
suite("HTTP Executor")(
suite("retrieving document by ID")(
test("successfully return document") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
val result = for {
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
document <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield document

assertZIO(result)(isRight(equalTo(customer)))
}
},
test("return DocumentNotFound if the document does not exist") {
checkOnce(genDocumentId) { documentId =>
assertZIO(ElasticRequest.getById[CustomerDocument](index, documentId).execute)(
isLeft(equalTo(DocumentNotFound))
)
}
},
test("fail with decoding error") {
checkOnce(genDocumentId, genEmployee) { (documentId, employee) =>
val result = for {
_ <- ElasticRequest.upsert[EmployeeDocument](index, documentId, employee).execute
document <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield document

assertZIO(result)(isLeft(equalTo(DecoderError(".address(missing)"))))
}
}
),
suite("creating document")(
test("successfully create document") {
checkOnce(genCustomer) { customer =>
val result = for {
docId <- ElasticRequest.create[CustomerDocument](index, customer).execute
res <- ElasticRequest.getById[CustomerDocument](index, docId.getOrElse(DocumentId(""))).execute
res <- ElasticRequest.getById[CustomerDocument](index, docId).execute
} yield res

assertZIO(result)(isRight(equalTo(customer)))
assertZIO(result)(isSome(equalTo(customer)))
}
},
test("successfully create document with ID given") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
val result = for {
_ <- ElasticRequest.create[CustomerDocument](index, documentId, customer).execute
doc <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield doc

assertZIO(result)(isRight(equalTo(customer)))
assertZIO(ElasticRequest.create[CustomerDocument](index, documentId, customer).execute)(equalTo(Created))
}
},
test("fail to create document with ID given") {
test("return 'AlreadyExists' if document with given ID already exists") {
checkOnce(genDocumentId, genCustomer, genCustomer) { (documentId, customer1, customer2) =>
val result = for {
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer1).execute
_ <- ElasticRequest.create[CustomerDocument](index, documentId, customer2).execute
doc <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield doc
res <- ElasticRequest.create[CustomerDocument](index, documentId, customer2).execute
} yield res

assertZIO(result)(equalTo(AlreadyExists))
}
}
),
suite("creating index")(
test("successfully create index") {
checkOnce(genIndexName) { name =>
assertZIO(ElasticRequest.createIndex(name, None).execute)(equalTo(Created))
}
},
test("return 'AlreadyExists' if index already exists") {
checkOnce(genIndexName) { name =>
val result = for {
_ <- ElasticRequest.createIndex(name, None).execute
res <- ElasticRequest.createIndex(name, None).execute
} yield res

assertZIO(result)(isRight(equalTo(customer1)))
assertZIO(result)(equalTo(AlreadyExists))
}
}
),
Expand All @@ -84,7 +62,7 @@ object HttpExecutorSpec extends IntegrationSpec {
doc <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield doc

assertZIO(result)(isRight(equalTo(customer)))
assertZIO(result)(isSome(equalTo(customer)))
}
},
test("successfully update document") {
Expand All @@ -95,76 +73,87 @@ object HttpExecutorSpec extends IntegrationSpec {
doc <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield doc

assertZIO(result)(isRight(equalTo(customer2)))
assertZIO(result)(isSome(equalTo(customer2)))
}
}
),
suite("finding document")(
test("return true if the document exists") {
suite("deleting document by ID")(
test("successfully delete existing document") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
val result = for {
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
exists <- ElasticRequest.exists(index, documentId).execute
} yield exists
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
res <- ElasticRequest.deleteById(index, documentId).execute
} yield res

assertZIO(result)(isTrue)
assertZIO(result)(equalTo(Deleted))
}
},
test("return false if the document does not exist") {
test("return 'NotFound' if the document does not exist") {
checkOnce(genDocumentId) { documentId =>
assertZIO(ElasticRequest.exists(index, documentId).execute)(isFalse)
assertZIO(ElasticRequest.deleteById(index, documentId).execute)(equalTo(NotFound))
}
}
),
suite("deleting document by ID")(
test("return unit if document deletion was successful") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
suite("delete index")(
test("successfully delete existing index") {
checkOnce(genIndexName) { name =>
val result = for {
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
res <- ElasticRequest.deleteById(index, documentId).execute
_ <- ElasticRequest.createIndex(name, None).execute
res <- ElasticRequest.deleteIndex(name).execute
} yield res

assertZIO(result)(isRight(isUnit))
assertZIO(result)(equalTo(Deleted))
}
},
test("return DocumentNotFound if the document does not exist") {
checkOnce(genDocumentId) { documentId =>
assertZIO(ElasticRequest.deleteById(index, documentId).execute)(isLeft(equalTo(DocumentNotFound)))
test("return 'NotFound' if index does not exists") {
checkOnce(genIndexName) { name =>
assertZIO(ElasticRequest.deleteIndex(name).execute)(equalTo(NotFound))
}
}
),
suite("creating index")(
test("return true if creation was successful") {
checkOnce(genIndexName) { name =>
suite("finding document")(
test("return true if the document exists") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
val result = for {
_ <- ElasticRequest.createIndex(name, None).execute
sttp <- ZIO.service[SttpBackend[Task, Any]]
indexExists <- basicRequest
.head(Default.uri.withPath(name.toString))
.send(sttp)
.map(_.code.equals(Ok))
} yield indexExists
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
res <- ElasticRequest.exists(index, documentId).execute
} yield res

assertZIO(result)(isTrue)
}
},
test("return false if the document does not exist") {
checkOnce(genDocumentId) { documentId =>
assertZIO(ElasticRequest.exists(index, documentId).execute)(isFalse)
}
}
),
suite("delete index")(
test("return true if deletion was successful") {
checkOnce(genIndexName) { name =>
suite("retrieving document by ID")(
test("successfully return document") {
checkOnce(genDocumentId, genCustomer) { (documentId, customer) =>
val result = for {
_ <- ElasticRequest.createIndex(name, None).execute
deleted <- ElasticRequest.deleteIndex(name).execute
} yield deleted
_ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute
res <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield res

assertZIO(result)(isTrue)
assertZIO(result)(isSome(equalTo(customer)))
}
},
test("return false if deletion was not successful") {
checkOnce(genIndexName) { name =>
assertZIO(ElasticRequest.deleteIndex(name).execute)(isFalse)
test("return None if the document does not exist") {
checkOnce(genDocumentId) { documentId =>
assertZIO(ElasticRequest.getById[CustomerDocument](index, documentId).execute)(isNone)
}
},
test("fail with throwable if decoding fails") {
checkOnce(genDocumentId, genEmployee) { (documentId, employee) =>
val result = for {
_ <- ElasticRequest.upsert[EmployeeDocument](index, documentId, employee).execute
res <- ElasticRequest.getById[CustomerDocument](index, documentId).execute
} yield res

assertZIO(result.exit)(fails(isSubtype[Exception](assertException("Decoding error: .address(missing)"))))
}
}
)
).provideShared(elasticsearchLayer, HttpClientZioBackend.layer()) @@ nondeterministic
).provideShared(elasticsearchLayer) @@ nondeterministic
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package zio.elasticsearch
import sttp.client3.httpclient.zio.HttpClientZioBackend
import zio.ZLayer
import zio.prelude.Newtype.unsafeWrap
import zio.test.Assertion.{containsString, hasMessage}
import zio.test.CheckVariants.CheckN
import zio.test.{Gen, ZIOSpecDefault, checkN}
import zio.test.{Assertion, Gen, ZIOSpecDefault, checkN}

trait IntegrationSpec extends ZIOSpecDefault {
val elasticsearchLayer: ZLayer[Any, Throwable, ElasticExecutor] =
Expand All @@ -31,4 +32,6 @@ trait IntegrationSpec extends ZIOSpecDefault {
} yield EmployeeDocument(id = id, name = name, degree = degree)

def checkOnce: CheckN = checkN(1)

def assertException(substring: String): Assertion[Throwable] = hasMessage(containsString(substring))
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import zio.json.ast.Json
import zio.json.{DeriveJsonDecoder, JsonDecoder, jsonField}

private[elasticsearch] final case class ElasticGetResponse(
found: Boolean,
@jsonField("_source")
source: Json
)
Expand Down
Loading