Rewrite example application
This commit is contained in:
parent
41269f06d3
commit
67c1cd33ad
@ -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'
|
||||||
```
|
```
|
||||||
|
@ -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(
|
|
||||||
success = { customer ->
|
|
||||||
call.respond(HttpStatusCode.OK, CustomerDto.from(customer))
|
|
||||||
},
|
|
||||||
failure = { (status, message) ->
|
|
||||||
call.respond(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)
|
|
||||||
.andThen(CustomerService::upsert)
|
|
||||||
.mapError(::messageToResponse)
|
|
||||||
.mapBoth(
|
|
||||||
success = { event ->
|
|
||||||
if (event == null) {
|
|
||||||
call.respond(HttpStatusCode.NotModified)
|
|
||||||
} else {
|
} else {
|
||||||
val (status, message) = messageToResponse(event)
|
call.respond(status)
|
||||||
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}"
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 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()
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -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(
|
data class PersonalName(
|
||||||
val first: String,
|
val first: String,
|
||||||
val last: String
|
val last: String
|
||||||
) {
|
)
|
||||||
companion object {
|
|
||||||
private const val MAX_LENGTH = 10
|
private const val MAX_LENGTH = 10
|
||||||
|
|
||||||
fun create(first: String?, last: String?): Result<PersonalName, DomainMessage> {
|
fun Pair<String?, String?>.toPersonalName(): Result<PersonalName, DomainMessage> {
|
||||||
|
val (first, last) = this
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
first.isNullOrBlank() -> Err(FirstNameRequired)
|
first.isNullOrBlank() -> Err(FirstNameRequired)
|
||||||
last.isNullOrBlank() -> Err(LastNameRequired)
|
last.isNullOrBlank() -> Err(LastNameRequired)
|
||||||
@ -20,5 +22,3 @@ data class PersonalName(
|
|||||||
else -> Ok(PersonalName(first, last))
|
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
|
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
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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].
|
* 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)
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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