Skip to content
Open
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
17 changes: 6 additions & 11 deletions src/main/kotlin/cli/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,25 @@ import arrow.core.left
import arrow.core.right
import cli.readInput
import config.appConfig
import domain.AllowedSenders
import domain.ApplicationErrors
import domain.CreateValidatedEmailRoute
import domain.EmailRoute
import domain.InterruptedError
import domain.ReceiveEmailConsents

suspend fun main() = either<ApplicationErrors, Unit> {
val (allowedSenders, receiverEmailConsent) = appConfig().bind()

with(allowedSenders) {
with(receiverEmailConsent) {
while (true) {
runProgram().bind()
}
}
val createValidatedEmailRoute = EmailRoute.factoryWithContext(allowedSenders, receiverEmailConsent)
while (true) {
runProgram(createValidatedEmailRoute).bind()
}
}.getOrHandle { errors ->
errors.log()
}

context(AllowedSenders, ReceiveEmailConsents)
private suspend fun runProgram() = either<ApplicationErrors, Unit> {
private suspend fun runProgram(createValidatedEmailRoute: CreateValidatedEmailRoute) = either<ApplicationErrors, Unit> {
val (from, to, cc, bcc) = readInput().leftNel().bind()
val emailRoute = EmailRoute.validated(from, to, cc, bcc).bind()
val emailRoute = createValidatedEmailRoute(from, to, cc, bcc).bind()

println("Sending email to $emailRoute")
}.handleErrorWith { errors ->
Expand Down
45 changes: 22 additions & 23 deletions src/main/kotlin/domain/EmailRoute.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,35 @@ import arrow.core.traverseValidated
import arrow.core.valid
import validate

typealias CreateValidatedEmailRoute = (String, String, List<String?>, List<String?>) -> Validated<ValidationErrors, EmailRoute>

data class EmailRoute private constructor(
val from: Email,
val to: Email,
val cc: List<Email> = emptyList(),
val bcc: List<Email> = emptyList()
) {
companion object {
context(AllowedSenders, ReceiveEmailConsents)
fun validated(
from: String,
to: String,
cc: List<String> = emptyList(),
bcc: List<String> = emptyList()
): Validated<ValidationErrors, EmailRoute> =
validate(
Email.valueOf(from),
Email.valueOf(to),
cc.traverseValidated { Email.valueOf(it) },
bcc.traverseValidated { Email.valueOf(it) }
) { validFrom, validTo, validCc, validBcc ->
EmailRoute(validFrom, validTo, validCc, validBcc)
}.andThen { emailRoute ->
when {
emailRoute.cc.isEmpty() && emailRoute.bcc.isEmpty() ->
ValidationError("Both cc and bcc are empty").invalidNel()
emailRoute.from !in this@AllowedSenders ->
ValidationError("'${emailRoute.from}' is not in the list of allowed senders").invalidNel()
emailRoute.to !in this@ReceiveEmailConsents ->
ValidationError("'${emailRoute.to}' does not consent receiving emails").invalidNel()
else -> emailRoute.valid()
fun factoryWithContext(allowedSenders: AllowedSenders, receiveEmailConsents: ReceiveEmailConsents):
CreateValidatedEmailRoute =
{ from, to, cc, bcc ->
validate(
Email.valueOf(from),
Email.valueOf(to),
cc.traverseValidated { Email.valueOf(it) },
bcc.traverseValidated { Email.valueOf(it) }
) { validFrom, validTo, validCc, validBcc ->
EmailRoute(validFrom, validTo, validCc, validBcc)
}.andThen { emailRoute ->
when {
emailRoute.cc.isEmpty() && emailRoute.bcc.isEmpty() ->
ValidationError("Both cc and bcc are empty").invalidNel()
emailRoute.from !in allowedSenders ->
ValidationError("'${emailRoute.from.value}' is not in the list of allowed senders").invalidNel()
emailRoute.to !in receiveEmailConsents ->
ValidationError("'${emailRoute.to.value}' does not consent receiving emails").invalidNel()
else -> emailRoute.valid()
}
}
}
}
Expand Down
123 changes: 62 additions & 61 deletions src/test/kotlin/domain/EmailRouteSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,78 +14,79 @@ class EmailRouteSpec : FreeSpec({
val allReceiversConsent = ReceiveEmailConsents { true }
val noReceiversConsent = ReceiveEmailConsents { false }

with(allSendersAllowed) {
with(allReceiversConsent) {
"should validate when all validation rules pass" {
val validatedEmailRoute = EmailRoute.validated(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost")
)

with(validatedEmailRoute.shouldBeValid()) {
from shouldBe validEmail("sender@localhost")
to shouldBe validEmail("receiver@localhost")
cc shouldBe listOf(validEmail("cc@localhost"))
bcc shouldBe emptyList()
}
}
"given all senders allowed and all receivers consent" - {
val createValidatedEmailRoute = EmailRoute.factoryWithContext(allSendersAllowed, allReceiversConsent)

"should invalidate both empty cc and bcc" {
val emailRoute = EmailRoute.validated(
"sender@localhost",
"receiver@localhost"
)
"should validate when all validation rules pass" {
val validatedEmailRoute = createValidatedEmailRoute(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost"),
emptyList()
)

emailRoute shouldBe ValidationError("Both cc and bcc are empty").invalidNel()
with(validatedEmailRoute.shouldBeValid()) {
from shouldBe validEmail("sender@localhost")
to shouldBe validEmail("receiver@localhost")
cc shouldBe listOf(validEmail("cc@localhost"))
bcc shouldBe emptyList()
}
}

"should accumulate errors" {
val emailRoute = EmailRoute.validated(
"invalid-from",
"invalid-to",
listOf("invalid-cc"),
listOf("invalid-bcc")
)

emailRoute shouldBe nonEmptyListOf(
ValidationError("'invalid-from' should be a valid email address"),
ValidationError("'invalid-to' should be a valid email address"),
ValidationError("'invalid-cc' should be a valid email address"),
ValidationError("'invalid-bcc' should be a valid email address")
).invalid()
}
"should invalidate both empty cc and bcc" {
val emailRoute = createValidatedEmailRoute(
"sender@localhost",
"receiver@localhost",
emptyList(),
emptyList()
)

emailRoute shouldBe ValidationError("Both cc and bcc are empty").invalidNel()
}

"should accumulate errors" {
val emailRoute = createValidatedEmailRoute(
"invalid-from",
"invalid-to",
listOf("invalid-cc"),
listOf("invalid-bcc")
)

emailRoute shouldBe nonEmptyListOf(
ValidationError("'invalid-from' should be a valid email address"),
ValidationError("'invalid-to' should be a valid email address"),
ValidationError("'invalid-cc' should be a valid email address"),
ValidationError("'invalid-bcc' should be a valid email address")
).invalid()
}
}

"should invalidate when the sender is not allowed" {
with(noSendersAllowed) {
with(allReceiversConsent) {
val validatedEmailRoute = EmailRoute.validated(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost")
)

validatedEmailRoute shouldBe
ValidationError("'sender@localhost' is not in the list of allowed senders").invalidNel()
}
}
val createValidatedEmailRoute = EmailRoute.factoryWithContext(noSendersAllowed, allReceiversConsent)

val validatedEmailRoute = createValidatedEmailRoute(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost"),
emptyList()
)

validatedEmailRoute shouldBe
ValidationError("'sender@localhost' is not in the list of allowed senders").invalidNel()
}

"should invalidate when the receiver does not consent receiving emails" {
with(allSendersAllowed) {
with(noReceiversConsent) {
val validatedEmailRoute = EmailRoute.validated(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost")
)

validatedEmailRoute shouldBe
ValidationError("'receiver@localhost' does not consent receiving emails").invalidNel()
}
}
val createValidatedEmailRoute = EmailRoute.factoryWithContext(allSendersAllowed, noReceiversConsent)

val validatedEmailRoute = createValidatedEmailRoute(
"sender@localhost",
"receiver@localhost",
listOf("cc@localhost"),
emptyList()
)

validatedEmailRoute shouldBe
ValidationError("'receiver@localhost' does not consent receiving emails").invalidNel()
}
})

Expand Down