Rewrite example application

This commit is contained in:
Michael Bull 2020-08-26 22:02:22 +01:00
parent 41269f06d3
commit 67c1cd33ad
19 changed files with 274 additions and 256 deletions

View File

@ -217,19 +217,18 @@ user-facing errors.
#### Fetch customer information #### Fetch customer information
``` ```
$ curl -i -X GET 'http://localhost:9000/customers/5' $ curl -i -X GET 'http://localhost:9000/customers/1'
``` ```
``` ```
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8 Content-Type: application/json; charset=UTF-8
Content-Length: 93 Content-Length: 84
{ {
"id": 5,
"firstName": "Michael", "firstName": "Michael",
"lastName": "Bull", "lastName": "Bull",
"email": "example@email.com" "email": "michael@example.com"
} }
``` ```
@ -242,7 +241,7 @@ $ curl -i -X POST \
'{ '{
"firstName": "Your", "firstName": "Your",
"lastName": "Name", "lastName": "Name",
"email": "your@email.com" "email": "email@example.com"
}' \ }' \
'http://localhost:9000/customers/200' 'http://localhost:9000/customers/200'
``` ```

View File

@ -1,11 +1,8 @@
package com.github.michaelbull.result.example package com.github.michaelbull.result.example
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen import com.github.michaelbull.result.andThen
import com.github.michaelbull.result.example.model.domain.Customer import com.github.michaelbull.result.example.model.domain.Created
import com.github.michaelbull.result.example.model.domain.CustomerCreated
import com.github.michaelbull.result.example.model.domain.CustomerId
import com.github.michaelbull.result.example.model.domain.CustomerIdMustBePositive import com.github.michaelbull.result.example.model.domain.CustomerIdMustBePositive
import com.github.michaelbull.result.example.model.domain.CustomerNotFound import com.github.michaelbull.result.example.model.domain.CustomerNotFound
import com.github.michaelbull.result.example.model.domain.CustomerRequired import com.github.michaelbull.result.example.model.domain.CustomerRequired
@ -16,15 +13,20 @@ import com.github.michaelbull.result.example.model.domain.EmailAddressChanged
import com.github.michaelbull.result.example.model.domain.EmailInvalid import com.github.michaelbull.result.example.model.domain.EmailInvalid
import com.github.michaelbull.result.example.model.domain.EmailRequired import com.github.michaelbull.result.example.model.domain.EmailRequired
import com.github.michaelbull.result.example.model.domain.EmailTooLong import com.github.michaelbull.result.example.model.domain.EmailTooLong
import com.github.michaelbull.result.example.model.domain.Event
import com.github.michaelbull.result.example.model.domain.FirstNameChanged
import com.github.michaelbull.result.example.model.domain.FirstNameRequired import com.github.michaelbull.result.example.model.domain.FirstNameRequired
import com.github.michaelbull.result.example.model.domain.FirstNameTooLong import com.github.michaelbull.result.example.model.domain.FirstNameTooLong
import com.github.michaelbull.result.example.model.domain.LastNameChanged
import com.github.michaelbull.result.example.model.domain.LastNameRequired import com.github.michaelbull.result.example.model.domain.LastNameRequired
import com.github.michaelbull.result.example.model.domain.LastNameTooLong import com.github.michaelbull.result.example.model.domain.LastNameTooLong
import com.github.michaelbull.result.example.model.domain.SqlCustomerInvalid import com.github.michaelbull.result.example.model.domain.SqlCustomerInvalid
import com.github.michaelbull.result.example.model.dto.CustomerDto import com.github.michaelbull.result.example.model.dto.CustomerDto
import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.example.model.entity.CustomerId
import com.github.michaelbull.result.example.repository.InMemoryCustomerRepository
import com.github.michaelbull.result.example.service.CustomerService import com.github.michaelbull.result.example.service.CustomerService
import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.toResultOr import com.github.michaelbull.result.toResultOr
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
@ -52,56 +54,49 @@ fun Application.main() {
} }
} }
val customers = setOf(
CustomerEntity(CustomerId(1L), "Michael", "Bull", "michael@example.com"),
CustomerEntity(CustomerId(2L), "Kevin", "Herron", "kevin@example.com"),
CustomerEntity(CustomerId(3L), "Markus", "Padourek", "markus@example.com"),
CustomerEntity(CustomerId(4L), "Tristan", "Hamilton", "tristan@example.com"),
)
val customersById = customers.associateBy(CustomerEntity::id).toMutableMap()
val customerRepository = InMemoryCustomerRepository(customersById)
val customerService = CustomerService(customerRepository)
routing { routing {
get("/customers/{id}") { get("/customers/{id}") {
call.parameters.readId() val (status, message) = call.parameters.readId()
.andThen(CustomerId.Companion::create) .andThen(customerService::getById)
.andThen(CustomerService::getById) .mapBoth(::customerToResponse, ::messageToResponse)
.mapError(::messageToResponse)
.mapBoth( call.respond(status, message)
success = { customer ->
call.respond(HttpStatusCode.OK, CustomerDto.from(customer))
},
failure = { (status, message) ->
call.respond(status, message)
}
)
} }
post("/customers/{id}") { post("/customers/{id}") {
call.parameters.readId() val (status, message) = call.parameters.readId()
.andThen { id -> .andThen { customerService.save(it, call.receive()) }
val dto = call.receive<CustomerDto>() .mapBoth(::eventToResponse, ::messageToResponse)
dto.id = id
Ok(dto) if (message != null) {
} call.respond(status, message)
.andThen(Customer.Companion::from) } else {
.andThen(CustomerService::upsert) call.respond(status)
.mapError(::messageToResponse) }
.mapBoth(
success = { event ->
if (event == null) {
call.respond(HttpStatusCode.NotModified)
} else {
val (status, message) = messageToResponse(event)
call.respond(status, message)
}
},
failure = { (status, message) ->
call.respond(status, message)
}
)
} }
} }
} }
private fun Parameters.readId(): Result<Long, DomainMessage> { private fun Parameters.readId(): Result<Long, DomainMessage> {
return this["id"] return get("id")
?.toLongOrNull() ?.toLongOrNull()
.toResultOr { CustomerRequired } .toResultOr { CustomerRequired }
} }
private fun customerToResponse(customer: CustomerDto) = HttpStatusCode.OK to customer
private fun messageToResponse(message: DomainMessage) = when (message) { private fun messageToResponse(message: DomainMessage) = when (message) {
CustomerRequired, CustomerRequired,
CustomerIdMustBePositive, CustomerIdMustBePositive,
@ -112,23 +107,32 @@ private fun messageToResponse(message: DomainMessage) = when (message) {
EmailRequired, EmailRequired,
EmailTooLong, EmailTooLong,
EmailInvalid -> EmailInvalid ->
Pair(HttpStatusCode.BadRequest, "There is an error in your request") HttpStatusCode.BadRequest to "There is an error in your request"
// events
CustomerCreated ->
Pair(HttpStatusCode.Created, "Customer created")
is EmailAddressChanged ->
Pair(HttpStatusCode.OK, "Email address changed from ${message.old} to ${message.new}")
// exposed errors // exposed errors
CustomerNotFound -> CustomerNotFound ->
Pair(HttpStatusCode.NotFound, "Unknown customer") HttpStatusCode.NotFound to "Unknown customer"
// internal errors // internal errors
SqlCustomerInvalid, SqlCustomerInvalid,
DatabaseTimeout, DatabaseTimeout,
is DatabaseError -> is DatabaseError ->
Pair(HttpStatusCode.InternalServerError, "Internal server error occurred") HttpStatusCode.InternalServerError to "Internal server error occurred"
}
private fun eventToResponse(event: Event?) = when (event) {
null ->
HttpStatusCode.NotModified to null
Created ->
HttpStatusCode.Created to "Customer created"
is FirstNameChanged ->
HttpStatusCode.OK to "First name changed from ${event.old} to ${event.new}"
is LastNameChanged ->
HttpStatusCode.OK to "First name changed from ${event.old} to ${event.new}"
is EmailAddressChanged ->
HttpStatusCode.OK to "Email address changed from ${event.old} to ${event.new}"
} }

View File

@ -1,28 +1,6 @@
package com.github.michaelbull.result.example.model.domain package com.github.michaelbull.result.example.model.domain
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.example.model.dto.CustomerDto
import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.zip
data class Customer( data class Customer(
val id: CustomerId,
val name: PersonalName, val name: PersonalName,
val email: EmailAddress val email: EmailAddress
) { )
companion object {
fun from(entity: CustomerEntity): Result<Customer, DomainMessage> {
val createId = { CustomerId.create(entity.id) }
val createName = { PersonalName.create(entity.firstName, entity.lastName) }
val createEmail = { EmailAddress.create(entity.email) }
return zip(createId, createName, createEmail, ::Customer)
}
fun from(dto: CustomerDto): Result<Customer, DomainMessage> {
val createId = { CustomerId.create(dto.id) }
val createName = { PersonalName.create(dto.firstName, dto.lastName) }
val createEmail = { EmailAddress.create(dto.email) }
return zip(createId, createName, createEmail, ::Customer)
}
}
}

View File

@ -1,14 +0,0 @@
package com.github.michaelbull.result.example.model.domain
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
data class CustomerId(val id: Long) {
companion object {
fun create(id: Long?) = when {
id == null -> Err(CustomerRequired)
id < 1 -> Err(CustomerIdMustBePositive)
else -> Ok(CustomerId(id))
}
}
}

View File

@ -20,11 +20,6 @@ object EmailRequired : DomainMessage()
object EmailTooLong : DomainMessage() object EmailTooLong : DomainMessage()
object EmailInvalid : DomainMessage() object EmailInvalid : DomainMessage()
/* events */
object CustomerCreated : DomainMessage()
class EmailAddressChanged(val old: String, val new: String) : DomainMessage()
/* exposed errors */ /* exposed errors */
object CustomerNotFound : DomainMessage() object CustomerNotFound : DomainMessage()

View File

@ -1,24 +1,5 @@
package com.github.michaelbull.result.example.model.domain package com.github.michaelbull.result.example.model.domain
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
data class EmailAddress( data class EmailAddress(
val address: String val address: String
) { )
companion object {
private const val MAX_LENGTH = 20
private val PATTERN = ".+@.+\\..+".toRegex() // crude validation
fun create(address: String?): Result<EmailAddress, DomainMessage> {
return when {
address.isNullOrBlank() -> Err(EmailRequired)
address.length > MAX_LENGTH -> Err(EmailTooLong)
!address.matches(PATTERN) -> Err(EmailInvalid)
else -> Ok(EmailAddress(address))
}
}
}
}

View File

@ -0,0 +1,8 @@
package com.github.michaelbull.result.example.model.domain
sealed class Event
object Created : Event()
class FirstNameChanged(val old: String, val new: String) : Event()
class LastNameChanged(val old: String, val new: String) : Event()
class EmailAddressChanged(val old: String, val new: String) : Event()

View File

@ -7,18 +7,18 @@ import com.github.michaelbull.result.Result
data class PersonalName( data class PersonalName(
val first: String, val first: String,
val last: String val last: String
) { )
companion object {
private const val MAX_LENGTH = 10
fun create(first: String?, last: String?): Result<PersonalName, DomainMessage> { private const val MAX_LENGTH = 10
return when {
first.isNullOrBlank() -> Err(FirstNameRequired) fun Pair<String?, String?>.toPersonalName(): Result<PersonalName, DomainMessage> {
last.isNullOrBlank() -> Err(LastNameRequired) val (first, last) = this
first.length > MAX_LENGTH -> Err(FirstNameTooLong)
last.length > MAX_LENGTH -> Err(LastNameTooLong) return when {
else -> Ok(PersonalName(first, last)) first.isNullOrBlank() -> Err(FirstNameRequired)
} last.isNullOrBlank() -> Err(LastNameRequired)
} first.length > MAX_LENGTH -> Err(FirstNameTooLong)
last.length > MAX_LENGTH -> Err(LastNameTooLong)
else -> Ok(PersonalName(first, last))
} }
} }

View File

@ -1,9 +0,0 @@
package com.github.michaelbull.result.example.model.domain.repository
import com.github.michaelbull.result.example.model.domain.Customer
import com.github.michaelbull.result.example.model.entity.CustomerEntity
/**
* A repository that stores [Customers][Customer] with a [Long] ID.
*/
interface CustomerRepository : Repository<CustomerEntity, Long>

View File

@ -1,23 +1,7 @@
package com.github.michaelbull.result.example.model.dto package com.github.michaelbull.result.example.model.dto
import com.github.michaelbull.result.example.model.domain.Customer
/**
* A [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) sent over the network
* that represents a [Customer].
*/
data class CustomerDto( data class CustomerDto(
var id: Long = 0L, val firstName: String,
var firstName: String? = null, val lastName: String,
var lastName: String? = null, val email: String
var email: String? = null )
) {
companion object {
fun from(customer: Customer) = CustomerDto(
id = customer.id.id,
firstName = customer.name.first,
lastName = customer.name.last,
email = customer.email.address
)
}
}

View File

@ -1,23 +1,12 @@
package com.github.michaelbull.result.example.model.entity package com.github.michaelbull.result.example.model.entity
import com.github.michaelbull.result.example.model.domain.Customer
/** /**
* Represents an [Entity](https://docs.oracle.com/cd/E17277_02/html/collections/tutorial/Entity.html) * Represents an [Entity](https://docs.oracle.com/cd/E17277_02/html/collections/tutorial/Entity.html)
* mapped to a table in a database. * mapped to a table in a database.
*/ */
data class CustomerEntity( data class CustomerEntity(
var id: Long = 0L, val id: CustomerId,
var firstName: String? = null, val firstName: String,
var lastName: String? = null, val lastName: String,
var email: String? = null val email: String
) { )
companion object {
fun from(customer: Customer) = CustomerEntity(
id = customer.id.id,
firstName = customer.name.first,
lastName = customer.name.last,
email = customer.email.address
)
}
}

View File

@ -0,0 +1,3 @@
package com.github.michaelbull.result.example.model.entity
data class CustomerId(val id: Long)

View File

@ -0,0 +1,9 @@
package com.github.michaelbull.result.example.repository
import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.example.model.entity.CustomerId
/**
* A repository that stores a [CustomerEntity] identified by a [CustomerId].
*/
interface CustomerRepository : Repository<CustomerEntity, CustomerId>

View File

@ -0,0 +1,28 @@
package com.github.michaelbull.result.example.repository
import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.example.model.entity.CustomerId
import java.sql.SQLTimeoutException
class InMemoryCustomerRepository(
private val customers: MutableMap<CustomerId, CustomerEntity>
) : CustomerRepository {
override fun findById(id: CustomerId): CustomerEntity? {
return customers.entries.find { (key) -> key == id }?.value
}
override fun save(entity: CustomerEntity) {
val id = entity.id
if (id == TIMEOUT_CUSTOMER_ID) {
throw SQLTimeoutException()
} else {
customers[id] = entity
}
}
private companion object {
private val TIMEOUT_CUSTOMER_ID = CustomerId(42L)
}
}

View File

@ -1,10 +1,9 @@
package com.github.michaelbull.result.example.model.domain.repository package com.github.michaelbull.result.example.repository
/** /**
* A class that encapsulates storage and retrieval of domain objects of type [T], identified by a key of type [ID]. * A class that encapsulates storage and retrieval of domain objects of type [T], identified by a key of type [ID].
*/ */
interface Repository<T, ID> { interface Repository<T, ID> {
fun findAll(): Collection<T> fun findById(id: ID): T?
fun update(entity: T) fun save(entity: T)
fun insert(entity: T)
} }

View File

@ -1,69 +1,126 @@
package com.github.michaelbull.result.example.service package com.github.michaelbull.result.example.service
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen import com.github.michaelbull.result.andThen
import com.github.michaelbull.result.example.model.domain.Created
import com.github.michaelbull.result.example.model.domain.Customer import com.github.michaelbull.result.example.model.domain.Customer
import com.github.michaelbull.result.example.model.domain.CustomerCreated import com.github.michaelbull.result.example.model.domain.CustomerIdMustBePositive
import com.github.michaelbull.result.example.model.domain.CustomerId
import com.github.michaelbull.result.example.model.domain.CustomerNotFound import com.github.michaelbull.result.example.model.domain.CustomerNotFound
import com.github.michaelbull.result.example.model.domain.CustomerRequired
import com.github.michaelbull.result.example.model.domain.DatabaseError import com.github.michaelbull.result.example.model.domain.DatabaseError
import com.github.michaelbull.result.example.model.domain.DatabaseTimeout import com.github.michaelbull.result.example.model.domain.DatabaseTimeout
import com.github.michaelbull.result.example.model.domain.DomainMessage import com.github.michaelbull.result.example.model.domain.DomainMessage
import com.github.michaelbull.result.example.model.domain.EmailAddressChanged import com.github.michaelbull.result.example.model.domain.EmailAddressChanged
import com.github.michaelbull.result.example.model.domain.Event
import com.github.michaelbull.result.example.model.domain.FirstNameChanged
import com.github.michaelbull.result.example.model.domain.LastNameChanged
import com.github.michaelbull.result.example.model.dto.CustomerDto
import com.github.michaelbull.result.example.model.entity.CustomerEntity import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.example.model.entity.CustomerId
import com.github.michaelbull.result.example.repository.CustomerRepository
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.map import com.github.michaelbull.result.map
import com.github.michaelbull.result.mapAll
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import com.github.michaelbull.result.toResultOr import com.github.michaelbull.result.toResultOr
import com.github.michaelbull.result.zip
import java.sql.SQLTimeoutException import java.sql.SQLTimeoutException
object CustomerService { class CustomerService(
private val repository = InMemoryCustomerRepository() private val repository: CustomerRepository
) {
fun getAll(): Result<Collection<Customer>, DomainMessage> { fun getById(id: Long): Result<CustomerDto, DomainMessage> {
return runCatching(repository::findAll) return parseCustomerId(id)
.mapError(::exceptionToDomainMessage) .andThen(::findById)
.mapAll(Customer.Companion::from) .map(::entityToDto)
} }
fun getById(id: CustomerId): Result<Customer, DomainMessage> { fun save(id: Long, dto: CustomerDto): Result<Event?, DomainMessage> {
return getAll().andThen { customers -> customers.findCustomer(id) } return parseCustomerId(id)
.andThen { upsert(it, dto) }
} }
fun upsert(customer: Customer): Result<DomainMessage?, DomainMessage> { private fun parseCustomerId(id: Long?) = when {
val entity = CustomerEntity.from(customer) id == null -> Err(CustomerRequired)
return getById(customer.id).mapBoth( id < 1 -> Err(CustomerIdMustBePositive)
success = { existing -> updateCustomer(entity, existing, customer) }, else -> Ok(CustomerId(id))
failure = { createCustomer(entity) } }
private fun entityToDto(entity: CustomerEntity): CustomerDto {
return CustomerDto(
firstName = entity.firstName,
lastName = entity.lastName,
email = entity.email
) )
} }
private fun updateCustomer(entity: CustomerEntity, old: Customer, new: Customer) = private fun findById(id: CustomerId): Result<CustomerEntity, CustomerNotFound> {
runCatching { repository.update(entity) } return repository.findById(id)
.map { differenceBetween(old, new) } .toResultOr { CustomerNotFound }
.mapError(::exceptionToDomainMessage)
private fun createCustomer(entity: CustomerEntity) =
runCatching { repository.insert(entity) }
.map { CustomerCreated }
.mapError(::exceptionToDomainMessage)
private fun Collection<Customer>.findCustomer(id: CustomerId): Result<Customer, CustomerNotFound> {
return find { it.id == id }.toResultOr { CustomerNotFound }
} }
private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? { private fun upsert(id: CustomerId, dto: CustomerDto): Result<Event?, DomainMessage> {
return if (new.email != old.email) { val existingCustomer = repository.findById(id)
EmailAddressChanged(old.email.address, new.email.address)
return if (existingCustomer != null) {
update(existingCustomer, dto)
} else { } else {
null insert(id, dto)
} }
} }
private fun update(entity: CustomerEntity, dto: CustomerDto): Result<Event?, DomainMessage> {
val validated = validate(dto).getOrElse { return Err(it) }
val updated = entity.copy(
firstName = validated.name.first,
lastName = validated.name.last,
email = validated.email.address
)
return runCatching { repository.save(updated) }
.map { compare(entity, updated) }
.mapError(::exceptionToDomainMessage)
}
private fun insert(id: CustomerId, dto: CustomerDto): Result<Created, DomainMessage> {
val entity = createEntity(id, dto).getOrElse { return Err(it) }
return runCatching { repository.save(entity) }
.map { Created }
.mapError(::exceptionToDomainMessage)
}
private fun validate(dto: CustomerDto): Result<Customer, DomainMessage> {
return zip(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
::Customer
)
}
private fun createEntity(id: CustomerId, dto: CustomerDto): Result<CustomerEntity, DomainMessage> {
return zip(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
{ (first, last), (address) -> CustomerEntity(id, first, last, address) }
)
}
private fun exceptionToDomainMessage(t: Throwable) = when (t) { private fun exceptionToDomainMessage(t: Throwable) = when (t) {
is SQLTimeoutException -> DatabaseTimeout is SQLTimeoutException -> DatabaseTimeout
else -> DatabaseError(t.message) else -> DatabaseError(t.message)
} }
private fun compare(old: CustomerEntity, new: CustomerEntity): Event? {
return when {
new.firstName != old.firstName -> FirstNameChanged(old.firstName, new.firstName)
new.lastName != old.lastName -> LastNameChanged(old.lastName, new.lastName)
new.email != old.email -> EmailAddressChanged(old.email, new.email)
else -> null
}
}
} }

View File

@ -0,0 +1,25 @@
package com.github.michaelbull.result.example.service
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.example.model.domain.DomainMessage
import com.github.michaelbull.result.example.model.domain.EmailAddress
import com.github.michaelbull.result.example.model.domain.EmailInvalid
import com.github.michaelbull.result.example.model.domain.EmailRequired
import com.github.michaelbull.result.example.model.domain.EmailTooLong
object EmailAddressParser {
private const val MAX_LENGTH = 20
private val PATTERN = ".+@.+\\..+".toRegex() // crude validation
fun parse(address: String?): Result<EmailAddress, DomainMessage> {
return when {
address.isNullOrBlank() -> Err(EmailRequired)
address.length > MAX_LENGTH -> Err(EmailTooLong)
!address.matches(this.PATTERN) -> Err(EmailInvalid)
else -> Ok(EmailAddress(address))
}
}
}

View File

@ -1,44 +0,0 @@
package com.github.michaelbull.result.example.service
import com.github.michaelbull.result.example.model.domain.repository.CustomerRepository
import com.github.michaelbull.result.example.model.entity.CustomerEntity
import java.sql.SQLException
import java.sql.SQLTimeoutException
class InMemoryCustomerRepository : CustomerRepository {
private val table = mutableMapOf(
5L to CustomerEntity(5L, "Michael", "Bull", "example@email.com")
)
override fun findAll(): Collection<CustomerEntity> {
return table.values
}
override fun update(entity: CustomerEntity) {
val id = entity.id
if (id !in table) {
throw SQLException("No customer found for id $id")
} else {
setOrTimeout(id, entity)
}
}
override fun insert(entity: CustomerEntity) {
val id = entity.id
if (id in table) {
throw SQLException("Customer already exists with id $id")
} else {
setOrTimeout(id, entity)
}
}
private fun setOrTimeout(id: Long, entity: CustomerEntity) {
if (id == 42L) {
throw SQLTimeoutException()
} else {
table[id] = entity
}
}
}

View File

@ -0,0 +1,26 @@
package com.github.michaelbull.result.example.service
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.example.model.domain.DomainMessage
import com.github.michaelbull.result.example.model.domain.FirstNameRequired
import com.github.michaelbull.result.example.model.domain.FirstNameTooLong
import com.github.michaelbull.result.example.model.domain.LastNameRequired
import com.github.michaelbull.result.example.model.domain.LastNameTooLong
import com.github.michaelbull.result.example.model.domain.PersonalName
object PersonalNameParser {
private const val MAX_LENGTH = 10
fun parse(first: String?, last: String?): Result<PersonalName, DomainMessage> {
return when {
first.isNullOrBlank() -> Err(FirstNameRequired)
last.isNullOrBlank() -> Err(LastNameRequired)
first.length > MAX_LENGTH -> Err(FirstNameTooLong)
last.length > MAX_LENGTH -> Err(LastNameTooLong)
else -> Ok(PersonalName(first, last))
}
}
}