Skip to content

Commit

Permalink
Merge pull request #7 from arrow-kt/sv-small-update
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev authored Dec 15, 2021
2 parents 23325be + 62021f7 commit 86e3bf4
Show file tree
Hide file tree
Showing 9 changed files with 46 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/io/arrow/example/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class ExampleApp(
order.entries.parTraverseValidated {
warehouse.validateAvailability(it.id, it.amount)
}.mapLeft { availability ->
badRequest("Following productIds weren't available: ${availability.joinToString { it.productId }}")
badRequest("Following productIds weren't available: ${availability.joinToString { it.productId.value }}")
}.bind()
}
when (result) {
Expand Down
7 changes: 5 additions & 2 deletions src/main/kotlin/io/arrow/example/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package io.arrow.example

import arrow.optics.optics

@JvmInline
value class ProductId(val value: String)

@optics
data class Entry(val id: String, val amount: Int) {
data class Entry(val id: ProductId, val amount: Int) {
companion object // required by @optics

val asPair: Pair<String, Int>
get() = Pair(id, amount)
get() = Pair(id.value, amount)
}

@optics
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/io/arrow/example/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineScope
fun <A, E> A.ensure(predicate: (A) -> Boolean, problem: () -> E): ValidatedNel<E, A> =
if (predicate(this)) this.validNel() else problem().invalidNel()

// TODO move to Arrow Fx Coroutines
suspend fun <E, A, B> Iterable<A>.parTraverseValidated(
f: suspend CoroutineScope.(A) -> ValidatedNel<E, B>
): ValidatedNel<E, List<B>> =
Expand Down
31 changes: 23 additions & 8 deletions src/main/kotlin/io/arrow/example/external/Billing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package io.arrow.example.external

import arrow.fx.coroutines.CircuitBreaker
import arrow.fx.coroutines.Schedule
import arrow.fx.coroutines.retry
import java.util.concurrent.ScheduledExecutorService
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime

enum class BillingResponse {
OK, USER_ERROR, SYSTEM_ERROR
Expand All @@ -11,14 +15,25 @@ interface Billing {
suspend fun processBilling(order: Map<String, Int>): BillingResponse
}

class BillingWithBreaker(private val underlying: Billing, private val circuitBreaker: CircuitBreaker, private val retries: Int): Billing {
fun Billing.withBreaker(circuitBreaker: CircuitBreaker, retries: Int): Billing =
BillingWithBreaker(this, circuitBreaker, retries)

@OptIn(ExperimentalTime::class)
private class BillingWithBreaker(
private val underlying: Billing,
private val circuitBreaker: CircuitBreaker,
private val retries: Int
) : Billing {
override suspend fun processBilling(order: Map<String, Int>): BillingResponse =
(Schedule.recurs<BillingResponse>(retries) zipRight Schedule.doWhile { it == BillingResponse.SYSTEM_ERROR }).repeat {
circuitBreaker.protectOrThrow {
underlying.processBilling(order)
Schedule.recurs<BillingResponse>(retries)
.zipRight(Schedule.doWhile { it == BillingResponse.SYSTEM_ERROR })
.repeat {
Schedule.recurs<Throwable>(retries)
.and(Schedule.exponential(20.milliseconds))
.retry {
circuitBreaker.protectOrThrow {
underlying.processBilling(order)
}
}
}
}
}

fun Billing.withBreaker(circuitBreaker: CircuitBreaker, retries: Int): Billing =
BillingWithBreaker(this, circuitBreaker, retries)
7 changes: 4 additions & 3 deletions src/main/kotlin/io/arrow/example/external/Warehouse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import arrow.core.ValidatedNel
import arrow.core.invalidNel
import arrow.core.validNel
import io.arrow.example.Entry
import io.arrow.example.ProductId

interface Warehouse {
suspend fun checkAvailability(productId: String, amount: Int): Boolean
suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean
}

data class AvailabilityProblem(val productId: String)
data class AvailabilityProblem(val productId: ProductId)

suspend fun Warehouse.validateAvailability(productId: String, amount: Int)
suspend fun Warehouse.validateAvailability(productId: ProductId, amount: Int)
: ValidatedNel<AvailabilityProblem, Entry> =
if (checkAvailability(productId, amount))
Entry(productId, amount).validNel()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package io.arrow.example.external.impl

import io.arrow.example.ProductId
import io.arrow.example.external.Warehouse
import io.ktor.http.*

class WarehouseImpl(private val serviceUrl: Url): Warehouse {
override suspend fun checkAvailability(productId: String, amount: Int): Boolean {
override suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean {
TODO("Not yet implemented")
}
}
9 changes: 5 additions & 4 deletions src/main/kotlin/io/arrow/example/validation/Structure.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import arrow.core.traverseValidated
import arrow.core.zip
import io.arrow.example.Entry
import io.arrow.example.Order
import io.arrow.example.ProductId
import io.arrow.example.ensure
import io.arrow.example.flatten

Expand All @@ -29,10 +30,10 @@ suspend fun validateStructure(order: Order): ValidatedNel<ValidateStructureProbl
fun validateEntry(entry: Entry): ValidatedNel<ValidateStructureProblem, Entry> =
validateEntryId(entry.id).zip(validateEntryAmount(entry.amount), ::Entry)

fun validateEntryId(id: String): ValidatedNel<ValidateStructureProblem, String> =
eager<Nel<ValidateStructureProblem>, String> {
ensure(id.isNotEmpty()) { ValidateStructureProblem.EMPTY_ID.nel() }
ensure(Regex("^ID-(\\d){4}\$").matches(id)) { ValidateStructureProblem.INCORRECT_ID.nel() }
fun validateEntryId(id: ProductId): ValidatedNel<ValidateStructureProblem, ProductId> =
eager<Nel<ValidateStructureProblem>, ProductId> {
ensure(id.value.isNotEmpty()) { ValidateStructureProblem.EMPTY_ID.nel() }
ensure(Regex("^ID-(\\d){4}\$").matches(id.value)) { ValidateStructureProblem.INCORRECT_ID.nel() }
id
}.toValidated()

Expand Down
6 changes: 3 additions & 3 deletions src/test/kotlin/io/arrow/example/ApplicationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import io.ktor.request.*
import kotlin.test.*
import io.ktor.server.testing.*

class ApplicationTest {
class ApplicationTest() {

// inject fake implementations
private val app =
Expand Down Expand Up @@ -40,15 +40,15 @@ class ApplicationTest {

@Test
fun `wrong id gives error`() = testProcess(
Order(listOf(Entry("NOT-AN-ID", 2)))
Order(listOf(Entry(ProductId("NOT-AN-ID"), 2)))
) {
assertEquals(HttpStatusCode.BadRequest, response.status())
assertEquals("INCORRECT_ID", response.content)
}

@Test
fun `reasonable order`() = testProcess(
Order(listOf(Entry("ID-1234", 2)))
Order(listOf(Entry(ProductId("ID-1234"), 2)))
) {
assertEquals(HttpStatusCode.OK, response.status())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.arrow.example.external.test

import io.arrow.example.ProductId
import io.arrow.example.external.Warehouse

class WarehouseTest(private val maxAmount: Int): Warehouse {
override suspend fun checkAvailability(productId: String, amount: Int): Boolean =
override suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean =
amount < maxAmount
}

0 comments on commit 86e3bf4

Please sign in to comment.