Ajout de la vérification des ingrédients des mélanges

This commit is contained in:
William Nolin 2021-04-18 19:54:28 -04:00
parent 9502ae2220
commit 931494c1f9
6 changed files with 129 additions and 19 deletions

1
.gitignore vendored
View File

@ -11,5 +11,6 @@ logs/
data/
dokka/
dist/
out/
/src/main/resources/angular/static/*

View File

@ -1,8 +1,12 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated
import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository> {
@ -18,7 +22,12 @@ interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository>
/** Updates the [quantity] of the given [mixMaterial]. */
fun updateQuantity(mixMaterial: MixMaterial, quantity: Float): MixMaterial
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
/**
* Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set.
* There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages.
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
*/
fun validateMixMaterials(mixMaterials: Set<MixMaterial>)
}
@Service
@ -44,4 +53,90 @@ class MixMaterialServiceImpl(
update(mixMaterial.apply {
this.quantity = quantity
})
override fun validateMixMaterials(mixMaterials: Set<MixMaterial>) {
if (mixMaterials.isEmpty()) return
val sortedMixMaterials = mixMaterials.sortedBy { it.position }
val firstMixMaterial = sortedMixMaterials[0]
val errors = mutableSetOf<InvalidMixMaterialsPositionsError>()
// Check if the first mix material position is 1
fun isFirstMixMaterialPositionInvalid() =
sortedMixMaterials[0].position != 1
// Check if the first mix material is expressed in percents
fun isFirstMixMaterialPercentages() =
sortedMixMaterials[0].material.materialType!!.usePercentages
// Check if any positions is duplicated
fun getDuplicatedPositionsErrors() =
sortedMixMaterials
.findDuplicated { it.position }
.map { duplicatedMixMaterialsPositions(it) }
// Find all errors and throw if there is any
if (isFirstMixMaterialPositionInvalid()) errors += invalidFirstMixMaterialPosition(sortedMixMaterials[0])
errors += getDuplicatedPositionsErrors()
if (errors.isEmpty() && mixMaterials.hasGaps { it.position }) errors += gapBetweenStepsPositions()
if (errors.isNotEmpty()) {
throw InvalidMixMaterialsPositionsException(errors)
}
if (isFirstMixMaterialPercentages()) {
throw InvalidFirstMixMaterial(firstMixMaterial)
}
}
}
class InvalidMixMaterialsPositionsError(
val type: String,
val details: String
)
class InvalidMixMaterialsPositionsException(
val errors: Set<InvalidMixMaterialsPositionsError>
) : RestException(
"invalid-mixmaterial-position",
"Invalid mix materials positions",
HttpStatus.BAD_REQUEST,
"The position of mix materials are invalid",
mapOf(
"invalidMixMaterials" to errors
)
)
class InvalidFirstMixMaterial(
val mixMaterial: MixMaterial
) : RestException(
"invalid-mixmaterial-first",
"Invalid first mix material",
HttpStatus.BAD_REQUEST,
"The first mix material is invalid because its material must not be expressed in percents",
mapOf(
"mixMaterial" to mixMaterial
)
)
const val INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE = "first"
const val DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE = "duplicated"
const val GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE = "gap"
private fun invalidFirstMixMaterialPosition(mixMaterial: MixMaterial) =
InvalidMixMaterialsPositionsError(
INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE,
"The position ${mixMaterial.position} is under the minimum of 1"
)
private fun duplicatedMixMaterialsPositions(position: Int) =
InvalidMixMaterialsPositionsError(
DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE,
"The position $position is duplicated"
)
private fun gapBetweenStepsPositions() =
InvalidMixMaterialsPositionsError(
GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE,
"There is a gap between mix materials positions"
)

View File

@ -3,6 +3,8 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated
import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
@ -36,6 +38,8 @@ class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
}
override fun validateSteps(steps: Set<RecipeStep>) {
if (steps.isEmpty()) return
val sortedSteps = steps.sortedBy { it.position }
val errors = mutableSetOf<InvalidStepsPositionsError>()
@ -46,21 +50,13 @@ class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
// Check if any position is duplicated
fun getDuplicatedPositionsErrors() =
sortedSteps
.groupBy { it.position }
.filter { it.value.count() > 1 }
.map { duplicatedStepsPositions(it.key) }
// Check if there is any gap between steps positions
// Knowing that steps are sorted by position, if the position of the step is not index + 1, there is a gap between them
fun hasGapBetweenPositions() =
sortedSteps
.filterIndexed { index, step -> step.position != index + 1 }
.isNotEmpty()
.findDuplicated { it.position }
.map { duplicatedStepsPositions(it) }
// Find all errors and throw if there is any
if (isFirstStepPositionInvalid()) errors += invalidFirstStepPosition(sortedSteps[0])
errors += getDuplicatedPositionsErrors()
if (errors.isEmpty() && hasGapBetweenPositions()) errors += gapBetweenStepsPositions()
if (errors.isEmpty() && steps.hasGaps { it.position }) errors += gapBetweenStepsPositions()
if (errors.isNotEmpty()) {
throw InvalidStepsPositionsException(errors)
}
@ -89,7 +85,7 @@ class InvalidGroupStepsPositionsException(
val exception: InvalidStepsPositionsException
) : RestException(
"invalid-groupinformation-recipestep-position",
"Invalid steps position",
"Invalid steps positions",
HttpStatus.BAD_REQUEST,
"The position of steps for the group ${group.name} are invalid",
mapOf(
@ -97,7 +93,10 @@ class InvalidGroupStepsPositionsException(
"groupId" to group.id!!,
"invalidSteps" to exception.errors
)
)
) {
val errors: Set<InvalidStepsPositionsError>
get() = exception.errors
}
const val INVALID_FIRST_STEP_POSITION_ERROR_CODE = "first"
const val DUPLICATED_STEPS_POSITIONS_ERROR_CODE = "duplicated"

View File

@ -17,6 +17,20 @@ inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
}
}
/** Find duplicated in the given [Iterable] from keys obtained from the given [keySelector]. */
inline fun <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) =
this.groupBy(keySelector)
.filter { it.value.count() > 1 }
.map { it.key }
/** Check if the given [Iterable] has gaps between each items, using keys obtained from the given [keySelector]. */
inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
this.map(keySelector)
.toIntArray()
.sorted()
.filterIndexed { index, it -> it != index + 1 }
.isNotEmpty()
/** Clears and fills the given [MutableCollection] with the given [elements]. */
fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
this.clear()

View File

@ -20,7 +20,8 @@ class RecipeServiceTest :
private val companyService: CompanyService = mock()
private val mixService: MixService = mock()
private val groupService: EmployeeGroupService = mock()
override val service: RecipeService = spy(RecipeServiceImpl(repository, companyService, mixService, groupService))
private val recipeStepService: RecipeStepService = mock()
override val service: RecipeService = spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService))
private val company: Company = company(id = 0L)
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)

View File

@ -16,10 +16,10 @@ class RecipeStepServiceTest :
override val entity: RecipeStep = recipeStep(id = 0L, message = "message")
override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message")
// validateStepsCollection()
// validateGroupInformationSteps()
@Test
fun `validateStepsCollection() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() {
fun `validateGroupInformationSteps() throws an InvalidGroupStepsPositionsException when the position of the first step of the given groupInformation is not 1`() {
withSteps(
mutableSetOf(
recipeStep(id = 0L, position = 0),
@ -38,7 +38,7 @@ class RecipeStepServiceTest :
}
@Test
fun `validateStepsCollection() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() {
fun `validateGroupInformationSteps() throws an InvalidGroupStepsPositionsException when steps positions are duplicated in the given groupInformation`() {
withSteps(
mutableSetOf(
recipeStep(id = 0L, position = 1),
@ -57,7 +57,7 @@ class RecipeStepServiceTest :
}
@Test
fun `validateStepsCollection() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() {
fun `validateGroupInformationSteps() throws an InvalidGroupStepsPositionsException when there is a gap between steps positions in the given groupInformation`() {
withSteps(
mutableSetOf(
recipeStep(id = 0L, position = 1),