diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/email/types/value/VerificationHash.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/email/types/value/VerificationHash.kt index 71b54b3..c301c73 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/email/types/value/VerificationHash.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/email/types/value/VerificationHash.kt @@ -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() { - public const val SIZE: Int = 128 - - override fun create(input: String): Result { - return when (input.length) { - SIZE -> Result.success(VerificationHash(input)) - else -> Result.failure(CreationFailure.ofSizeExact(SIZE)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.lengthExact(128), + ), + constructor = ::VerificationHash + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ApplicationName.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ApplicationName.kt index 61818c2..461bbc4 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ApplicationName.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ApplicationName.kt @@ -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() { - override fun create(input: String): Result { - return Result.success(ApplicationName(input)) - } - } + public companion object : Factory by factory(::ApplicationName) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/AuthorizationId.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/AuthorizationId.kt index 874f83b..46508a4 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/AuthorizationId.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/AuthorizationId.kt @@ -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() { - override fun create(input: Int): Result { - return Result.success(AuthorizationId(input)) - } - } + public companion object : Factory by factory(::AuthorizationId) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientIpAddress.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientIpAddress.kt index 6c65b67..7d75e4f 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientIpAddress.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientIpAddress.kt @@ -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() { - override fun create(input: String): Result { - return Result.success(ClientIpAddress(input)) - } - } + public companion object : Factory by factory(::ClientIpAddress) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientVersion.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientVersion.kt index 9f542a5..6685125 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientVersion.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ClientVersion.kt @@ -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() { - override fun create(input: Double): Result { - return Result.success(ClientVersion(input)) - } - } + public companion object : Factory by factory(::ClientVersion) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ConfirmationCode.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ConfirmationCode.kt index b2ead9d..199a024 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ConfirmationCode.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/sessions/types/value/ConfirmationCode.kt @@ -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() { - public const val SIZE: Int = 8 - - override fun create(input: String): Result { - 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 by factory( + rules = listOf( + ValidationRule.notBlank(), + ValidationRule.lengthExact(8), + ), + constructor = ::ConfirmationCode, + ) } diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/types/value/HashValue.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/types/value/HashValue.kt index e69582d..dbeca81 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/types/value/HashValue.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/authorization/types/value/HashValue.kt @@ -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() { - override fun create(input: String): Result { - return Result.success(HashValue(input)) - } - } + public companion object : Factory by factory( + constructor = ::HashValue, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/CreationFailure.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/CreationFailure.kt index cbf255e..0ef8860 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/CreationFailure.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/CreationFailure.kt @@ -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. @@ -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( + public val size: T, + ) : CreationFailure("Minimal value is $size") + where T : Number, T : Comparable + + /** + * Represents a creation failure due to a invalid value range. + */ + public data class ValueRangeFailure( + public val range: ClosedRange, + ) : CreationFailure("Value should be in range $range.") + where T : Number, T : Comparable /** * 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( + "Multiple validation was failed: \n " + + failures.withIndex().joinToString("\n") { "${it.index + 1}. ${it.value.message}" } + ) public companion object { + public fun ofValueRange( + range: ClosedRange, + ): CreationFailure where T : Number, T : Comparable { + 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) } /** @@ -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 ofMin(size: T): CreationFailure where T : Number, T : Comparable { return MinValueFailure(size) } diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/Factory.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/Factory.kt index d177652..dbb4731 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/Factory.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/Factory.kt @@ -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 { +public interface Factory { + public val rules: List> + + /** - * 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 + public fun createSafe(value: TRaw): SafeCreationResult +} + +public fun factory( + rules: List>, + constructor: (TRaw) -> TBoxed, +): Factory { + return object : Factory { + override val rules: List> by ::rules + override fun createSafe(value: TRaw): SafeCreationResult { + 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 factory( + constructor: (TRaw) -> TBoxed, +): Factory { + return object : Factory { + override val rules: List> get() = emptyList() + override fun createSafe(value: TRaw): SafeCreationResult { + 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 Factory.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 Factory.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 Factory.createOrNull(input: Input): Output? { - return create(input).getOrNull() -} +public fun Factory.createOrNull(value: TRaw): TBoxed? { + @Suppress("UNCHECKED_CAST") + return (createSafe(value) as? SafeCreationResult.Valid<*, *>)?.boxed as TBoxed +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/ValidationException.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/ValidationException.kt new file mode 100644 index 0000000..985da11 --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/ValidationException.kt @@ -0,0 +1,5 @@ +package org.timemates.sdk.common.constructor + +public data class ValidationException( + val failures: List, +) : Exception("The following validation constraints have failed: \n ${failures.joinToString("\n") { "1. ${it.message}" }}") \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/SafeCreationResult.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/SafeCreationResult.kt new file mode 100644 index 0000000..ef04c5c --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/SafeCreationResult.kt @@ -0,0 +1,60 @@ +package org.timemates.sdk.common.constructor.results + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.ValidationException +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +public sealed interface SafeCreationResult { + public val value: TRaw + + public class Valid internal constructor( + public override val value: TRaw, + public val boxed: TBoxed, + ) : SafeCreationResult + + public class Invalid internal constructor( + public override val value: TRaw, + public val failures: List, + ) : SafeCreationResult { + public constructor(value: TRaw, failure: CreationFailure) : this(value, listOf(failure)) + } +} + +@OptIn(ExperimentalContracts::class) +public fun SafeCreationResult<*, *>.isValid(): Boolean { + contract { + returns(true) implies (this@isValid is SafeCreationResult.Valid<*, *>) + returns(false) implies (this@isValid is SafeCreationResult.Invalid) + } + + return this is SafeCreationResult.Valid +} + +@OptIn(ExperimentalContracts::class) +public fun SafeCreationResult<*, *>.isInvalid(): Boolean { + contract { + returns(true) implies (this@isInvalid is SafeCreationResult.Invalid) + returns(false) implies (this@isInvalid is SafeCreationResult.Valid<*, *>) + } + + return this is SafeCreationResult.Invalid<*> +} + +public fun SafeCreationResult.getUnsafe(): TBoxed { + return if (isValid()) { + @Suppress("UNCHECKED_CAST") + boxed as TBoxed + } else { + throw ValidationException(failures) + } +} + +public fun SafeCreationResult.toResult(): Result = runCatching { + getUnsafe() +} + +public val SafeCreationResult<*, *>.failuresIfPresent: List + get() { + return if (this is SafeCreationResult.Invalid) failures else emptyList() + } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/ValidationResult.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/ValidationResult.kt new file mode 100644 index 0000000..42b63c5 --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/results/ValidationResult.kt @@ -0,0 +1,11 @@ +package org.timemates.sdk.common.constructor.results + +import org.timemates.sdk.common.constructor.CreationFailure +import kotlin.jvm.JvmInline + +public sealed interface ValidationResult { + public data object Valid : ValidationResult + + @JvmInline + public value class Invalid(public val failure: CreationFailure) : ValidationResult +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthRangeValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthRangeValidationRule.kt new file mode 100644 index 0000000..ed4783e --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthRangeValidationRule.kt @@ -0,0 +1,18 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public class LengthRangeValidationRule( + private val range: IntRange, +) : ValidationRule { + override fun validate(value: String): ValidationResult { + return if (value.length in range) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofLengthRange(range)) + } +} + +public fun ValidationRule.Companion.lengthRange(range: IntRange): LengthRangeValidationRule { + return LengthRangeValidationRule(range) +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthValidationRule.kt new file mode 100644 index 0000000..c9d5571 --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/LengthValidationRule.kt @@ -0,0 +1,18 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public class LengthValidationRule( + private val requiredLength: Int, +) : ValidationRule { + override fun validate(value: String): ValidationResult { + return if (value.length == requiredLength) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofLengthExact(requiredLength)) + } +} + +public fun ValidationRule.Companion.lengthExact(requiredLength: Int): LengthValidationRule { + return LengthValidationRule(requiredLength) +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/MinValueValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/MinValueValidationRule.kt new file mode 100644 index 0000000..4fc7cb3 --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/MinValueValidationRule.kt @@ -0,0 +1,20 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public class MinValueValidationRule( + private val minValue: T, +) : ValidationRule where T : Number, T : Comparable { + override fun validate(value: T): ValidationResult { + return if (value >= minValue) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofMin(minValue)) + } +} + +public fun ValidationRule.Companion.minValue( + min: T, +): MinValueValidationRule where T : Number, T : Comparable { + return MinValueValidationRule(min) +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/PatternValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/PatternValidationRule.kt new file mode 100644 index 0000000..dc4af9f --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/PatternValidationRule.kt @@ -0,0 +1,18 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public class PatternValidationRule( + private val regex: Regex, +) : ValidationRule { + override fun validate(value: String): ValidationResult { + return if (value.matches(regex)) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofPattern(regex)) + } +} + +public fun ValidationRule.Companion.matchesPattern(regex: Regex): PatternValidationRule { + return PatternValidationRule(regex) +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValidationRule.kt new file mode 100644 index 0000000..6855a9a --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValidationRule.kt @@ -0,0 +1,10 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.results.ValidationResult + +public interface ValidationRule { + public fun validate(value: Input): ValidationResult + + public companion object +} + diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueIsNotBlankRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueIsNotBlankRule.kt new file mode 100644 index 0000000..c233bde --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueIsNotBlankRule.kt @@ -0,0 +1,16 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public object ValueIsNotBlankRule : ValidationRule { + override fun validate(value: String): ValidationResult { + return if (value.isNotBlank()) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofBlank()) + } +} + +public fun ValidationRule.Companion.notBlank(): ValueIsNotBlankRule { + return ValueIsNotBlankRule +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueRangeValidationRule.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueRangeValidationRule.kt new file mode 100644 index 0000000..7ebb60e --- /dev/null +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/constructor/rules/ValueRangeValidationRule.kt @@ -0,0 +1,14 @@ +package org.timemates.sdk.common.constructor.rules + +import org.timemates.sdk.common.constructor.CreationFailure +import org.timemates.sdk.common.constructor.results.ValidationResult + +public class ValueRangeValidationRule( + private val range: ClosedRange, +) : ValidationRule where T : Number, T : Comparable { + override fun validate(value: T): ValidationResult { + return if (value in range) + ValidationResult.Valid + else ValidationResult.Invalid(CreationFailure.ofValueRange(range)) + } +} \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PageToken.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PageToken.kt index b09cf02..1801ca7 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PageToken.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PageToken.kt @@ -1,6 +1,7 @@ package org.timemates.sdk.common.pagination import org.timemates.sdk.common.constructor.Factory +import org.timemates.sdk.common.constructor.factory import kotlin.jvm.JvmInline /** @@ -12,9 +13,5 @@ import kotlin.jvm.JvmInline */ @JvmInline public value class PageToken private constructor(public val string: String) { - public companion object : Factory() { - override fun create(input: String): Result { - return Result.success(PageToken(input)) - } - } + public companion object : Factory by factory(::PageToken) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PagesIterator.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PagesIterator.kt index 5c42c70..d7aa676 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PagesIterator.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/pagination/PagesIterator.kt @@ -1,6 +1,12 @@ package org.timemates.sdk.common.pagination import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.retry import org.timemates.sdk.common.annotations.ApiStatus import org.timemates.sdk.common.annotations.ExperimentalTimeMatesApi import org.timemates.sdk.common.constructor.createOrThrow @@ -8,13 +14,8 @@ import org.timemates.sdk.common.exceptions.TimeMatesException import org.timemates.sdk.common.pagination.PagesIteratorImpl.State import org.timemates.sdk.common.types.TimeMatesEntity import org.timemates.sdk.common.types.value.Count -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.retry import kotlin.time.Duration +import kotlin.coroutines.cancellation.CancellationException as CoreCancellationException /** * Interface representing a page iterator for paginated data retrieval. @@ -39,7 +40,7 @@ public interface PagesIterator { * @return The list of elements in the next page. * @throws NoSuchElementException if there are no more pages available. */ - @Throws(NoSuchElementException::class, CancellationException::class) + @Throws(NoSuchElementException::class, CancellationException::class, CoreCancellationException::class) public suspend operator fun next(): Result> /** diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/types/value/Count.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/types/value/Count.kt index 76f847a..1f724ac 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/common/types/value/Count.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/common/types/value/Count.kt @@ -1,19 +1,15 @@ package org.timemates.sdk.common.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.minValue import kotlin.jvm.JvmInline @JvmInline public value class Count private constructor(public val int: Int) { - public companion object : Factory() { - public const val MIN_VALUE: Int = 0 - - override fun create(input: Int): Result { - return when { - input < MIN_VALUE -> Result.failure(CreationFailure.ofMin(MIN_VALUE)) - else -> Result.success(Count(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf(ValidationRule.minValue(0)), + constructor = ::Count, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteCode.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteCode.kt index dcd8862..00b1a9a 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteCode.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteCode.kt @@ -1,19 +1,15 @@ package org.timemates.sdk.timers.members.invites.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 kotlin.jvm.JvmInline @JvmInline public value class InviteCode private constructor(public val string: String) { - public companion object : Factory() { - public const val SIZE: Int = 8 - - override fun create(input: String): Result { - return when { - input.length == SIZE -> Result.success(InviteCode(input)) - else -> Result.failure(CreationFailure.ofSizeExact(SIZE)) - } - } - } + public companion object : Factory by factory( + rules = listOf(ValidationRule.lengthExact(8)), + constructor = ::InviteCode, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteName.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteName.kt index 25e9e39..6796078 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteName.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/members/invites/types/value/InviteName.kt @@ -1,17 +1,17 @@ package org.timemates.sdk.timers.members.invites.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.notBlank import kotlin.jvm.JvmInline @JvmInline public value class InviteName private constructor(public val string: String) { - public companion object : Factory() { - override fun create(input: String): Result { - return when { - input.isBlank() -> Result.failure(CreationFailure.ofBlank()) - else -> Result.success(InviteName(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.notBlank(), + ), + constructor = ::InviteName + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerDescription.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerDescription.kt index 8b46794..de1f54f 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerDescription.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerDescription.kt @@ -1,22 +1,17 @@ package org.timemates.sdk.timers.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.lengthRange import kotlin.jvm.JvmInline @JvmInline public value class TimerDescription private constructor(public val string: String) { - public companion object : Factory() { - /** - * Size range of the timer's name. - */ - public val SIZE_RANGE: IntRange = 0..500 - - override fun create(input: String): Result { - return when (input.length) { - !in SIZE_RANGE -> Result.failure(CreationFailure.ofSizeRange(SIZE_RANGE)) - else -> Result.success(TimerDescription(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.lengthRange(0..500), + ), + constructor = ::TimerDescription, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerId.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerId.kt index 03b64d0..289343f 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerId.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerId.kt @@ -1,17 +1,17 @@ package org.timemates.sdk.timers.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.minValue import kotlin.jvm.JvmInline @JvmInline public value class TimerId private constructor(public val long: Long) { - public companion object : Factory() { - override fun create(input: Long): Result { - return when { - input < 0 -> Result.failure(CreationFailure.ofMin(0)) - else -> Result.success(TimerId(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.minValue(0), + ), + constructor = ::TimerId, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerName.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerName.kt index 39a0448..974c440 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerName.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/timers/types/value/TimerName.kt @@ -1,22 +1,17 @@ package org.timemates.sdk.timers.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.lengthRange import kotlin.jvm.JvmInline @JvmInline public value class TimerName private constructor(public val string: String) { - public companion object : Factory() { - /** - * Size range of the timer's name. - */ - public val SIZE_RANGE: IntRange = 3..50 - - override fun create(input: String): Result { - return when (input.length) { - !in SIZE_RANGE -> Result.failure(CreationFailure.ofSizeRange(SIZE_RANGE)) - else -> Result.success(TimerName(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.lengthRange(3..50), + ), + constructor = ::TimerName, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/Avatar.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/Avatar.kt index 1e225bf..3517273 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/Avatar.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/Avatar.kt @@ -1,19 +1,17 @@ package org.timemates.sdk.users.profile.types -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.notBlank import kotlin.jvm.JvmInline public sealed interface Avatar { @JvmInline public value class GravatarId private constructor(public val string: String) : Avatar { - public companion object : Factory() { - override fun create(input: String): Result { - return when { - input.isBlank() -> Result.failure(CreationFailure.ofBlank()) - else -> Result.success(GravatarId(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf(ValidationRule.notBlank()), + constructor = ::GravatarId, + ) } } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/EmailAddress.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/EmailAddress.kt index 753c75d..219c458 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/EmailAddress.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/EmailAddress.kt @@ -1,33 +1,29 @@ package org.timemates.sdk.users.profile.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.lengthRange +import org.timemates.sdk.common.constructor.rules.matchesPattern import kotlin.jvm.JvmInline @JvmInline public value class EmailAddress private constructor(public val string: String) { - public companion object : Factory() { - public val SIZE_RANGE: IntRange = 5..200 + public companion object : Factory by factory( + rules = listOf( + ValidationRule.lengthRange(5..200), + ValidationRule.matchesPattern(EMAIL_PATTERN), + ), + constructor = ::EmailAddress, + ) +} - public val PATTERN: Regex = Regex( - "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + - "\\@" + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + - "(" + - "\\." + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + - ")+" - ) - - override fun create(input: String): Result { - return when { - input.length !in SIZE_RANGE -> - Result.failure(CreationFailure.ofSizeRange(SIZE_RANGE)) - !PATTERN.matches(input) -> - Result.failure(CreationFailure.ofPattern(PATTERN)) - - else -> Result.success(EmailAddress(input)) - } - } - } -} \ No newline at end of file +private val EMAIL_PATTERN: Regex = Regex( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" +) \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserDescription.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserDescription.kt index c4a06a4..8c4d02d 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserDescription.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserDescription.kt @@ -1,24 +1,17 @@ package org.timemates.sdk.users.profile.types.value -import org.timemates.sdk.common.constructor.CreationFailure import org.timemates.sdk.common.constructor.Factory -import kotlin.Result.Companion.failure -import kotlin.Result.Companion.success +import org.timemates.sdk.common.constructor.factory +import org.timemates.sdk.common.constructor.rules.ValidationRule +import org.timemates.sdk.common.constructor.rules.lengthRange import kotlin.jvm.JvmInline @JvmInline public value class UserDescription private constructor(public val string: String) { - public companion object : Factory() { - /** - * Size range of the user's short bio. - */ - public val SIZE_RANGE: IntRange = 3..200 - - override fun create(input: String): Result { - return when (input.length) { - !in 0..200 -> failure(CreationFailure.ofSizeRange(SIZE_RANGE)) - else -> success(UserDescription(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf( + ValidationRule.lengthRange(3..200), + ), + constructor = ::UserDescription, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserId.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserId.kt index 335116a..c1c0e96 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserId.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserId.kt @@ -1,20 +1,15 @@ package org.timemates.sdk.users.profile.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.minValue import kotlin.jvm.JvmInline @JvmInline public value class UserId private constructor(public val long: Long) { - public companion object : Factory() { - public const val MIN_VALUE: Int = 0 - - override fun create(input: Long): Result { - return when { - input < MIN_VALUE -> Result.failure(CreationFailure.ofMin(MIN_VALUE)) - else -> Result.success(UserId(input)) - } - } - - } + public companion object : Factory by factory( + rules = listOf(ValidationRule.minValue(0)), + constructor = ::UserId, + ) } \ No newline at end of file diff --git a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserName.kt b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserName.kt index fb09621..9cb0c52 100644 --- a/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserName.kt +++ b/sdk/src/commonMain/kotlin/org/timemates/sdk/users/profile/types/value/UserName.kt @@ -1,22 +1,15 @@ package org.timemates.sdk.users.profile.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.lengthRange import kotlin.jvm.JvmInline @JvmInline public value class UserName private constructor(public val string: String) { - public companion object : Factory() { - /** - * Size range of the user's name. - */ - public val SIZE_RANGE: IntRange = 3..50 - - override fun create(input: String): Result { - return when (input.length) { - !in SIZE_RANGE -> Result.failure(CreationFailure.ofSizeRange(SIZE_RANGE)) - else -> Result.success(UserName(input)) - } - } - } + public companion object : Factory by factory( + rules = listOf(ValidationRule.lengthRange(3..50)), + constructor = ::UserName, + ) } \ No newline at end of file