Rewrite example application
This commit is contained in:
parent
41269f06d3
commit
67c1cd33ad
@ -217,19 +217,18 @@ user-facing errors.
|
||||
#### 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
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 93
|
||||
Content-Length: 84
|
||||
|
||||
{
|
||||
"id": 5,
|
||||
"firstName": "Michael",
|
||||
"lastName": "Bull",
|
||||
"email": "example@email.com"
|
||||
"email": "michael@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
@ -242,7 +241,7 @@ $ curl -i -X POST \
|
||||
'{
|
||||
"firstName": "Your",
|
||||
"lastName": "Name",
|
||||
"email": "your@email.com"
|
||||
"email": "email@example.com"
|
||||
}' \
|
||||
'http://localhost:9000/customers/200'
|
||||
```
|
||||
|
@ -1,11 +1,8 @@
|
||||
package com.github.michaelbull.result.example
|
||||
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.andThen
|
||||
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.CustomerId
|
||||
import com.github.michaelbull.result.example.model.domain.Created
|
||||
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.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.EmailRequired
|
||||
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.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.LastNameTooLong
|
||||
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.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.mapBoth
|
||||
import com.github.michaelbull.result.mapError
|
||||
import com.github.michaelbull.result.toResultOr
|
||||
import io.ktor.application.Application
|
||||
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 {
|
||||
get("/customers/{id}") {
|
||||
call.parameters.readId()
|
||||
.andThen(CustomerId.Companion::create)
|
||||
.andThen(CustomerService::getById)
|
||||
.mapError(::messageToResponse)
|
||||
.mapBoth(
|
||||
success = { customer ->
|
||||
call.respond(HttpStatusCode.OK, CustomerDto.from(customer))
|
||||
},
|
||||
failure = { (status, message) ->
|
||||
val (status, message) = call.parameters.readId()
|
||||
.andThen(customerService::getById)
|
||||
.mapBoth(::customerToResponse, ::messageToResponse)
|
||||
|
||||
call.respond(status, message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
post("/customers/{id}") {
|
||||
call.parameters.readId()
|
||||
.andThen { id ->
|
||||
val dto = call.receive<CustomerDto>()
|
||||
dto.id = id
|
||||
Ok(dto)
|
||||
}
|
||||
.andThen(Customer.Companion::from)
|
||||
.andThen(CustomerService::upsert)
|
||||
.mapError(::messageToResponse)
|
||||
.mapBoth(
|
||||
success = { event ->
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotModified)
|
||||
val (status, message) = call.parameters.readId()
|
||||
.andThen { customerService.save(it, call.receive()) }
|
||||
.mapBoth(::eventToResponse, ::messageToResponse)
|
||||
|
||||
if (message != null) {
|
||||
call.respond(status, message)
|
||||
} else {
|
||||
val (status, message) = messageToResponse(event)
|
||||
call.respond(status, message)
|
||||
call.respond(status)
|
||||
}
|
||||
},
|
||||
failure = { (status, message) ->
|
||||
call.respond(status, message)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Parameters.readId(): Result<Long, DomainMessage> {
|
||||
return this["id"]
|
||||
return get("id")
|
||||
?.toLongOrNull()
|
||||
.toResultOr { CustomerRequired }
|
||||
}
|
||||
|
||||
private fun customerToResponse(customer: CustomerDto) = HttpStatusCode.OK to customer
|
||||
|
||||
private fun messageToResponse(message: DomainMessage) = when (message) {
|
||||
CustomerRequired,
|
||||
CustomerIdMustBePositive,
|
||||
@ -112,23 +107,32 @@ private fun messageToResponse(message: DomainMessage) = when (message) {
|
||||
EmailRequired,
|
||||
EmailTooLong,
|
||||
EmailInvalid ->
|
||||
Pair(HttpStatusCode.BadRequest, "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}")
|
||||
HttpStatusCode.BadRequest to "There is an error in your request"
|
||||
|
||||
// exposed errors
|
||||
CustomerNotFound ->
|
||||
Pair(HttpStatusCode.NotFound, "Unknown customer")
|
||||
HttpStatusCode.NotFound to "Unknown customer"
|
||||
|
||||
// internal errors
|
||||
SqlCustomerInvalid,
|
||||
DatabaseTimeout,
|
||||
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}"
|
||||
}
|
||||
|
@ -1,28 +1,6 @@
|
||||
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(
|
||||
val id: CustomerId,
|
||||
val name: PersonalName,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -20,11 +20,6 @@ object EmailRequired : DomainMessage()
|
||||
object EmailTooLong : DomainMessage()
|
||||
object EmailInvalid : DomainMessage()
|
||||
|
||||
/* events */
|
||||
|
||||
object CustomerCreated : DomainMessage()
|
||||
class EmailAddressChanged(val old: String, val new: String) : DomainMessage()
|
||||
|
||||
/* exposed errors */
|
||||
|
||||
object CustomerNotFound : DomainMessage()
|
||||
|
@ -1,24 +1,5 @@
|
||||
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(
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
)
|
||||
|
@ -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()
|
@ -7,11 +7,13 @@ import com.github.michaelbull.result.Result
|
||||
data class PersonalName(
|
||||
val first: String,
|
||||
val last: String
|
||||
) {
|
||||
companion object {
|
||||
private const val MAX_LENGTH = 10
|
||||
)
|
||||
|
||||
private const val MAX_LENGTH = 10
|
||||
|
||||
fun Pair<String?, String?>.toPersonalName(): Result<PersonalName, DomainMessage> {
|
||||
val (first, last) = this
|
||||
|
||||
fun create(first: String?, last: String?): Result<PersonalName, DomainMessage> {
|
||||
return when {
|
||||
first.isNullOrBlank() -> Err(FirstNameRequired)
|
||||
last.isNullOrBlank() -> Err(LastNameRequired)
|
||||
@ -19,6 +21,4 @@ data class PersonalName(
|
||||
last.length > MAX_LENGTH -> Err(LastNameTooLong)
|
||||
else -> Ok(PersonalName(first, last))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -1,23 +1,7 @@
|
||||
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(
|
||||
var id: Long = 0L,
|
||||
var firstName: String? = null,
|
||||
var lastName: String? = null,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String
|
||||
)
|
||||
|
@ -1,23 +1,12 @@
|
||||
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)
|
||||
* mapped to a table in a database.
|
||||
*/
|
||||
data class CustomerEntity(
|
||||
var id: Long = 0L,
|
||||
var firstName: String? = null,
|
||||
var lastName: String? = null,
|
||||
var email: String? = null
|
||||
) {
|
||||
companion object {
|
||||
fun from(customer: Customer) = CustomerEntity(
|
||||
id = customer.id.id,
|
||||
firstName = customer.name.first,
|
||||
lastName = customer.name.last,
|
||||
email = customer.email.address
|
||||
)
|
||||
}
|
||||
}
|
||||
val id: CustomerId,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String
|
||||
)
|
||||
|
@ -0,0 +1,3 @@
|
||||
package com.github.michaelbull.result.example.model.entity
|
||||
|
||||
data class CustomerId(val id: Long)
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
@ -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].
|
||||
*/
|
||||
interface Repository<T, ID> {
|
||||
fun findAll(): Collection<T>
|
||||
fun update(entity: T)
|
||||
fun insert(entity: T)
|
||||
fun findById(id: ID): T?
|
||||
fun save(entity: T)
|
||||
}
|
@ -1,69 +1,126 @@
|
||||
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.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.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.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.DatabaseTimeout
|
||||
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.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.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.mapAll
|
||||
import com.github.michaelbull.result.mapBoth
|
||||
import com.github.michaelbull.result.mapError
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import com.github.michaelbull.result.toResultOr
|
||||
import com.github.michaelbull.result.zip
|
||||
import java.sql.SQLTimeoutException
|
||||
|
||||
object CustomerService {
|
||||
private val repository = InMemoryCustomerRepository()
|
||||
class CustomerService(
|
||||
private val repository: CustomerRepository
|
||||
) {
|
||||
|
||||
fun getAll(): Result<Collection<Customer>, DomainMessage> {
|
||||
return runCatching(repository::findAll)
|
||||
.mapError(::exceptionToDomainMessage)
|
||||
.mapAll(Customer.Companion::from)
|
||||
fun getById(id: Long): Result<CustomerDto, DomainMessage> {
|
||||
return parseCustomerId(id)
|
||||
.andThen(::findById)
|
||||
.map(::entityToDto)
|
||||
}
|
||||
|
||||
fun getById(id: CustomerId): Result<Customer, DomainMessage> {
|
||||
return getAll().andThen { customers -> customers.findCustomer(id) }
|
||||
fun save(id: Long, dto: CustomerDto): Result<Event?, DomainMessage> {
|
||||
return parseCustomerId(id)
|
||||
.andThen { upsert(it, dto) }
|
||||
}
|
||||
|
||||
fun upsert(customer: Customer): Result<DomainMessage?, DomainMessage> {
|
||||
val entity = CustomerEntity.from(customer)
|
||||
return getById(customer.id).mapBoth(
|
||||
success = { existing -> updateCustomer(entity, existing, customer) },
|
||||
failure = { createCustomer(entity) }
|
||||
private fun parseCustomerId(id: Long?) = when {
|
||||
id == null -> Err(CustomerRequired)
|
||||
id < 1 -> Err(CustomerIdMustBePositive)
|
||||
else -> Ok(CustomerId(id))
|
||||
}
|
||||
|
||||
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) =
|
||||
runCatching { repository.update(entity) }
|
||||
.map { differenceBetween(old, new) }
|
||||
.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 findById(id: CustomerId): Result<CustomerEntity, CustomerNotFound> {
|
||||
return repository.findById(id)
|
||||
.toResultOr { CustomerNotFound }
|
||||
}
|
||||
|
||||
private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? {
|
||||
return if (new.email != old.email) {
|
||||
EmailAddressChanged(old.email.address, new.email.address)
|
||||
private fun upsert(id: CustomerId, dto: CustomerDto): Result<Event?, DomainMessage> {
|
||||
val existingCustomer = repository.findById(id)
|
||||
|
||||
return if (existingCustomer != null) {
|
||||
update(existingCustomer, dto)
|
||||
} 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) {
|
||||
is SQLTimeoutException -> DatabaseTimeout
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user