diff --git a/README.md b/README.md index be6005d..a1dbb63 100644 --- a/README.md +++ b/README.md @@ -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' ``` diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt index fd70dea..18bcb42 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt @@ -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) -> - call.respond(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() - dto.id = id - Ok(dto) - } - .andThen(Customer.Companion::from) - .andThen(CustomerService::upsert) - .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) - } - ) + val (status, message) = call.parameters.readId() + .andThen { customerService.save(it, call.receive()) } + .mapBoth(::eventToResponse, ::messageToResponse) + + if (message != null) { + call.respond(status, message) + } else { + call.respond(status) + } } } } private fun Parameters.readId(): Result { - 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}" } diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Customer.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Customer.kt index 4e4aeba..32ec51b 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Customer.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Customer.kt @@ -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 { - 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 { - 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) - } - } -} +) diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/CustomerId.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/CustomerId.kt deleted file mode 100644 index fcfb5f5..0000000 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/CustomerId.kt +++ /dev/null @@ -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)) - } - } -} diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/DomainMessage.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/DomainMessage.kt index e867dec..832b48c 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/DomainMessage.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/DomainMessage.kt @@ -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() diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/EmailAddress.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/EmailAddress.kt index 0ec6944..407ba20 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/EmailAddress.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/EmailAddress.kt @@ -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 { - return when { - address.isNullOrBlank() -> Err(EmailRequired) - address.length > MAX_LENGTH -> Err(EmailTooLong) - !address.matches(PATTERN) -> Err(EmailInvalid) - else -> Ok(EmailAddress(address)) - } - } - } -} - +) diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Event.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Event.kt new file mode 100644 index 0000000..56527fd --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/Event.kt @@ -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() diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/PersonalName.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/PersonalName.kt index ae6564e..710ee0b 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/PersonalName.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/PersonalName.kt @@ -7,18 +7,18 @@ import com.github.michaelbull.result.Result data class PersonalName( val first: String, val last: String -) { - companion object { - private const val MAX_LENGTH = 10 +) - fun create(first: String?, last: String?): Result { - 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)) - } - } +private const val MAX_LENGTH = 10 + +fun Pair.toPersonalName(): Result { + val (first, last) = this + + 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)) } } diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/CustomerRepository.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/CustomerRepository.kt deleted file mode 100644 index 0fb7656..0000000 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/CustomerRepository.kt +++ /dev/null @@ -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 diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/dto/CustomerDto.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/dto/CustomerDto.kt index 70c5a88..70b4ecf 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/dto/CustomerDto.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/dto/CustomerDto.kt @@ -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 +) diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerEntity.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerEntity.kt index 1ea6b2d..6b72424 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerEntity.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerEntity.kt @@ -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 +) diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerId.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerId.kt new file mode 100644 index 0000000..587f3aa --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/model/entity/CustomerId.kt @@ -0,0 +1,3 @@ +package com.github.michaelbull.result.example.model.entity + +data class CustomerId(val id: Long) diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/repository/CustomerRepository.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/CustomerRepository.kt new file mode 100644 index 0000000..3883b33 --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/CustomerRepository.kt @@ -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 diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/repository/InMemoryCustomerRepository.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/InMemoryCustomerRepository.kt new file mode 100644 index 0000000..1fdeb18 --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/InMemoryCustomerRepository.kt @@ -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 +) : 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) + } +} diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/Repository.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/Repository.kt similarity index 50% rename from example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/Repository.kt rename to example/src/main/kotlin/com/github/michaelbull/result/example/repository/Repository.kt index e22844e..3e9b96a 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/model/domain/repository/Repository.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/repository/Repository.kt @@ -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 { - fun findAll(): Collection - fun update(entity: T) - fun insert(entity: T) + fun findById(id: ID): T? + fun save(entity: T) } diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt index 34b4d46..7e1a9df 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt @@ -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, DomainMessage> { - return runCatching(repository::findAll) - .mapError(::exceptionToDomainMessage) - .mapAll(Customer.Companion::from) + fun getById(id: Long): Result { + return parseCustomerId(id) + .andThen(::findById) + .map(::entityToDto) } - fun getById(id: CustomerId): Result { - return getAll().andThen { customers -> customers.findCustomer(id) } + fun save(id: Long, dto: CustomerDto): Result { + return parseCustomerId(id) + .andThen { upsert(it, dto) } } - fun upsert(customer: Customer): Result { - 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.findCustomer(id: CustomerId): Result { - return find { it.id == id }.toResultOr { CustomerNotFound } + private fun findById(id: CustomerId): Result { + 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 { + 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 { + 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 { + val entity = createEntity(id, dto).getOrElse { return Err(it) } + + return runCatching { repository.save(entity) } + .map { Created } + .mapError(::exceptionToDomainMessage) + } + + private fun validate(dto: CustomerDto): Result { + return zip( + { PersonalNameParser.parse(dto.firstName, dto.lastName) }, + { EmailAddressParser.parse(dto.email) }, + ::Customer + ) + } + + private fun createEntity(id: CustomerId, dto: CustomerDto): Result { + 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 + } + } } diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/service/EmailAddressParser.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/service/EmailAddressParser.kt new file mode 100644 index 0000000..f929825 --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/service/EmailAddressParser.kt @@ -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 { + return when { + address.isNullOrBlank() -> Err(EmailRequired) + address.length > MAX_LENGTH -> Err(EmailTooLong) + !address.matches(this.PATTERN) -> Err(EmailInvalid) + else -> Ok(EmailAddress(address)) + } + } +} diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/service/InMemoryCustomerRepository.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/service/InMemoryCustomerRepository.kt deleted file mode 100644 index 3f828d0..0000000 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/service/InMemoryCustomerRepository.kt +++ /dev/null @@ -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 { - 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 - } - } -} diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/service/PersonalNameParser.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/service/PersonalNameParser.kt new file mode 100644 index 0000000..1759739 --- /dev/null +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/service/PersonalNameParser.kt @@ -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 { + 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)) + } + } +}