Compare commits

..

2 Commits

Author SHA1 Message Date
Florian THIERRY
f98e1227e8 Implementation of getAll and create products endpoints. 2025-04-23 23:13:48 +02:00
Florian THIERRY
ed0acfc5dc Enable coroutines. 2025-04-23 21:56:14 +02:00
19 changed files with 153 additions and 43 deletions

View File

@@ -0,0 +1,11 @@
meta {
name: Get all products
type: http
seq: 2
}
get {
url: {{url}}/api/products
body: none
auth: none
}

View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "GiteaActionsWorkshop",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -0,0 +1,3 @@
vars {
url: http://localhost:8080
}

View File

@@ -1,13 +1,6 @@
object Versions {
const val springBoot = "3.4.3"
const val springDependencyManagement = "1.1.7"
const val kotlinJvm = "1.9.25"
const val kotlinPluginSpring = "1.9.25"
}
plugins { plugins {
kotlin("jvm") version "1.9.25" kotlin("jvm") version "2.1.20"
kotlin("plugin.spring") version "1.9.25" kotlin("plugin.spring") version "2.1.20"
id("io.spring.dependency-management") version "1.1.7" id("io.spring.dependency-management") version "1.1.7"
} }
@@ -33,13 +26,14 @@ subprojects {
} }
dependencies { dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:${Versions.springBoot}")) implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.4"))
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.1")
} }
} }
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:${Versions.springBoot}") mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.4")
} }
} }

View File

@@ -7,4 +7,6 @@ dependencies {
implementation(kotlin("stdlib")) implementation(kotlin("stdlib"))
implementation(project(":demo-domain")) implementation(project(":demo-domain"))
implementation("org.springframework:spring-context") implementation("org.springframework:spring-context")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
} }

View File

@@ -1,12 +1,45 @@
package com.example.demo.application.product 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.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 org.springframework.stereotype.Service
import java.util.*
@Service @Service
class ProductUseCases( class ProductUseCases(
private val productPort: ProductPort private val productOutputPort: ProductOutputPort
) { ) : ProductInputPort {
fun getAll(): List<Product> = productPort.getAll() override suspend fun getAll(): Result<List<Product>, FunctionalError> = productOutputPort.getAll()
.mapError { FunctionalError(it.message) }
// override suspend fun create(name: String, type: ProductType): Result<Product, FunctionalError> {
// 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<Product, FunctionalError> = 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<Unit, FunctionalError> = when {
name.isBlank() -> Err(FunctionalError("Product name should be set."))
else -> Ok(Unit)
}
} }

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
open class DomainError(val message: String)

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
class FunctionalError(message: String) : DomainError(message)

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
class TechnicalError(message: String) : DomainError(message)

View File

@@ -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<Product, FunctionalError>
suspend fun getAll(): Result<List<Product>, FunctionalError>
}

View File

@@ -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<List<Product>, TechnicalError>
fun save(product: Product): Result<Unit, TechnicalError>
fun deleteById(id: UUID)
}

View File

@@ -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<Product>
fun save(product: Product)
fun deleteById(id: UUID)
}

View File

@@ -9,4 +9,7 @@ dependencies {
implementation(project(":demo-domain")) implementation(project(":demo-domain"))
implementation("org.springframework:spring-context") implementation("org.springframework:spring-context")
implementation("org.springframework:spring-web") implementation("org.springframework:spring-web")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.3")
} }

View File

@@ -1,16 +1,28 @@
package com.example.demo.exposition.product 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.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.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.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/products") @RequestMapping("/api/products")
class ProductController( class ProductController(
private val productUseCases: ProductUseCases private val productInputPort: ProductInputPort
) { ) {
@GetMapping @GetMapping
fun getAll(): List<ProductDto> = productUseCases.getAll().map(::ProductDto) suspend fun getAll(): Result<List<ProductDto>, FunctionalError> = productInputPort.getAll()
.map { products -> products.map(::ProductDto) }
@PostMapping
suspend fun create(@RequestBody request: ProductCreationRequest): Result<Product, FunctionalError> =
productInputPort.create(request.name, request.type)
} }

View File

@@ -0,0 +1,8 @@
package com.example.demo.exposition.product.core.advice
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class ResultControllerAdvice {
}

View File

@@ -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
)

View File

@@ -8,4 +8,6 @@ dependencies {
implementation(project(":demo-application")) implementation(project(":demo-application"))
implementation(project(":demo-domain")) implementation(project(":demo-domain"))
implementation("org.springframework:spring-context") implementation("org.springframework:spring-context")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
} }

View File

@@ -1,14 +1,17 @@
package com.example.demo.infrastructure.product 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.Product
import com.example.demo.domain.product.model.ProductType.LIQUID import com.example.demo.domain.product.model.ProductType.LIQUID
import com.example.demo.domain.product.model.ProductType.SOLID 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 org.springframework.stereotype.Component
import java.util.* import java.util.*
@Component @Component
class ProductInMemoryAdapter : ProductPort { class ProductInMemoryAdapter : ProductOutputPort {
private val products = mutableListOf( private val products = mutableListOf(
Product( Product(
id = UUID.randomUUID(), id = UUID.randomUUID(),
@@ -24,15 +27,11 @@ class ProductInMemoryAdapter : ProductPort {
override fun getById(id: UUID): Product? = products.find { product -> product.id == id } override fun getById(id: UUID): Product? = products.find { product -> product.id == id }
override fun getAll(): List<Product> = products override suspend fun getAll(): Result<List<Product>, TechnicalError> = Ok(products)
override fun save(product: Product) { override fun save(product: Product): Result<Unit, TechnicalError> {
if (getById(product.id) == null) { products += product
products.add(product) return Ok(Unit)
} else {
deleteById(product.id)
save(product)
}
} }
override fun deleteById(id: UUID) { override fun deleteById(id: UUID) {

View File

@@ -6,13 +6,17 @@ plugins {
dependencies { dependencies {
implementation(kotlin("stdlib")) implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation(project(":demo-domain")) implementation(project(":demo-domain"))
implementation(project(":demo-application")) implementation(project(":demo-application"))
implementation(project(":demo-infrastructure")) implementation(project(":demo-infrastructure"))
implementation(project(":demo-exposition")) implementation(project(":demo-exposition"))
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-webflux")
} }
springBoot { springBoot {
mainClass = "com.example.demo.launcher.ApplicationLauncherKt" mainClass = "com.example.demo.launcher.ApplicationLauncherKt"
} }