Skip to content

Commit

Permalink
feat: better validation (now it returns multiple CreationFailures)
Browse files Browse the repository at this point in the history
  • Loading branch information
y9vad9 committed Mar 16, 2024
1 parent 5eadfbc commit 065edf1
Show file tree
Hide file tree
Showing 32 changed files with 432 additions and 247 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package org.timemates.sdk.authorization.email.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.CreationFailure
import org.timemates.sdk.common.constructor.factory
import org.timemates.sdk.common.constructor.rules.ValidationRule
import org.timemates.sdk.common.constructor.rules.lengthExact
import kotlin.jvm.JvmInline

@JvmInline
public value class VerificationHash private constructor(public val string: String) {
public companion object : Factory<VerificationHash, String>() {
public const val SIZE: Int = 128

override fun create(input: String): Result<VerificationHash> {
return when (input.length) {
SIZE -> Result.success(VerificationHash(input))
else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
}
}
}
public companion object : Factory<VerificationHash, String> by factory(
rules = listOf(
ValidationRule.lengthExact(128),
),
constructor = ::VerificationHash
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.timemates.sdk.authorization.sessions.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import kotlin.jvm.JvmInline

@JvmInline
public value class ApplicationName private constructor(public val string: String) {
public companion object : Factory<ApplicationName, String>() {
override fun create(input: String): Result<ApplicationName> {
return Result.success(ApplicationName(input))
}
}
public companion object : Factory<ApplicationName, String> by factory(::ApplicationName)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.timemates.sdk.authorization.sessions.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import kotlin.jvm.JvmInline

@JvmInline
public value class AuthorizationId private constructor(public val int: Int) {
public companion object : Factory<AuthorizationId, Int>() {
override fun create(input: Int): Result<AuthorizationId> {
return Result.success(AuthorizationId(input))
}
}
public companion object : Factory<AuthorizationId, Int> by factory(::AuthorizationId)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.timemates.sdk.authorization.sessions.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import kotlin.jvm.JvmInline

@JvmInline
public value class ClientIpAddress private constructor(public val string: String) {
public companion object : Factory<ClientIpAddress, String>() {
override fun create(input: String): Result<ClientIpAddress> {
return Result.success(ClientIpAddress(input))
}
}
public companion object : Factory<ClientIpAddress, String> by factory(::ClientIpAddress)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.timemates.sdk.authorization.sessions.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import kotlin.jvm.JvmInline

@JvmInline
public value class ClientVersion private constructor(public val double: Double) {
public companion object : Factory<ClientVersion, Double>() {
override fun create(input: Double): Result<ClientVersion> {
return Result.success(ClientVersion(input))
}
}
public companion object : Factory<ClientVersion, Double> by factory(::ClientVersion)
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package org.timemates.sdk.authorization.sessions.types.value

import org.timemates.sdk.common.constructor.CreationFailure
import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import org.timemates.sdk.common.constructor.rules.ValidationRule
import org.timemates.sdk.common.constructor.rules.lengthExact
import org.timemates.sdk.common.constructor.rules.notBlank
import kotlin.jvm.JvmInline

@JvmInline
public value class ConfirmationCode private constructor(public val string: String) {
public companion object : Factory<ConfirmationCode, String>() {
public const val SIZE: Int = 8

override fun create(input: String): Result<ConfirmationCode> {
return when {
input.isBlank() -> Result.failure(CreationFailure.ofBlank())
input.length != SIZE -> Result.failure(CreationFailure.ofSizeExact(SIZE))
else -> Result.success(ConfirmationCode(input))
}
}
}
public companion object : Factory<ConfirmationCode, String> by factory(
rules = listOf(
ValidationRule.notBlank(),
ValidationRule.lengthExact(8),
),
constructor = ::ConfirmationCode,
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package org.timemates.sdk.authorization.types.value

import org.timemates.sdk.common.constructor.Factory
import org.timemates.sdk.common.constructor.factory
import kotlin.jvm.JvmInline

@JvmInline
public value class HashValue private constructor(public val string: String) {
public companion object : Factory<HashValue, String>() {
override fun create(input: String): Result<HashValue> {
return Result.success(HashValue(input))
}
}
public companion object : Factory<HashValue, String> by factory(
constructor = ::HashValue,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

package org.timemates.sdk.common.constructor

import org.timemates.sdk.common.exceptions.TimeMatesException
import org.timemates.sdk.common.types.TimeMatesEntity

/**
* Represents a failure that occurs during the creation of an object.
Expand All @@ -12,51 +12,77 @@ import org.timemates.sdk.common.exceptions.TimeMatesException
*
* @property message The error message associated with the creation failure.
*/
public sealed class CreationFailure(message: String) : TimeMatesException(message) {
public sealed class CreationFailure(
public val message: String,
) : TimeMatesEntity() {
/**
* Represents a creation failure due to a size range constraint.
*/
public data class SizeRangeFailure(public val range: IntRange) : CreationFailure("Constraint failure: size must be in range of $range")
public data class LengthRangeFailure(public val range: IntRange) : CreationFailure("Length must be in range of $range")

/**
* Represents a creation failure due to an exact size constraint.
*/
public data class SizeExactFailure(public val size: Int) : CreationFailure("Constraint failure: size must be exactly $size")
public data class LengthExactFailure(public val size: Int) : CreationFailure("Length must be exactly $size")

/**
* Represents a creation failure due to a minimum value constraint.
*/
public data class MinValueFailure(public val size: Int) : CreationFailure("Constraint failure: minimal value is $size")
public data class MinValueFailure<T>(
public val size: T,
) : CreationFailure("Minimal value is $size")
where T : Number, T : Comparable<T>

/**
* Represents a creation failure due to a invalid value range.
*/
public data class ValueRangeFailure<T>(
public val range: ClosedRange<T>,
) : CreationFailure("Value should be in range $range.")
where T : Number, T : Comparable<T>

/**
* Represents a creation failure due to a blank value constraint.
*/
public class BlankValueFailure : CreationFailure("Constraint failure: provided value is empty")
public class BlankValueFailure : CreationFailure("Provided value is empty")

/**
* Represents a creation failure due to a pattern constraint.
*/
public data class PatternFailure(public val regex: Regex) : CreationFailure("Constraint failure: input should match $regex")
public data class PatternFailure(public val regex: Regex) : CreationFailure("Input should match $regex")

public data class CompoundFailure(
public val failures: List<CreationFailure>,
) : CreationFailure(
"Multiple validation was failed: \n " +
failures.withIndex().joinToString("\n") { "${it.index + 1}. ${it.value.message}" }
)

public companion object {
public fun <T> ofValueRange(
range: ClosedRange<T>,
): CreationFailure where T : Number, T : Comparable<T> {
return ValueRangeFailure(range)
}

/**
* Creates a [SizeRangeFailure] object with a size constraint failure message.
* Creates a [LengthRangeFailure] object with a size constraint failure message.
*
* @param size The size range constraint for the creation failure.
* @return The [SizeRangeFailure] object with the specified size constraint failure message.
* @return The [LengthRangeFailure] object with the specified size constraint failure message.
*/
public fun ofSizeRange(size: IntRange): CreationFailure {
return SizeRangeFailure(size)
public fun ofLengthRange(size: IntRange): CreationFailure {
return LengthRangeFailure(size)
}

/**
* Creates a [SizeExactFailure] with a constraint failure message based on the provided size.
* Creates a [LengthExactFailure] with a constraint failure message based on the provided size.
*
* @param size The expected size that caused the constraint failure.
* @return A [SizeExactFailure] object with the constraint failure message.
* @return A [LengthExactFailure] object with the constraint failure message.
*/
public fun ofSizeExact(size: Int): CreationFailure {
return SizeExactFailure(size)
public fun ofLengthExact(size: Int): CreationFailure {
return LengthExactFailure(size)
}

/**
Expand All @@ -65,7 +91,7 @@ public sealed class CreationFailure(message: String) : TimeMatesException(messag
* @param size The minimal value that caused the constraint failure.
* @return A [MinValueFailure] object with the constraint failure message.
*/
public fun ofMin(size: Int): CreationFailure {
public fun <T> ofMin(size: T): CreationFailure where T : Number, T : Comparable<T> {
return MinValueFailure(size)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,87 @@
package org.timemates.sdk.common.constructor

import org.timemates.sdk.common.constructor.rules.ValidationRule
import org.timemates.sdk.common.constructor.results.SafeCreationResult
import org.timemates.sdk.common.constructor.results.ValidationResult
import org.timemates.sdk.common.constructor.results.getUnsafe

/**
* Represents a generic constructor class for creating objects of type [Output] from input of type [Input].
* Represents a generic constructor class for creating objects of type [TBoxed] from TRaw of type [TRaw].
*
* This class is abstract and provides a template for creating objects. Even if there's no need
* in validating, we should follow our code-style and provide only [Factory] API.
*
* Primary and secondary constructors should be private and use only [Factory] as public API.
*
* @param Output The type of object to be created.
* @param Input The type of input used to create the object.
* @param TBoxed The type of object to be created.
* @param TRaw The type of TRaw used to create the object.
*/
public abstract class Factory<Output, Input> {
public interface Factory<TBoxed, TRaw> {
public val rules: List<ValidationRule<TRaw>>


/**
* Instantiates the entity of given type [Output].
*
* **Shouldn't throw anything, but instantiate object of type [Output]**
* Creates [TBoxed] from [TRaw] in the safe way by validating
* [rules] that are defined in the factory.
*/
public abstract fun create(input: Input): Result<Output>
public fun createSafe(value: TRaw): SafeCreationResult<TRaw, TBoxed>
}

public fun <TBoxed, TRaw> factory(
rules: List<ValidationRule<TRaw>>,
constructor: (TRaw) -> TBoxed,
): Factory<TBoxed, TRaw> {
return object : Factory<TBoxed, TRaw> {
override val rules: List<ValidationRule<TRaw>> by ::rules
override fun createSafe(value: TRaw): SafeCreationResult<TRaw, TBoxed> {
val failures = rules.mapNotNull { rule ->
(rule.validate(value) as? ValidationResult.Invalid)?.failure
}

return if (failures.any())
SafeCreationResult.Invalid(value, failures)
else SafeCreationResult.Valid(value, constructor(value))
}
}
}

public fun <TBoxed, TRaw> factory(
constructor: (TRaw) -> TBoxed,
): Factory<TBoxed, TRaw> {
return object : Factory<TBoxed, TRaw> {
override val rules: List<ValidationRule<TRaw>> get() = emptyList()
override fun createSafe(value: TRaw): SafeCreationResult<TRaw, TBoxed> {
return SafeCreationResult.Valid(value, constructor(value))
}
}
}

/**
* Creates an instance of the specified [Output] type using the provided [input].
* Creates an instance of the specified [TBoxed] type using the provided [TRaw].
*
* This function attempts to instantiate an entity of the given type [Output] based on the provided [input].
* This function attempts to instantiate an entity of the given type [TBoxed] based on the provided [TRaw].
* It returns the instantiated entity if the operation is successful, or throws an exception if an error occurs
* during the instantiation process.
*
* @param input The input required for the entity creation.
* @return The instantiated entity of type [Output].
* @param TRaw The TRaw required for the entity creation.
* @return The instantiated entity of type [TBoxed].
* @throws Throwable if an error occurs during the entity creation process.
*/
public fun <Output, Input> Factory<Output, Input>.createOrThrow(input: Input): Output {
val result = create(input)

return if(result.isSuccess) {
result.getOrThrow()
} else {
throw result.exceptionOrNull() ?: error("Failed to create an object.")
}
public fun <TBoxed, TRaw> Factory<TBoxed, TRaw>.createOrThrow(value: TRaw): TBoxed {
return createSafe(value).getUnsafe()
}

/**
* Creates an instance of the specified [Output] type using the provided [input].
* Creates an instance of the specified [TBoxed] type using the provided [TRaw].
*
* This function attempts to instantiate an entity of the given type [Output] based on the provided [input].
* This function attempts to instantiate an entity of the given type [TBoxed] based on the provided [TRaw].
* It returns the instantiated entity if the operation is successful, or returns `null` if an error occurs
* during the instantiation process.
*
* @param input The input required for the entity creation.
* @return The instantiated entity of type [Output], or `null` if an error occurs during the entity creation process.
* @param TRaw The TRaw required for the entity creation.
* @return The instantiated entity of type [TBoxed], or `null` if an error occurs during the entity creation process.
*/
public fun <Output, Input> Factory<Output, Input>.createOrNull(input: Input): Output? {
return create(input).getOrNull()
}
public fun <TBoxed, TRaw> Factory<TBoxed, TRaw>.createOrNull(value: TRaw): TBoxed? {
@Suppress("UNCHECKED_CAST")
return (createSafe(value) as? SafeCreationResult.Valid<*, *>)?.boxed as TBoxed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.timemates.sdk.common.constructor

public data class ValidationException(
val failures: List<CreationFailure>,
) : Exception("The following validation constraints have failed: \n ${failures.joinToString("\n") { "1. ${it.message}" }}")
Loading

0 comments on commit 065edf1

Please sign in to comment.