From f98e1227e89c2161e7fce6ef7f83ad0e0313d3ff Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Wed, 23 Apr 2025 23:13:48 +0200 Subject: [PATCH] Implementation of getAll and create products endpoints. --- .../GiteaActionsWorkshop/Get all products.bru | 11 +++++ bruno-rest/GiteaActionsWorkshop/bruno.json | 9 ++++ .../environments/Localhost.bru | 3 ++ .../application/product/ProductUseCases.kt | 41 +++++++++++++++++-- .../demo/domain/core/error/DomainError.kt | 3 ++ .../demo/domain/core/error/FunctionalError.kt | 3 ++ .../demo/domain/core/error/TechnicalError.kt | 3 ++ .../product/inputport/ProductInputPort.kt | 11 +++++ .../product/outputport/ProductOutputPort.kt | 16 ++++++++ .../demo/domain/product/port/ProductPort.kt | 14 ------- .../exposition/product/ProductController.kt | 18 ++++++-- .../core/advice/ResultControllerAdvice.kt | 8 ++++ .../product/model/ProductCreationRequest.kt | 8 ++++ .../product/ProductInMemoryAdapter.kt | 19 ++++----- 14 files changed, 136 insertions(+), 31 deletions(-) create mode 100644 bruno-rest/GiteaActionsWorkshop/Get all products.bru create mode 100644 bruno-rest/GiteaActionsWorkshop/bruno.json create mode 100644 bruno-rest/GiteaActionsWorkshop/environments/Localhost.bru create mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/core/error/DomainError.kt create mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/core/error/FunctionalError.kt create mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/core/error/TechnicalError.kt create mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/product/inputport/ProductInputPort.kt create mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/product/outputport/ProductOutputPort.kt delete mode 100644 demo-domain/src/main/kotlin/com/example/demo/domain/product/port/ProductPort.kt create mode 100644 demo-exposition/src/main/kotlin/com/example/demo/exposition/product/core/advice/ResultControllerAdvice.kt create mode 100644 demo-exposition/src/main/kotlin/com/example/demo/exposition/product/model/ProductCreationRequest.kt diff --git a/bruno-rest/GiteaActionsWorkshop/Get all products.bru b/bruno-rest/GiteaActionsWorkshop/Get all products.bru new file mode 100644 index 0000000..d9e89ff --- /dev/null +++ b/bruno-rest/GiteaActionsWorkshop/Get all products.bru @@ -0,0 +1,11 @@ +meta { + name: Get all products + type: http + seq: 2 +} + +get { + url: {{url}}/api/products + body: none + auth: none +} diff --git a/bruno-rest/GiteaActionsWorkshop/bruno.json b/bruno-rest/GiteaActionsWorkshop/bruno.json new file mode 100644 index 0000000..747714b --- /dev/null +++ b/bruno-rest/GiteaActionsWorkshop/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "GiteaActionsWorkshop", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno-rest/GiteaActionsWorkshop/environments/Localhost.bru b/bruno-rest/GiteaActionsWorkshop/environments/Localhost.bru new file mode 100644 index 0000000..3c3d33a --- /dev/null +++ b/bruno-rest/GiteaActionsWorkshop/environments/Localhost.bru @@ -0,0 +1,3 @@ +vars { + url: http://localhost:8080 +} diff --git a/demo-application/src/main/kotlin/com/example/demo/application/product/ProductUseCases.kt b/demo-application/src/main/kotlin/com/example/demo/application/product/ProductUseCases.kt index 30b22ae..a086215 100644 --- a/demo-application/src/main/kotlin/com/example/demo/application/product/ProductUseCases.kt +++ b/demo-application/src/main/kotlin/com/example/demo/application/product/ProductUseCases.kt @@ -1,12 +1,45 @@ package com.example.demo.application.product +import com.example.demo.domain.core.error.FunctionalError +import com.example.demo.domain.product.inputport.ProductInputPort import com.example.demo.domain.product.model.Product -import com.example.demo.domain.product.port.ProductPort +import com.example.demo.domain.product.model.ProductType +import com.example.demo.domain.product.outputport.ProductOutputPort +import com.github.michaelbull.result.* +import com.github.michaelbull.result.coroutines.coroutineBinding import org.springframework.stereotype.Service +import java.util.* @Service class ProductUseCases( - private val productPort: ProductPort -) { - fun getAll(): List = productPort.getAll() + private val productOutputPort: ProductOutputPort +) : ProductInputPort { + override suspend fun getAll(): Result, FunctionalError> = productOutputPort.getAll() + .mapError { FunctionalError(it.message) } + +// override suspend fun create(name: String, type: ProductType): Result { +// if (name.isBlank()) { +// return Err(FunctionalError("Product name should be set.")) +// } +// +// val newProduct = Product(id = UUID.randomUUID(), name, type) +// return productOutputPort.save(newProduct) +// .mapError { FunctionalError(it.message) } +// .map { newProduct } +// } + + override suspend fun create(name: String, type: ProductType): Result = coroutineBinding { + validateName(name).bind() + + val newProduct = Product(id = UUID.randomUUID(), name, type) + productOutputPort.save(newProduct) + .mapError { FunctionalError(it.message) } + .map { newProduct } + .bind() + } + + private fun validateName(name: String): Result = when { + name.isBlank() -> Err(FunctionalError("Product name should be set.")) + else -> Ok(Unit) + } } \ No newline at end of file diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/DomainError.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/DomainError.kt new file mode 100644 index 0000000..023c983 --- /dev/null +++ b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/DomainError.kt @@ -0,0 +1,3 @@ +package com.example.demo.domain.core.error + +open class DomainError(val message: String) diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/FunctionalError.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/FunctionalError.kt new file mode 100644 index 0000000..afd94b5 --- /dev/null +++ b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/FunctionalError.kt @@ -0,0 +1,3 @@ +package com.example.demo.domain.core.error + +class FunctionalError(message: String) : DomainError(message) diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/TechnicalError.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/TechnicalError.kt new file mode 100644 index 0000000..86594fa --- /dev/null +++ b/demo-domain/src/main/kotlin/com/example/demo/domain/core/error/TechnicalError.kt @@ -0,0 +1,3 @@ +package com.example.demo.domain.core.error + +class TechnicalError(message: String) : DomainError(message) diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/product/inputport/ProductInputPort.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/product/inputport/ProductInputPort.kt new file mode 100644 index 0000000..ea9304b --- /dev/null +++ b/demo-domain/src/main/kotlin/com/example/demo/domain/product/inputport/ProductInputPort.kt @@ -0,0 +1,11 @@ +package com.example.demo.domain.product.inputport + +import com.example.demo.domain.core.error.FunctionalError +import com.example.demo.domain.product.model.Product +import com.example.demo.domain.product.model.ProductType +import com.github.michaelbull.result.Result + +interface ProductInputPort { + suspend fun create(name: String, type: ProductType): Result + suspend fun getAll(): Result, FunctionalError> +} \ No newline at end of file diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/product/outputport/ProductOutputPort.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/product/outputport/ProductOutputPort.kt new file mode 100644 index 0000000..e5ae5f7 --- /dev/null +++ b/demo-domain/src/main/kotlin/com/example/demo/domain/product/outputport/ProductOutputPort.kt @@ -0,0 +1,16 @@ +package com.example.demo.domain.product.outputport + +import com.example.demo.domain.core.error.TechnicalError +import com.example.demo.domain.product.model.Product +import com.github.michaelbull.result.Result +import java.util.UUID + +interface ProductOutputPort { + fun getById(id: UUID): Product? + + suspend fun getAll(): Result, TechnicalError> + + fun save(product: Product): Result + + fun deleteById(id: UUID) +} \ No newline at end of file diff --git a/demo-domain/src/main/kotlin/com/example/demo/domain/product/port/ProductPort.kt b/demo-domain/src/main/kotlin/com/example/demo/domain/product/port/ProductPort.kt deleted file mode 100644 index e5f2e34..0000000 --- a/demo-domain/src/main/kotlin/com/example/demo/domain/product/port/ProductPort.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.demo.domain.product.port - -import com.example.demo.domain.product.model.Product -import java.util.UUID - -interface ProductPort { - fun getById(id: UUID): Product? - - fun getAll(): List - - fun save(product: Product) - - fun deleteById(id: UUID) -} \ No newline at end of file diff --git a/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/ProductController.kt b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/ProductController.kt index 8f91ed4..0b8ad68 100644 --- a/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/ProductController.kt +++ b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/ProductController.kt @@ -1,16 +1,28 @@ package com.example.demo.exposition.product -import com.example.demo.application.product.ProductUseCases +import com.example.demo.domain.core.error.FunctionalError +import com.example.demo.domain.product.inputport.ProductInputPort +import com.example.demo.domain.product.model.Product +import com.example.demo.exposition.product.model.ProductCreationRequest import com.example.demo.exposition.product.model.ProductDto +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.map import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/products") class ProductController( - private val productUseCases: ProductUseCases + private val productInputPort: ProductInputPort ) { @GetMapping - fun getAll(): List = productUseCases.getAll().map(::ProductDto) + suspend fun getAll(): Result, FunctionalError> = productInputPort.getAll() + .map { products -> products.map(::ProductDto) } + + @PostMapping + suspend fun create(@RequestBody request: ProductCreationRequest): Result = + productInputPort.create(request.name, request.type) } \ No newline at end of file diff --git a/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/core/advice/ResultControllerAdvice.kt b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/core/advice/ResultControllerAdvice.kt new file mode 100644 index 0000000..36bc9f3 --- /dev/null +++ b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/core/advice/ResultControllerAdvice.kt @@ -0,0 +1,8 @@ +package com.example.demo.exposition.product.core.advice + +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class ResultControllerAdvice { + +} \ No newline at end of file diff --git a/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/model/ProductCreationRequest.kt b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/model/ProductCreationRequest.kt new file mode 100644 index 0000000..b098d1c --- /dev/null +++ b/demo-exposition/src/main/kotlin/com/example/demo/exposition/product/model/ProductCreationRequest.kt @@ -0,0 +1,8 @@ +package com.example.demo.exposition.product.model + +import com.example.demo.domain.product.model.ProductType + +data class ProductCreationRequest( + val name: String, + val type: ProductType +) diff --git a/demo-infrastructure/src/main/kotlin/com/example/demo/infrastructure/product/ProductInMemoryAdapter.kt b/demo-infrastructure/src/main/kotlin/com/example/demo/infrastructure/product/ProductInMemoryAdapter.kt index df007f1..33f4ac6 100644 --- a/demo-infrastructure/src/main/kotlin/com/example/demo/infrastructure/product/ProductInMemoryAdapter.kt +++ b/demo-infrastructure/src/main/kotlin/com/example/demo/infrastructure/product/ProductInMemoryAdapter.kt @@ -1,14 +1,17 @@ package com.example.demo.infrastructure.product +import com.example.demo.domain.core.error.TechnicalError import com.example.demo.domain.product.model.Product import com.example.demo.domain.product.model.ProductType.LIQUID import com.example.demo.domain.product.model.ProductType.SOLID -import com.example.demo.domain.product.port.ProductPort +import com.example.demo.domain.product.outputport.ProductOutputPort +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result import org.springframework.stereotype.Component import java.util.* @Component -class ProductInMemoryAdapter : ProductPort { +class ProductInMemoryAdapter : ProductOutputPort { private val products = mutableListOf( Product( id = UUID.randomUUID(), @@ -24,15 +27,11 @@ class ProductInMemoryAdapter : ProductPort { override fun getById(id: UUID): Product? = products.find { product -> product.id == id } - override fun getAll(): List = products + override suspend fun getAll(): Result, TechnicalError> = Ok(products) - override fun save(product: Product) { - if (getById(product.id) == null) { - products.add(product) - } else { - deleteById(product.id) - save(product) - } + override fun save(product: Product): Result { + products += product + return Ok(Unit) } override fun deleteById(id: UUID) {