├── .gitignore ├── framework ├── core │ ├── gradle.properties │ ├── src │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── ahmedmourad │ │ │ │ └── validation │ │ │ │ └── core │ │ │ │ ├── validations │ │ │ │ ├── CaseValidations.kt │ │ │ │ ├── TemporalValidations.kt │ │ │ │ ├── BooleanValidations.kt │ │ │ │ ├── PairValidations.kt │ │ │ │ ├── TripleValidations.kt │ │ │ │ ├── NullableValidations.kt │ │ │ │ ├── FloatValidations.kt │ │ │ │ ├── DoubleValidations.kt │ │ │ │ ├── IntValidations.kt │ │ │ │ ├── LongValidations.kt │ │ │ │ ├── ByteValidations.kt │ │ │ │ ├── ShortValidations.kt │ │ │ │ ├── CharValidations.kt │ │ │ │ ├── ComparableValidations.kt │ │ │ │ ├── CommonValidations.kt │ │ │ │ ├── StringValidations.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── MapValidations.kt │ │ │ │ └── CharSequenceValidations.kt │ │ │ │ ├── Case.kt │ │ │ │ └── Descriptors.kt │ │ └── commonTest │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── ahmedmourad │ │ │ └── validation │ │ │ └── core │ │ │ ├── CaseTests.kt │ │ │ ├── BooleanValidationsTests.kt │ │ │ ├── utils │ │ │ ├── ValidationsUtils.kt │ │ │ └── DescriptorsUtils.kt │ │ │ ├── PairValidationsTests.kt │ │ │ ├── ConstraintsBuilderTests.kt │ │ │ ├── NullableValidationsTests.kt │ │ │ ├── TripleValidationsTests.kt │ │ │ ├── IncludedValidatorDescriptorTests.kt │ │ │ ├── ScopedConstraintBuilderTests.kt │ │ │ ├── CommonValidationsTests.kt │ │ │ ├── ComparableValidationsTests.kt │ │ │ ├── FloatValidationsTests.kt │ │ │ ├── DoubleValidationsTests.kt │ │ │ ├── IntValidationsTests.kt │ │ │ ├── ByteValidationsTests.kt │ │ │ ├── LongValidationsTests.kt │ │ │ ├── ShortValidationsTests.kt │ │ │ └── ConstraintBuilderTests.kt │ └── build.gradle.kts ├── gradle-plugin │ ├── gradle.properties │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── ahmedmourad │ │ │ └── validation │ │ │ └── gradle │ │ │ └── ValidationGradlePlugin.kt │ └── build.gradle.kts ├── compiler-plugin │ ├── gradle.properties │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ └── META-INF │ │ │ │ │ └── services │ │ │ │ │ ├── javax.script.ScriptEngineFactory │ │ │ │ │ └── org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── ahmedmourad │ │ │ │ └── validation │ │ │ │ └── compiler │ │ │ │ ├── descriptors │ │ │ │ ├── MetaDescriptor.kt │ │ │ │ ├── ViolationDescriptor.kt │ │ │ │ ├── IncludedValidatorDescriptor.kt │ │ │ │ └── ValidatorDescriptor.kt │ │ │ │ ├── codegen │ │ │ │ ├── CodeSectionGenerator.kt │ │ │ │ ├── CodeGenerator.kt │ │ │ │ ├── validations │ │ │ │ │ ├── ViolationsGenerator.kt │ │ │ │ │ └── ValidationContextGenerator.kt │ │ │ │ └── ValidationsCodeGenerator.kt │ │ │ │ ├── ValidationExtensions.kt │ │ │ │ ├── utils │ │ │ │ ├── MessageCollectorUtils.kt │ │ │ │ ├── Constants.kt │ │ │ │ └── PsiUtils.kt │ │ │ │ ├── ValidationPlugin.kt │ │ │ │ ├── files │ │ │ │ └── FileBuilder.kt │ │ │ │ ├── CompilerContext.kt │ │ │ │ ├── ValidationSyntheticResolver.kt │ │ │ │ └── ValidationAnalysisHandlerExtension.kt │ │ └── test │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── ahmedmourad │ │ │ └── validation │ │ │ └── compiler │ │ │ ├── FunctionsGeneratorTests.kt │ │ │ ├── TestUtils.kt │ │ │ ├── ValidationContextGeneratorTests.kt │ │ │ └── ViolationsGeneratorTests.kt │ └── build.gradle.kts ├── settings.gradle.kts ├── build.gradle.kts ├── gradle.properties ├── gradlew.bat └── gradlew ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── validators ├── gradle.properties ├── src │ ├── commonTest │ │ └── kotlin │ │ │ └── dev │ │ │ └── ahmedmourad │ │ │ └── validation │ │ │ └── validators │ │ │ └── PasswordValidatorTests.kt │ └── commonMain │ │ └── kotlin │ │ └── dev │ │ └── ahmedmourad │ │ └── validation │ │ └── validators │ │ ├── EmailValidator.kt │ │ ├── IsbnValidator.kt │ │ └── PasswordValidator.kt └── build.gradle.kts ├── gradle.properties ├── settings.gradle.kts ├── sample └── build.gradle.kts ├── gradlew.bat └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /compiler-plugin/build/ 4 | /sample/build/ 5 | /.idea/ 6 | /build/ -------------------------------------------------------------------------------- /framework/core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Kotlin Validation Core 2 | POM_ARTIFACT_ID=validation-core 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedMourad0/kotlin-validation/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /validators/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Kotlin Validation Validators 2 | POM_ARTIFACT_ID=validation-validators 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /framework/gradle-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Kotlin Validation Gradle Plugin 2 | POM_ARTIFACT_ID=validation-gradle-plugin 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /framework/compiler-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Kotlin Validation Compiler Plugin 2 | POM_ARTIFACT_ID=validation-compiler-plugin 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/CaseValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/TemporalValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory: -------------------------------------------------------------------------------- 1 | org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar: -------------------------------------------------------------------------------- 1 | dev.ahmedmourad.validation.compiler.ValidationPlugin -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlinVersion=1.7.10 3 | jvmTargetVersion=1.8 4 | mavenPublishPluginVersion=0.15.1 5 | validationVersion=0.1.0-SNAPSHOT 6 | -------------------------------------------------------------------------------- /framework/gradle-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin: -------------------------------------------------------------------------------- 1 | dev.ahmedmourad.validation.gradle.ValidationGradlePlugin 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /framework/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | jcenter() 6 | } 7 | } 8 | 9 | rootProject.name = "framework" 10 | 11 | include("gradle-plugin") 12 | include("compiler-plugin") 13 | include("core") 14 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/BooleanValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | fun Constraint.isTrue() = validation { 6 | subject 7 | } 8 | 9 | fun Constraint.isFalse() = validation { 10 | !subject 11 | } 12 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/descriptors/MetaDescriptor.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.descriptors 2 | 3 | import org.jetbrains.kotlin.psi.KtExpression 4 | 5 | internal data class MetaDescriptor( 6 | val name: String, 7 | val nameExpression: KtExpression, 8 | val typeFqName: String, 9 | val includedValidator: IncludedValidatorDescriptor? 10 | ) 11 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/codegen/CodeSectionGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.codegen 2 | 3 | import dev.ahmedmourad.validation.compiler.descriptors.ValidatorDescriptor 4 | 5 | internal interface CodeSectionGenerator { 6 | fun imports(validatorDescriptor: ValidatorDescriptor): Set 7 | fun generate(validatorDescriptor: ValidatorDescriptor): Set 8 | } 9 | -------------------------------------------------------------------------------- /validators/src/commonTest/kotlin/dev/ahmedmourad/validation/validators/PasswordValidatorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.validators 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | //import dev.ahmedmourad.validation.validators.validations.* 6 | 7 | class PasswordValidatorTests { 8 | @Test 9 | fun build() { 10 | // PasswordValidator.validate { 11 | // "Hello@123" 12 | // } 13 | } 14 | } -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/codegen/CodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.codegen 2 | 3 | import org.jetbrains.kotlin.descriptors.ModuleDescriptor 4 | import org.jetbrains.kotlin.psi.KtFile 5 | import java.io.File 6 | 7 | internal interface CodeGenerator { 8 | fun generate( 9 | codeGenDir: File, 10 | module: ModuleDescriptor, 11 | projectFiles: Collection 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/descriptors/ViolationDescriptor.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.descriptors 2 | 3 | import org.jetbrains.kotlin.psi.KtExpression 4 | 5 | internal data class ViolationDescriptor( 6 | val name: String, 7 | val nameExpression: KtExpression, 8 | val metas: List 9 | ) { 10 | val regularMetas by lazy { metas.filter { it.includedValidator == null } } 11 | val inclusionMetas by lazy { metas.filter { it.includedValidator != null } } 12 | } 13 | -------------------------------------------------------------------------------- /framework/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() 7 | } 8 | 9 | val kotlinVersion: String by project 10 | val mavenPublishPluginVersion: String by project 11 | 12 | dependencies { 13 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") 14 | classpath("com.vanniktech:gradle-maven-publish-plugin:$mavenPublishPluginVersion") 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | jcenter() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "validation" 3 | include("validators") 4 | include("sample") 5 | 6 | pluginManagement { 7 | val kotlinVersion: String by settings 8 | plugins { 9 | kotlin("jvm") version kotlinVersion 10 | } 11 | } 12 | 13 | includeBuild("framework") { 14 | dependencySubstitution { 15 | substitute(module("dev.ahmedmourad.validation:validation-gradle-plugin")).with(project(":gradle-plugin")) 16 | substitute(module("dev.ahmedmourad.validation:validation-compiler-plugin")).with(project(":compiler-plugin")) 17 | substitute(module("dev.ahmedmourad.validation:validation-core")).with(project(":core")) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /validators/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | } 4 | 5 | group = "dev.ahmedmourad.validation" 6 | version = "0.1.0-SNAPSHOT" 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | kotlin { 13 | jvm() 14 | // js(IR) { 15 | // browser() 16 | // nodejs() 17 | // } 18 | // ios() 19 | sourceSets { 20 | val commonMain by getting { 21 | dependencies { 22 | implementation("dev.ahmedmourad.validation:validation-core") 23 | } 24 | } 25 | val commonTest by getting { 26 | dependencies { 27 | implementation(kotlin("test")) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/PairValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | inline fun Constraint>.first( 7 | crossinline firstConstraint: Constraint.() -> Unit 8 | ) = validation { 9 | ScopedConstraintBuilder().apply(firstConstraint).matchesAll(subject.first) 10 | } 11 | 12 | inline fun Constraint>.second( 13 | crossinline secondConstraint: Constraint.() -> Unit 14 | ) = validation { 15 | ScopedConstraintBuilder().apply(secondConstraint).matchesAll(subject.second) 16 | } 17 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/descriptors/IncludedValidatorDescriptor.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.descriptors 2 | 3 | import dev.ahmedmourad.validation.compiler.utils.deepFqName 4 | import org.jetbrains.kotlin.types.KotlinType 5 | 6 | internal data class IncludedValidatorDescriptor( 7 | val validationsFileFqName: String, 8 | val subjectType: KotlinType, 9 | val validatorType: KotlinType, 10 | val subjectAliasOrSimpleName: String 11 | ) { 12 | 13 | val subjectFqName by lazy { subjectType.deepFqName() } 14 | 15 | val validatorFqName by lazy { validatorType.deepFqName() } 16 | 17 | val validateFqName by lazy { "$validationsFileFqName.validate" } 18 | val isValidFqName by lazy { "$validationsFileFqName.isValid" } 19 | } 20 | -------------------------------------------------------------------------------- /framework/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | } 4 | 5 | group = "dev.ahmedmourad.validation" 6 | version = "0.1.0-SNAPSHOT" 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | kotlin { 13 | jvm() 14 | // js(IR) { 15 | // browser() 16 | // nodejs() 17 | // } 18 | // ios() 19 | sourceSets { 20 | val commonMain by getting { 21 | dependencies { 22 | 23 | } 24 | } 25 | val commonTest by getting { 26 | dependencies { 27 | implementation(kotlin("test")) 28 | } 29 | } 30 | } 31 | 32 | targets.all { 33 | compilations.all { 34 | kotlinOptions { 35 | freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/Case.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | sealed class Case { 4 | data class Illegal(val v: V) : Case() 5 | data class Legal(val v: T) : Case() 6 | } 7 | 8 | fun T.legal(): Case { 9 | return Case.Legal(this) 10 | } 11 | 12 | fun V.illegal(): Case { 13 | return Case.Illegal(this) 14 | } 15 | 16 | fun Case.swap(): Case { 17 | return when (this) { 18 | is Case.Illegal -> this.v.legal() 19 | is Case.Legal -> this.v.illegal() 20 | } 21 | } 22 | 23 | fun Case.orElse(substitute: () -> T): T { 24 | return when (this) { 25 | is Case.Illegal -> substitute() 26 | is Case.Legal -> this.v 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/ValidationExtensions.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | //TODO: also enforce no vars 4 | //TODO: this's just poc, it's terrible, it will be changed when typed quotes are released 5 | //internal fun Meta.validationsClassDeclarationQuote(cc: CompilerContext): ExtensionPhase = classDeclaration(cc, { 6 | // this.isAnnotatedWith("@MustBeValid".toRegex()) 7 | //}) { 8 | // this.allConstructors.value.forEach { 9 | // if (!it.hasModifier(KtTokens.INTERNAL_KEYWORD)) { 10 | // cc.messageCollector?.report( 11 | // CompilerMessageSeverity.ERROR, 12 | // "All constructors of @MustBeValid annotated classes must be internal", 13 | // MessageUtil.psiElementToMessageLocation(it) 14 | // ) 15 | // } 16 | // } 17 | // Transform.empty 18 | //} 19 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/CaseTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.Case.* 4 | import kotlin.test.* 5 | import kotlin.test.Test 6 | 7 | class CaseTests { 8 | 9 | @Test 10 | fun legal_createsLegalCaseInstance() { 11 | assertEquals(Legal(5), 5.legal()) 12 | } 13 | 14 | @Test 15 | fun illegal_createsIllegalCaseInstance() { 16 | assertEquals(Illegal(5), 5.illegal()) 17 | } 18 | 19 | @Test 20 | fun swap_swapsLegalCasesForIllegalOnesAndViceVersa() { 21 | assertEquals(Illegal(5), 5.legal().swap()) 22 | assertEquals(Legal(5), 5.illegal().swap()) 23 | } 24 | 25 | @Test 26 | fun orElse_returnsThisValueIfLegalOrTheGivenOneIfIllegal() { 27 | assertEquals(5, 5.legal().orElse { 4 }) 28 | assertEquals(4, 5.illegal().orElse { 4 }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /framework/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlinVersion=1.7.10 3 | jvmTargetVersion=1.8 4 | mavenPublishPluginVersion=0.15.1 5 | validationVersion=0.1.0-SNAPSHOT 6 | kapt.includeCompileClasspath=false 7 | GROUP=dev.ahmedmourad.validation 8 | VERSION_NAME=0.1.0-SNAPSHOT 9 | POM_DESCRIPTION=A multiplatform, declarative, flexible and type-safe Kotlin validation framework. 10 | POM_URL=https://github.com/AhmedMourad0/kotlin-validation/ 11 | POM_SCM_URL=https://github.com/AhmedMourad0/kotlin-validation/ 12 | POM_SCM_CONNECTION=scm:git:git://github.com/AhmedMourad0/kotlin-validation.git 13 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/AhmedMourad0/kotlin-validation.git 14 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 15 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 16 | POM_LICENCE_DIST=repo 17 | POM_DEVELOPER_ID=AhmedMourad 18 | POM_DEVELOPER_NAME=Ahmed Mourad 19 | POM_DEVELOPER_URL=https://www.ahmedmourad.dev 20 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/utils/MessageCollectorUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.utils 2 | 3 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation 4 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.cli.common.messages.MessageUtil 7 | import com.intellij.psi.PsiElement 8 | 9 | internal fun MessageCollector.error(message: String, element: PsiElement?) { 10 | this.report( 11 | CompilerMessageSeverity.ERROR, 12 | message, 13 | MessageUtil.psiElementToMessageLocation(element) 14 | ) 15 | } 16 | 17 | internal fun MessageCollector.log(message: String) { 18 | this.report( 19 | CompilerMessageSeverity.LOGGING, 20 | "Kotlin Validation: $message", 21 | CompilerMessageLocation.create(null) 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/TripleValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | inline fun Constraint>.first( 7 | crossinline firstConstraint: Constraint.() -> Unit 8 | ) = validation { 9 | ScopedConstraintBuilder().apply(firstConstraint).matchesAll(subject.first) 10 | } 11 | 12 | inline fun Constraint>.second( 13 | crossinline secondConstraint: Constraint.() -> Unit 14 | ) = validation { 15 | ScopedConstraintBuilder().apply(secondConstraint).matchesAll(subject.second) 16 | } 17 | 18 | inline fun Constraint>.third( 19 | crossinline thirdConstraint: Constraint.() -> Unit 20 | ) = validation { 21 | ScopedConstraintBuilder().apply(thirdConstraint).matchesAll(subject.third) 22 | } 23 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/BooleanValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.isFalse 7 | import dev.ahmedmourad.validation.core.validations.isTrue 8 | import kotlin.test.Test 9 | 10 | class BooleanValidationsTests { 11 | 12 | @Test 13 | fun isTrue_meansThisBooleanEqualsTrue() { 14 | constraint { 15 | isTrue() 16 | }.allMatch( 17 | true 18 | ).allFail( 19 | false 20 | ) 21 | } 22 | 23 | @Test 24 | fun isFalse_meansThisBooleanEqualsFalse() { 25 | constraint { 26 | isFalse() 27 | }.allMatch( 28 | false 29 | ).allFail( 30 | true 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/NullableValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | fun
Constraint.ifExists( 7 | validations: Constraint
.() -> Unit 8 | ) = this@ifExists.validation { 9 | if (subject != null) { 10 | ScopedConstraintBuilder
().apply(validations).matchesAll(subject) 11 | } else { 12 | true 13 | } 14 | } 15 | 16 | fun
Constraint.mustExist( 17 | validations: Constraint
.() -> Unit 18 | ) = this@mustExist.validation { 19 | if (subject != null) { 20 | ScopedConstraintBuilder
().apply(validations).matchesAll(subject) 21 | } else { 22 | false 23 | } 24 | } 25 | 26 | fun
Constraint.exists() = validation { 27 | subject != null 28 | } 29 | 30 | fun
Constraint.doesNotExist() = validation { 31 | subject == null 32 | } 33 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/utils/ValidationsUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.utils 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | fun
constraint(constraint: Constraint
.() -> Unit): ScopedConstraintBuilder
{ 7 | return ScopedConstraintBuilder
().apply(constraint) 8 | } 9 | 10 | fun
ScopedConstraintBuilder
.allMatch(vararg items: DT): ScopedConstraintBuilder
{ 11 | val failed = items.filterNot { 12 | this.matchesAll(it) 13 | } 14 | if (failed.isNotEmpty()) { 15 | throw AssertionError( 16 | "Validations do not match for items: {\n\t${failed.joinToString(",\n\t")}\n}" 17 | ) 18 | } 19 | return this 20 | } 21 | 22 | fun
ScopedConstraintBuilder
.allFail(vararg items: DT): ScopedConstraintBuilder
{ 23 | val matching = items.filter { 24 | this.matchesAll(it) 25 | } 26 | if (matching.isNotEmpty()) { 27 | throw AssertionError( 28 | "Validations do not fail for items: {\n\t${matching.joinToString(",\n\t")}\n}" 29 | ) 30 | } 31 | return this 32 | } 33 | -------------------------------------------------------------------------------- /validators/src/commonMain/kotlin/dev/ahmedmourad/validation/validators/EmailValidator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.validators 2 | 3 | import dev.ahmedmourad.validation.core.* 4 | import dev.ahmedmourad.validation.core.validations.* 5 | 6 | @ValidatorConfig(subjectAlias = "Email") 7 | object EmailValidator : Validator { 8 | override val constraints by describe { 9 | val elements = evaluate { subject.split('@') } 10 | constraint(violation = "MalformedEmail") { 11 | validation { subject.any { it == '@' } } 12 | } 13 | constraint(violation = "InvalidLocal") { 14 | val local = evaluate { elements.get().dropLast(1).joinToString("@") } 15 | meta("value", local) 16 | on(local) { 17 | isNotEmpty() 18 | maxLength(64) 19 | noneOf { 20 | startsWith(".") 21 | endsWith(".") 22 | } 23 | } 24 | } 25 | constraint(violation = "InvalidDomain") { 26 | val domain = evaluate { elements.get().lastOrNull() } 27 | meta("value", domain) 28 | on(domain) ifExists { 29 | isNotEmpty() 30 | maxLength(255) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/PairValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class PairValidationsTests { 10 | 11 | @Test 12 | fun first_meansTheGivenValidationsMatchTheFirstElementOfThePair() { 13 | constraint> { 14 | first { 15 | max(3) 16 | } 17 | }.allMatch( 18 | -1 to Unit, 19 | 1 to Unit, 20 | 2 to Unit, 21 | 3 to Unit 22 | ).allFail( 23 | 4 to Unit, 24 | 5 to Unit, 25 | 6 to Unit 26 | ) 27 | } 28 | 29 | @Test 30 | fun second_meansTheGivenValidationsMatchTheSecondElementOfThePair() { 31 | constraint> { 32 | second { 33 | max(3) 34 | } 35 | }.allMatch( 36 | Unit to -1, 37 | Unit to 1, 38 | Unit to 2, 39 | Unit to 3 40 | ).allFail( 41 | Unit to 4, 42 | Unit to 5, 43 | Unit to 6 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/test/kotlin/dev/ahmedmourad/validation/compiler/FunctionsGeneratorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import org.junit.Assert 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.rules.TemporaryFolder 9 | 10 | class FunctionsGeneratorTests { 11 | 12 | @Rule 13 | @JvmField 14 | var temporaryFolder: TemporaryFolder = TemporaryFolder() 15 | 16 | @Test 17 | fun `A ValidationContext interface is generated for each validator`() { 18 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 19 | 20 | object SomeValidator : Validator { 21 | override val constraints by describe { 22 | constraint("FirstViolation") { } 23 | } 24 | } 25 | 26 | fun main() { 27 | val context: IntValidationContext = object : IntValidationContext { } 28 | } 29 | """)) 30 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 31 | } 32 | 33 | private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { 34 | return prepareCompilation(temporaryFolder, *sourceFiles).compile() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /framework/compiler-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | } 6 | 7 | group = "dev.ahmedmourad.validation" 8 | version = "0.1.0-SNAPSHOT" 9 | 10 | val jvmTargetVersion: String by project 11 | 12 | dependencies { 13 | compileOnly(kotlin("stdlib")) 14 | compileOnly(kotlin("compiler-embeddable")) 15 | 16 | implementation("org.jetbrains.kotlin:kotlin-script-util:1.7.10") { 17 | exclude("org.jetbrains.kotlin", "kotlin-stdlib") 18 | exclude("org.jetbrains.kotlin", "kotlin-compiler") 19 | exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable") 20 | } 21 | implementation("org.jetbrains.kotlin:kotlin-compiler:1.7.10") 22 | implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10") 23 | implementation("org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10") 24 | 25 | testImplementation(kotlin("stdlib")) 26 | testImplementation(kotlin("compiler-embeddable")) 27 | testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.3.6") 28 | testImplementation("junit:junit:4.12") 29 | testImplementation(project(":core")) 30 | } 31 | 32 | tasks { 33 | withType { 34 | kotlinOptions { 35 | jvmTarget = jvmTargetVersion 36 | freeCompilerArgs = listOf("-Xjvm-default=enable") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /framework/gradle-plugin/src/main/kotlin/dev/ahmedmourad/validation/gradle/ValidationGradlePlugin.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.gradle 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Provider 5 | import org.jetbrains.kotlin.gradle.plugin.* 6 | 7 | class ValidationGradlePlugin : KotlinCompilerPluginSupportPlugin { 8 | 9 | override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { 10 | return kotlinCompilation.target.project.plugins.hasPlugin(ValidationGradlePlugin::class.java) 11 | } 12 | 13 | override fun getCompilerPluginId(): String = "validation-compiler-plugin" 14 | 15 | override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact( 16 | groupId = "dev.ahmedmourad.validation", 17 | artifactId = "validation-compiler-plugin", 18 | version = "0.1.0-SNAPSHOT" 19 | ) 20 | 21 | override fun apply(target: Project) { 22 | target.extensions.create("kotlin-validation", ValidationGradleExtension::class.java) 23 | } 24 | 25 | override fun applyToCompilation( 26 | kotlinCompilation: KotlinCompilation<*> 27 | ): Provider> { 28 | 29 | // project.dependencies.add( 30 | // "implementation", 31 | // "dev.ahmedmourad.validation:validation-core:0.1.0-SNAPSHOT" 32 | // ) 33 | 34 | val project = kotlinCompilation.target.project 35 | return project.provider { emptyList() } 36 | } 37 | } 38 | 39 | open class ValidationGradleExtension 40 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/FloatValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Float) -> Float 7 | ) = validation { 8 | subject % other(subject) == 0.0f 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Float) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Float) -> Float 15 | ) = validation { 16 | subject % other(subject) != 0.0f 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Float) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isPositive(orZero: Boolean) = validation { 22 | if (orZero) { 23 | subject >= 0.0f 24 | } else { 25 | subject > 0.0f 26 | } 27 | } 28 | 29 | fun Constraint.isNegative(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject <= 0.0f 32 | } else { 33 | subject < 0.0f 34 | } 35 | } 36 | 37 | fun Constraint.isZero() = validation { 38 | subject == 0.0f 39 | } 40 | 41 | fun Constraint.isNotZero() = validation { 42 | subject != 0.0f 43 | } 44 | 45 | fun Constraint.isNaN() = validation { 46 | subject.isNaN() 47 | } 48 | 49 | fun Constraint.isNotNaN() = validation { 50 | !subject.isNaN() 51 | } 52 | 53 | fun Constraint.isInfinite() = validation { 54 | subject.isInfinite() 55 | } 56 | 57 | fun Constraint.isFinite() = validation { 58 | subject.isFinite() 59 | } 60 | 61 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/DoubleValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Double) -> Double 7 | ) = validation { 8 | subject % other(subject) == 0.0 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Double) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Double) -> Double 15 | ) = validation { 16 | subject % other(subject) != 0.0 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Double) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isPositive(orZero: Boolean) = validation { 22 | if (orZero) { 23 | subject >= 0.0 24 | } else { 25 | subject > 0.0 26 | } 27 | } 28 | 29 | fun Constraint.isNegative(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject <= 0.0 32 | } else { 33 | subject < 0.0 34 | } 35 | } 36 | 37 | fun Constraint.isZero() = validation { 38 | subject == 0.0 39 | } 40 | 41 | fun Constraint.isNotZero() = validation { 42 | subject != 0.0 43 | } 44 | 45 | fun Constraint.isNaN() = validation { 46 | subject.isNaN() 47 | } 48 | 49 | fun Constraint.isNotNaN() = validation { 50 | !subject.isNaN() 51 | } 52 | 53 | fun Constraint.isInfinite() = validation { 54 | subject.isInfinite() 55 | } 56 | 57 | fun Constraint.isFinite() = validation { 58 | subject.isFinite() 59 | } 60 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/test/kotlin/dev/ahmedmourad/validation/compiler/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import dev.ahmedmourad.validation.compiler.utils.OUTPUT_FOLDER 6 | import org.intellij.lang.annotations.Language 7 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 8 | import org.jetbrains.kotlin.config.JvmTarget 9 | import org.junit.rules.TemporaryFolder 10 | 11 | @Language("kotlin") 12 | const val PACKAGE_AND_IMPORTS = """ 13 | package dev.ahmedmourad.validation.compiler 14 | import dev.ahmedmourad.validation.core.* 15 | import dev.ahmedmourad.validation.core.validations.* 16 | import dev.ahmedmourad.validation.compiler.$OUTPUT_FOLDER.* 17 | """ 18 | 19 | @Language("kotlin") 20 | const val MINIMAL_INT_VALIDATOR = """ 21 | object IntValidator : Validator { 22 | override val constraints by describe { 23 | constraint(violation = "TooShort") { } 24 | } 25 | } 26 | """ 27 | 28 | fun prepareCompilation( 29 | temporaryFolder: TemporaryFolder, 30 | vararg sourceFiles: SourceFile 31 | ): KotlinCompilation { 32 | return KotlinCompilation().apply { 33 | workingDir = temporaryFolder.root 34 | // workingDir = File("E://test//dev//ahmedmourad//validation//compiler") 35 | kotlincArguments = listOf("-Xopt-in=kotlin.RequiresOptIn") 36 | compilerPlugins = listOf(ValidationPlugin()) 37 | inheritClassPath = true 38 | sources = sourceFiles.asList() 39 | verbose = false 40 | jvmTarget = JvmTarget.JVM_1_8.description 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | } 6 | 7 | apply(plugin = "dev.ahmedmourad.validation.validation-gradle-plugin") 8 | 9 | group = "dev.ahmedmourad.validation" 10 | version = "0.1.0-SNAPSHOT" 11 | 12 | val validationVersion: String by project 13 | val jvmTargetVersion: String by project 14 | 15 | dependencies { 16 | implementation(kotlin("stdlib")) 17 | 18 | implementation("dev.ahmedmourad.validation:validation-core") 19 | implementation(project(":validators")) 20 | kotlinCompilerClasspath("org.jetbrains.kotlin:kotlin-script-util:1.7.10") { 21 | exclude("org.jetbrains.kotlin", "kotlin-stdlib") 22 | exclude("org.jetbrains.kotlin", "kotlin-compiler") 23 | exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable") 24 | } 25 | kotlinCompilerClasspath("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10") 26 | kotlinCompilerClasspath("org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10") 27 | // kotlinCompilerClasspath("dev.ahmedmourad.validation:validation-core") 28 | } 29 | 30 | tasks { 31 | val compileKotlin: KotlinCompile by this 32 | val compileTestKotlin: KotlinCompile by this 33 | compileKotlin.kotlinOptions { 34 | jvmTarget = jvmTargetVersion 35 | freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") 36 | } 37 | compileTestKotlin.kotlinOptions { 38 | jvmTarget = jvmTargetVersion 39 | freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") 40 | } 41 | jar { 42 | manifest { 43 | attributes("Main-Class" to "dev.ahmedmourad.validation.sample.MainKt") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/IntValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Int) -> Int 7 | ) = validation { 8 | subject % other(subject) == 0 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Int) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Int) -> Int 15 | ) = validation { 16 | subject % other(subject) != 0 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Int) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isEven() = validation { 22 | subject % 2 == 0 23 | } 24 | 25 | fun Constraint.isOdd() = validation { 26 | subject % 2 != 0 27 | } 28 | 29 | fun Constraint.isPositive(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject >= 0 32 | } else { 33 | subject > 0 34 | } 35 | } 36 | 37 | fun Constraint.isNegative(orZero: Boolean) = validation { 38 | if (orZero) { 39 | subject <= 0 40 | } else { 41 | subject < 0 42 | } 43 | } 44 | 45 | fun Constraint.isZero() = validation { 46 | subject == 0 47 | } 48 | 49 | fun Constraint.isNotZero() = validation { 50 | subject != 0 51 | } 52 | 53 | fun Constraint.isPrime() = validation { 54 | if (subject < 2) return@validation false 55 | (2..(subject / 2)).none { n -> 56 | subject % n == 0 57 | } 58 | } 59 | 60 | fun Constraint.isNotPrime() = validation { 61 | if (subject < 2) return@validation true 62 | (2..(subject / 2)).any { n -> 63 | subject % n == 0 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ConstraintsBuilderTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class ConstraintsBuilderTests : Validator { 7 | 8 | override val constraints get() = TODO() 9 | 10 | @Test 11 | fun describe_createsConstraintsDescriptorWithTheDeclaredConstraints() { 12 | 13 | assertEquals(ValidatorDescriptor(emptyList()), describe { }.value) 14 | 15 | val expectedConstraint = ConstraintDescriptor( 16 | "SomeViolation", 17 | emptyList(), 18 | emptyList(), 19 | emptyList() 20 | ) 21 | val expected = ValidatorDescriptor(listOf(expectedConstraint)) 22 | 23 | val actual = describe { 24 | constraint(expectedConstraint.violation) { } 25 | }.value 26 | 27 | assertEquals(expected, actual) 28 | } 29 | 30 | @Test 31 | fun constraint_declaresConstraintAsPartOfThisConstraintsDescriptor() { 32 | 33 | val expectedMeta = MetadataDescriptor("someMeta") { 2 } 34 | 35 | val expected = ConstraintDescriptor( 36 | "SomeViolation", 37 | emptyList(), 38 | emptyList(), 39 | listOf(expectedMeta) 40 | ) 41 | 42 | val actual = ConstraintsBuilder().apply { 43 | constraint(expected.violation) { 44 | meta(expectedMeta.name, expectedMeta::get) 45 | } 46 | }.build().first() 47 | 48 | val actualMeta = actual.metadata.first() 49 | 50 | assertEquals(expectedMeta.name, actualMeta.name) 51 | assertEquals(expectedMeta.get(5), actualMeta.get(5)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/LongValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Long) -> Long 7 | ) = validation { 8 | subject % other(subject) == 0L 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Long) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Long) -> Long 15 | ) = validation { 16 | subject % other(subject) != 0L 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Long) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isEven() = validation { 22 | subject % 2 == 0L 23 | } 24 | 25 | fun Constraint.isOdd() = validation { 26 | subject % 2 != 0L 27 | } 28 | 29 | fun Constraint.isPositive(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject >= 0 32 | } else { 33 | subject > 0 34 | } 35 | } 36 | 37 | fun Constraint.isNegative(orZero: Boolean) = validation { 38 | if (orZero) { 39 | subject <= 0 40 | } else { 41 | subject < 0 42 | } 43 | } 44 | 45 | fun Constraint.isZero() = validation { 46 | subject == 0L 47 | } 48 | 49 | fun Constraint.isNotZero() = validation { 50 | subject != 0L 51 | } 52 | 53 | fun Constraint.isPrime() = validation { 54 | if (subject < 2) return@validation false 55 | (2..(subject / 2)).none { n -> 56 | subject % n == 0L 57 | } 58 | } 59 | 60 | fun Constraint.isNotPrime() = validation { 61 | if (subject < 2) return@validation true 62 | (2..(subject / 2)).any { n -> 63 | subject % n == 0L 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/codegen/validations/ViolationsGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.codegen.validations 2 | 3 | import dev.ahmedmourad.validation.compiler.codegen.CodeSectionGenerator 4 | import dev.ahmedmourad.validation.compiler.descriptors.ValidatorDescriptor 5 | import dev.ahmedmourad.validation.compiler.descriptors.ViolationDescriptor 6 | 7 | internal class ViolationsGenerator : CodeSectionGenerator { 8 | 9 | override fun imports(validatorDescriptor: ValidatorDescriptor) = emptySet() 10 | 11 | override fun generate( 12 | validatorDescriptor: ValidatorDescriptor 13 | ): Set { 14 | val parentName = validatorDescriptor.violationsParentName 15 | val violations = validatorDescriptor.violations.joinToString("\n\t") { 16 | generateViolation(parentName, it).replace("\n", "\n\t") 17 | } 18 | return setOf(generateViolationsParent(parentName, violations)) 19 | } 20 | 21 | private fun generateViolationsParent(parentName: String, violations: String): String { 22 | return """ 23 | |sealed class $parentName { 24 | | $violations 25 | |} 26 | """.trimMargin() 27 | } 28 | 29 | private fun generateViolation( 30 | parentName: String, 31 | violation: ViolationDescriptor 32 | ): String { 33 | return if (violation.metas.isEmpty()) { 34 | "object ${violation.name} : $parentName()" 35 | } else { 36 | val metas = violation.metas.joinToString(",\n\t") { meta -> 37 | "val ${meta.name}: ${meta.typeFqName}" 38 | } 39 | "data class ${violation.name}(\n\t$metas\n) : $parentName()" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/ByteValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Byte) -> Byte 7 | ) = validation { 8 | subject % other(subject) == 0 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Byte) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Byte) -> Byte 15 | ) = validation { 16 | subject % other(subject) != 0 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Byte) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isEven() = validation { 22 | subject % 2 == 0 23 | } 24 | 25 | fun Constraint.isOdd() = validation { 26 | subject % 2 != 0 27 | } 28 | 29 | fun Constraint.isPositive(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject >= 0 32 | } else { 33 | subject > 0 34 | } 35 | } 36 | 37 | fun Constraint.isNegative(orZero: Boolean) = validation { 38 | if (orZero) { 39 | subject <= 0 40 | } else { 41 | subject < 0 42 | } 43 | } 44 | 45 | fun Constraint.isZero() = validation { 46 | subject == 0.toByte() 47 | } 48 | 49 | fun Constraint.isNotZero() = validation { 50 | subject != 0.toByte() 51 | } 52 | 53 | fun Constraint.isPrime() = validation { 54 | if (subject < 2) return@validation false 55 | (2..(subject / 2)).none { n -> 56 | subject % n == 0 57 | } 58 | } 59 | 60 | fun Constraint.isNotPrime() = validation { 61 | if (subject < 2) return@validation true 62 | (2..(subject / 2)).any { n -> 63 | subject % n == 0 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/utils/DescriptorsUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.utils 2 | 3 | import dev.ahmedmourad.validation.core.Validator 4 | import dev.ahmedmourad.validation.core.IncludedValidatorDescriptor 5 | 6 | data class IncludedValidatorTestWrapper>( 7 | val validator: IncludedValidatorDescriptor, 8 | val tester: IncludedValidatorDescriptor.(I) -> Boolean 9 | ) 10 | 11 | fun > IncludedValidatorDescriptor.with( 12 | tester: IncludedValidatorDescriptor.(I) -> Boolean 13 | ): IncludedValidatorTestWrapper { 14 | return IncludedValidatorTestWrapper(this, tester) 15 | } 16 | 17 | fun > IncludedValidatorTestWrapper.allMatch( 18 | vararg items: I 19 | ): IncludedValidatorTestWrapper { 20 | val illegalValues = items.filterNot { item -> 21 | this.tester(this.validator, item) 22 | } 23 | if (illegalValues.isNotEmpty()) { 24 | throw AssertionError( 25 | "Constraints do not match for items: {\n\t${illegalValues.joinToString(",\n\t")}\n}" 26 | ) 27 | } 28 | return this 29 | } 30 | 31 | fun > IncludedValidatorTestWrapper.allFail( 32 | vararg items: I 33 | ): IncludedValidatorTestWrapper { 34 | val illegalValues = items.filter { item -> 35 | this.tester(this.validator, item) 36 | } 37 | if (illegalValues.isNotEmpty()) { 38 | throw AssertionError( 39 | "Constraints do not fail for items: {\n\t${illegalValues.joinToString(",\n\t")}\n}" 40 | ) 41 | } 42 | return this 43 | } 44 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/ShortValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun Constraint.isDivisibleBy( 6 | crossinline other: (Short) -> Short 7 | ) = validation { 8 | subject % other(subject) == 0 9 | } 10 | 11 | fun Constraint.isDivisibleBy(other: Short) = isDivisibleBy { other } 12 | 13 | inline fun Constraint.isNotDivisibleBy( 14 | crossinline other: (Short) -> Short 15 | ) = validation { 16 | subject % other(subject) != 0 17 | } 18 | 19 | fun Constraint.isNotDivisibleBy(other: Short) = isNotDivisibleBy { other } 20 | 21 | fun Constraint.isEven() = validation { 22 | subject % 2 == 0 23 | } 24 | 25 | fun Constraint.isOdd() = validation { 26 | subject % 2 != 0 27 | } 28 | 29 | fun Constraint.isPositive(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject >= 0 32 | } else { 33 | subject > 0 34 | } 35 | } 36 | 37 | fun Constraint.isNegative(orZero: Boolean) = validation { 38 | if (orZero) { 39 | subject <= 0 40 | } else { 41 | subject < 0 42 | } 43 | } 44 | 45 | fun Constraint.isZero() = validation { 46 | subject == 0.toShort() 47 | } 48 | 49 | fun Constraint.isNotZero() = validation { 50 | subject != 0.toShort() 51 | } 52 | 53 | fun Constraint.isPrime() = validation { 54 | if (subject < 2) return@validation false 55 | (2..(subject / 2)).none { n -> 56 | subject % n == 0 57 | } 58 | } 59 | 60 | fun Constraint.isNotPrime() = validation { 61 | if (subject < 2) return@validation true 62 | (2..(subject / 2)).any { n -> 63 | subject % n == 0 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/codegen/ValidationsCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.codegen 2 | 3 | import dev.ahmedmourad.validation.compiler.analysers.ConstraintsAnalyser 4 | import dev.ahmedmourad.validation.compiler.codegen.validations.FunctionsGenerator 5 | import dev.ahmedmourad.validation.compiler.codegen.validations.ValidationContextGenerator 6 | import dev.ahmedmourad.validation.compiler.codegen.validations.ViolationsGenerator 7 | import dev.ahmedmourad.validation.compiler.dsl.DslValidator 8 | import dev.ahmedmourad.validation.compiler.files.FileBuilder 9 | import org.jetbrains.kotlin.descriptors.ModuleDescriptor 10 | import org.jetbrains.kotlin.psi.KtFile 11 | import org.jetbrains.kotlin.resolve.BindingContext 12 | import java.io.File 13 | 14 | //TODO: every one should follow the visibility of the least of the validator and subject (public or internal only) 15 | internal class ValidationsCodeGenerator( 16 | private val bindingContext: BindingContext, 17 | private val dslValidator: DslValidator 18 | ) : CodeGenerator { 19 | override fun generate( 20 | codeGenDir: File, 21 | module: ModuleDescriptor, 22 | projectFiles: Collection 23 | ) { 24 | val fileBuilder = FileBuilder(dslValidator) 25 | val violationsGenerator = ViolationsGenerator() 26 | val functionsGenerator = FunctionsGenerator() 27 | val validationContextGenerator = ValidationContextGenerator() 28 | ConstraintsAnalyser(bindingContext, dslValidator).analyse(projectFiles).forEach { descriptor -> 29 | fileBuilder.createFile( 30 | codeGenDir, 31 | descriptor, 32 | violationsGenerator, 33 | functionsGenerator, 34 | validationContextGenerator 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/CharValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | fun Constraint.isDefined() = validation { 6 | subject.isDefined() 7 | } 8 | 9 | fun Constraint.isLetter() = validation { 10 | subject.isLetter() 11 | } 12 | 13 | fun Constraint.isLetterOrDigit() = validation { 14 | subject.isLetterOrDigit() 15 | } 16 | 17 | fun Constraint.isDigit() = validation { 18 | subject.isDigit() 19 | } 20 | 21 | fun Constraint.isISOControl() = validation { 22 | subject.isISOControl() 23 | } 24 | 25 | fun Constraint.isWhitespace() = validation { 26 | subject.isWhitespace() 27 | } 28 | 29 | fun Constraint.isUpperCase() = validation { 30 | subject.isUpperCase() 31 | } 32 | 33 | fun Constraint.isLowerCase() = validation { 34 | subject.isLowerCase() 35 | } 36 | 37 | fun Constraint.isTitleCase() = validation { 38 | subject.isTitleCase() 39 | } 40 | 41 | fun Constraint.isHighSurrogate() = validation { 42 | subject.isHighSurrogate() 43 | } 44 | 45 | fun Constraint.isLowSurrogate() = validation { 46 | subject.isLowSurrogate() 47 | } 48 | 49 | inline fun Constraint.isEqualTo( 50 | ignoreCase: Boolean, 51 | crossinline other: (Char) -> Char 52 | ) = validation { 53 | subject.equals(other(subject), ignoreCase) 54 | } 55 | 56 | fun Constraint.isEqualTo( 57 | ignoreCase: Boolean, 58 | other: Char 59 | ) = isEqualTo(ignoreCase) { other } 60 | 61 | inline fun Constraint.isNotEqualTo( 62 | ignoreCase: Boolean, 63 | crossinline other: (Char) -> Char 64 | ) = validation { 65 | !subject.equals(other(subject), ignoreCase) 66 | } 67 | 68 | fun Constraint.isNotEqualTo( 69 | ignoreCase: Boolean, 70 | other: Char 71 | ) = isNotEqualTo(ignoreCase) { other } 72 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/NullableValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class NullableValidationsTests { 10 | 11 | @Test 12 | fun ifExists_meansThatThisObjectMustMatchTheGivenValidationsIfAndOnlyIfItIsNotNull() { 13 | constraint { 14 | ifExists { 15 | max(3) 16 | } 17 | }.allMatch( 18 | null, 19 | -1, 20 | 0, 21 | 1, 22 | 2, 23 | 3 24 | ).allFail( 25 | 4, 26 | 5, 27 | 6 28 | ) 29 | } 30 | 31 | @Test 32 | fun mustExist_meansThatThisObjectCannotBeNullAndMustMatchTheGivenValidations() { 33 | constraint { 34 | mustExist { 35 | max(3) 36 | } 37 | }.allMatch( 38 | -1, 39 | 0, 40 | 1, 41 | 2, 42 | 3 43 | ).allFail( 44 | null, 45 | 4, 46 | 5, 47 | 6 48 | ) 49 | } 50 | 51 | @Test 52 | fun exists_meansThatThisObjectCannotBeNull() { 53 | constraint { 54 | exists() 55 | }.allMatch( 56 | -1, 57 | 0, 58 | 1, 59 | 2, 60 | 3 61 | ).allFail( 62 | null 63 | ) 64 | } 65 | 66 | @Test 67 | fun exists_meansThatThisObjectMustBeNull() { 68 | constraint { 69 | doesNotExist() 70 | }.allMatch( 71 | null 72 | ).allFail( 73 | -1, 74 | 0, 75 | 1, 76 | 2, 77 | 3 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/ValidationPlugin.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.intellij.mock.MockProject 4 | import com.intellij.openapi.project.Project 5 | import dev.ahmedmourad.validation.compiler.codegen.ValidationsCodeGenerator 6 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 7 | import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot 8 | import org.jetbrains.kotlin.cli.common.config.kotlinSourceRoots 9 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 10 | import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder 11 | import org.jetbrains.kotlin.com.intellij.openapi.extensions.impl.ExtensionPointImpl 12 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 13 | import org.jetbrains.kotlin.config.CompilerConfiguration 14 | import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor 15 | import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension 16 | import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension 17 | import org.jetbrains.kotlin.utils.addToStdlib.cast 18 | import java.io.File 19 | 20 | class ValidationPlugin : ComponentRegistrar { 21 | override fun registerProjectComponents( 22 | project: MockProject, 23 | configuration: CompilerConfiguration 24 | ) { 25 | 26 | val messageCollector = configuration.get( 27 | CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, 28 | MessageCollector.NONE 29 | ) 30 | 31 | val ctx = CompilerContext( 32 | messageCollector, 33 | configuration 34 | ) 35 | 36 | val generators = listOf(CodeGeneratorFactory(::ValidationsCodeGenerator)) 37 | 38 | SyntheticResolveExtension.registerExtensionAsFirst( 39 | project, 40 | ValidationSyntheticResolveExtension(messageCollector) 41 | ) 42 | 43 | AnalysisHandlerExtension.registerExtensionAsFirst( 44 | project, 45 | ValidationAnalysisHandlerExtension(ctx, generators) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/TripleValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class TripleValidationsTests { 10 | 11 | @Test 12 | fun first_meansTheGivenValidationsMatchTheFirstElementOfTheTriple() { 13 | constraint> { 14 | first { 15 | max(3) 16 | } 17 | }.allMatch( 18 | Triple(-1, Unit, Unit), 19 | Triple(1, Unit, Unit), 20 | Triple(2, Unit, Unit), 21 | Triple(3, Unit, Unit) 22 | ).allFail( 23 | Triple(4, Unit, Unit), 24 | Triple(5, Unit, Unit), 25 | Triple(6, Unit, Unit) 26 | ) 27 | } 28 | 29 | @Test 30 | fun second_meansTheGivenValidationsMatchTheSecondElementOfTheTriple() { 31 | constraint> { 32 | second { 33 | max(3) 34 | } 35 | }.allMatch( 36 | Triple(Unit, -1, Unit), 37 | Triple(Unit, 1, Unit), 38 | Triple(Unit, 2, Unit), 39 | Triple(Unit, 3, Unit) 40 | ).allFail( 41 | Triple(Unit, 4, Unit), 42 | Triple(Unit, 5, Unit), 43 | Triple(Unit, 6, Unit) 44 | ) 45 | } 46 | 47 | @Test 48 | fun third_meansTheGivenValidationsMatchTheThirdElementOfTheTriple() { 49 | constraint> { 50 | third { 51 | max(3) 52 | } 53 | }.allMatch( 54 | Triple(Unit, Unit, -1), 55 | Triple(Unit, Unit, 1), 56 | Triple(Unit, Unit, 2), 57 | Triple(Unit, Unit, 3) 58 | ).allFail( 59 | Triple(Unit, Unit, 4), 60 | Triple(Unit, Unit, 5), 61 | Triple(Unit, Unit, 6) 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/ComparableValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | inline fun
> Constraint
.min( 6 | crossinline min: (DT) -> DT 7 | ) = validation { 8 | subject >= min(subject) 9 | } 10 | 11 | fun
> Constraint
.min(min: DT) = validation { 12 | subject >= min 13 | } 14 | 15 | inline fun
> Constraint
.max( 16 | crossinline max: (DT) -> DT 17 | ) = validation { 18 | subject <= max(subject) 19 | } 20 | 21 | fun
> Constraint
.max(max: DT) = validation { 22 | subject <= max 23 | } 24 | 25 | inline fun
> Constraint
.lessThan( 26 | crossinline maxExclusive: (DT) -> DT 27 | ) = validation { 28 | subject < maxExclusive(subject) 29 | } 30 | 31 | fun
> Constraint
.lessThan(maxExclusive: DT) = validation { 32 | subject < maxExclusive 33 | } 34 | 35 | inline fun
> Constraint
.largerThan( 36 | crossinline minExclusive: (DT) -> DT 37 | ) = validation { 38 | subject > minExclusive(subject) 39 | } 40 | 41 | fun
> Constraint
.largerThan(minExclusive: DT) = validation { 42 | subject > minExclusive 43 | } 44 | 45 | inline fun
> Constraint
.inRange( 46 | crossinline range: (DT) -> ClosedRange
47 | ) = validation { 48 | subject in range(subject) 49 | } 50 | 51 | fun
> Constraint
.inRange(range: ClosedRange
) = inRange { range } 52 | 53 | fun
> Constraint
.inRange(min: DT, max: DT) = inRange(min..max) 54 | 55 | inline fun
> Constraint
.notInRange( 56 | crossinline range: (DT) -> ClosedRange
57 | ) = validation { 58 | subject !in range(subject) 59 | } 60 | 61 | fun
> Constraint
.notInRange(range: ClosedRange
) = notInRange { range } 62 | 63 | fun
> Constraint
.notInRange(min: DT, max: DT) = notInRange(min..max) 64 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/CommonValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | inline fun
Constraint
.isEqualTo( 7 | crossinline other: (DT) -> DT 8 | ) = validation { 9 | subject == other(subject) 10 | } 11 | 12 | fun
Constraint
.isEqualTo(other: DT) = validation { 13 | subject == other 14 | } 15 | 16 | inline fun
Constraint
.isNotEqualTo( 17 | crossinline other: (DT) -> DT 18 | ) = validation { 19 | subject != other(subject) 20 | } 21 | 22 | fun
Constraint
.isNotEqualTo(other: DT) = validation { 23 | subject != other 24 | } 25 | 26 | inline fun
Constraint
.inValues( 27 | crossinline candidates: (DT) -> Iterable
28 | ) = validation { 29 | subject in candidates(subject) 30 | } 31 | 32 | fun
Constraint
.inValues(candidates: Iterable
) = inValues { candidates } 33 | 34 | fun
Constraint
.inValues(vararg candidates: DT) = validation { 35 | subject in candidates 36 | } 37 | 38 | inline fun
Constraint
.notInValues( 39 | crossinline candidates: (DT) -> Iterable
40 | ) = validation { 41 | subject !in candidates(subject) 42 | } 43 | 44 | fun
Constraint
.notInValues(candidates: Iterable
) = notInValues { candidates } 45 | 46 | fun
Constraint
.notInValues(vararg candidates: DT) = validation { 47 | subject !in candidates 48 | } 49 | 50 | inline fun
Constraint
.anyOf( 51 | crossinline constraint: Constraint
.() -> Unit 52 | ) = validation { 53 | ScopedConstraintBuilder
().apply { constraint() }.matchesAny(subject) 54 | } 55 | 56 | inline fun
Constraint
.allOf( 57 | crossinline constraint: Constraint
.() -> Unit 58 | ) = validation { 59 | ScopedConstraintBuilder
().apply { constraint() }.matchesAll(subject) 60 | } 61 | 62 | inline fun
Constraint
.noneOf( 63 | crossinline constraint: Constraint
.() -> Unit 64 | ) = validation { 65 | ScopedConstraintBuilder
().apply { constraint() }.matchesNone(subject) 66 | } 67 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/files/FileBuilder.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.files 2 | 3 | import dev.ahmedmourad.validation.compiler.codegen.CodeSectionGenerator 4 | import dev.ahmedmourad.validation.compiler.descriptors.ValidatorDescriptor 5 | import dev.ahmedmourad.validation.compiler.dsl.DslValidator 6 | import dev.ahmedmourad.validation.compiler.utils.OUTPUT_FOLDER 7 | import dev.ahmedmourad.validation.compiler.utils.SUFFIX_OUTPUT_FILE_NAME 8 | import org.jetbrains.kotlin.name.parentOrNull 9 | import org.jetbrains.kotlin.psi.KtObjectDeclaration 10 | import org.jetbrains.kotlin.utils.addToStdlib.safeAs 11 | import java.io.File 12 | 13 | internal class FileBuilder(private val dslValidator: DslValidator) { 14 | fun createFile( 15 | codeGenDir: File, 16 | validatorDescriptor: ValidatorDescriptor, 17 | vararg generators: CodeSectionGenerator 18 | ) { 19 | 20 | val fileName = validatorDescriptor.validatorClassOrObject.let { classOrObject -> 21 | if (classOrObject.safeAs()?.isCompanion() == true) { 22 | classOrObject.fqName?.parentOrNull()?.shortName()?.asString()?.plus("Companion") 23 | } else { 24 | classOrObject.fqName?.shortName()?.asString() 25 | } 26 | }?.plus(SUFFIX_OUTPUT_FILE_NAME) ?: dslValidator.reportError( 27 | "Unable to find validator name", 28 | validatorDescriptor.validatorClassOrObject 29 | ) 30 | 31 | val content = """ 32 | |package ${validatorDescriptor.packageName}.$OUTPUT_FOLDER 33 | | 34 | |${generators.flatMap { it.imports(validatorDescriptor) }.distinct().joinToString("\n") { "import $it" }} 35 | | 36 | |${generators.flatMap { it.generate(validatorDescriptor) }.joinToString("\n\n")} 37 | | 38 | """.trimMargin() 39 | 40 | val directory = File(codeGenDir, "${validatorDescriptor.packageAsPath}${File.separatorChar}$OUTPUT_FOLDER") 41 | val file = File(directory, "$fileName.kt") 42 | 43 | check(file.parentFile.exists() || file.parentFile.mkdirs()) { 44 | "Could not generate package directory: ${file.parentFile}" 45 | } 46 | 47 | file.writeText(content) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.utils 2 | 3 | import org.jetbrains.kotlin.name.FqName 4 | import org.jetbrains.kotlin.name.Name 5 | 6 | internal const val PACKAGE_BASE = "dev.ahmedmourad.validation" 7 | internal const val PACKAGE_CORE = "$PACKAGE_BASE.core" 8 | 9 | internal const val SUFFIX_VIOLATIONS_SUPER_CLASS = "Violation" 10 | internal const val SUFFIX_VALIDATION_CONTEXT = "ValidationContext" 11 | internal const val SUFFIX_VALIDATION_CONTEXT_IMPL = "Impl" 12 | internal const val SUFFIX_OUTPUT_FILE_NAME = "Validations" 13 | internal const val OUTPUT_FOLDER = "validations" 14 | 15 | internal val fqNameValidatorDescriptor = FqName("$PACKAGE_CORE.ValidatorDescriptor") 16 | internal val fqNameValidator = FqName("$PACKAGE_CORE.Validator") 17 | internal val fqNameConstraintDescriptor = FqName("$PACKAGE_CORE.ConstraintDescriptor") 18 | internal val fqNameValidationDescriptor = FqName("$PACKAGE_CORE.ValidationDescriptor") 19 | internal val fqNameIncludedValidatorDescriptor = FqName("$PACKAGE_CORE.IncludedValidatorDescriptor") 20 | internal val fqNameCase = FqName("$PACKAGE_CORE.Case") 21 | internal val fqNameMustBeValid = FqName("$PACKAGE_CORE.MustBeValid") 22 | internal val fqNameMeta = FqName("$PACKAGE_CORE.Meta") 23 | internal val fqNameMetaName = FqName("$PACKAGE_CORE.MetaName") 24 | internal val fqNameMetaType = FqName("$PACKAGE_CORE.MetaType") 25 | internal val fqNameInclusionType = FqName("$PACKAGE_CORE.InclusionType") 26 | internal val fqNameInternalValidationApi = FqName("$PACKAGE_CORE.InternalValidationApi") 27 | internal val fqNameValidatorConfig = FqName("$PACKAGE_CORE.ValidatorConfig") 28 | internal val fqNameSubjectHolder = FqName("$PACKAGE_CORE.SubjectHolder") 29 | 30 | internal val fqNameLegalFun = FqName("$PACKAGE_CORE.legal") 31 | internal val fqNameIllegalFun = FqName("$PACKAGE_CORE.illegal") 32 | internal val fqNameSwapFun = FqName("$PACKAGE_CORE.swap") 33 | internal val fqNameOrElseFun = FqName("$PACKAGE_CORE.orElse") 34 | internal val fqNameDescribeFun = FqName("$PACKAGE_CORE.describe") 35 | internal val fqNameConstraintFun = FqName("$PACKAGE_CORE.ConstraintsBuilder.constraint") 36 | 37 | internal val paramSubjectAlias = Name.identifier("subjectAlias") 38 | 39 | internal val propertyConstraints = Name.identifier("constraints") 40 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/StringValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | fun Constraint.isInteger() = validation { 6 | subject.toLongOrNull() != null 7 | } 8 | 9 | fun Constraint.isNumber() = validation { 10 | subject.toDoubleOrNull() != null 11 | } 12 | 13 | fun Constraint.isPositiveInteger(orZero: Boolean) = validation { 14 | if (orZero) { 15 | subject.toLongOrNull()?.let { it >= 0 } ?: false 16 | } else { 17 | subject.toLongOrNull()?.let { it > 0 } ?: false 18 | } 19 | } 20 | 21 | fun Constraint.isNegativeInteger(orZero: Boolean) = validation { 22 | if (orZero) { 23 | subject.toLongOrNull()?.let { it <= 0 } ?: false 24 | } else { 25 | subject.toLongOrNull()?.let { it < 0 } ?: false 26 | } 27 | } 28 | 29 | fun Constraint.isPositiveNumber(orZero: Boolean) = validation { 30 | if (orZero) { 31 | subject.toDoubleOrNull()?.let { it >= 0.0 } ?: false 32 | } else { 33 | subject.toDoubleOrNull()?.let { it > 0.0 } ?: false 34 | } 35 | } 36 | 37 | fun Constraint.isNegativeNumber(orZero: Boolean) = validation { 38 | if (orZero) { 39 | subject.toDoubleOrNull()?.let { it <= 0.0 } ?: false 40 | } else { 41 | subject.toDoubleOrNull()?.let { it < 0.0 } ?: false 42 | } 43 | } 44 | 45 | fun Constraint.isZero() = validation { 46 | (subject.toDoubleOrNull() ?: 1.0) == 0.0 47 | } 48 | 49 | fun Constraint.isNotZero() = validation { 50 | (subject.toDoubleOrNull() ?: 1.0) != 0.0 51 | } 52 | 53 | inline fun Constraint.isEqualTo( 54 | ignoreCase: Boolean, 55 | crossinline other: (String) -> String 56 | ) = validation { 57 | subject.equals(other(subject), ignoreCase) 58 | } 59 | 60 | fun Constraint.isEqualTo( 61 | other: String, 62 | ignoreCase: Boolean 63 | ) = isEqualTo(ignoreCase) { other } 64 | 65 | inline fun Constraint.isNotEqualTo( 66 | ignoreCase: Boolean, 67 | crossinline other: (String) -> String 68 | ) = validation { 69 | !subject.equals(other(subject), ignoreCase) 70 | } 71 | 72 | fun Constraint.isNotEqualTo( 73 | other: String, 74 | ignoreCase: Boolean 75 | ) = isNotEqualTo(ignoreCase) { other } 76 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | internal fun Iterable.contentEquals( 4 | ignoreDuplicates: Boolean, 5 | ignoreOrder: Boolean, 6 | other: Iterable 7 | ): Boolean { 8 | 9 | val thisWithDistinct = if (ignoreDuplicates) this.distinct() else this 10 | val otherWithDistinct = if (ignoreDuplicates) other.distinct() else other 11 | 12 | val thisCount = thisWithDistinct.count() 13 | if (thisCount != otherWithDistinct.count()) { 14 | return false 15 | } 16 | 17 | return if (ignoreOrder) { 18 | if (ignoreDuplicates) { 19 | thisWithDistinct.all { otherWithDistinct.contains(it) } 20 | } else { 21 | val otherWithOccurrences = otherWithDistinct.groupBy { it } 22 | thisWithDistinct.groupBy { it }.all { (item, occurrences) -> 23 | otherWithOccurrences[item]?.size == occurrences.size 24 | } 25 | } 26 | } else { 27 | for (i in 0 until thisCount) { 28 | if (thisWithDistinct.elementAt(i) != otherWithDistinct.elementAt(i)) { 29 | return false 30 | } 31 | } 32 | true 33 | } 34 | } 35 | 36 | internal fun Iterable.contentNotEquals( 37 | ignoreDuplicates: Boolean, 38 | ignoreOrder: Boolean, 39 | other: Iterable 40 | ): Boolean { 41 | 42 | val thisWithDistinct = if (ignoreDuplicates) this.distinct() else this 43 | val otherWithDistinct = if (ignoreDuplicates) other.distinct() else other 44 | 45 | val thisCount = thisWithDistinct.count() 46 | if (thisCount != otherWithDistinct.count()) { 47 | return true 48 | } 49 | 50 | return if (ignoreOrder) { 51 | if (ignoreDuplicates) { 52 | thisWithDistinct.any { !otherWithDistinct.contains(it) } 53 | } else { 54 | val otherWithOccurrences = otherWithDistinct.groupBy { it } 55 | thisWithDistinct.groupBy { it }.any { (item, occurrences) -> 56 | otherWithOccurrences[item]?.size != occurrences.size 57 | } 58 | } 59 | } else { 60 | for (i in 0 until thisCount) { 61 | if (thisWithDistinct.elementAt(i) != otherWithDistinct.elementAt(i)) { 62 | return true 63 | } 64 | } 65 | false 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/CompilerContext.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.intellij.openapi.project.Project 4 | import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot 5 | import org.jetbrains.kotlin.cli.common.config.kotlinSourceRoots 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder 8 | import org.jetbrains.kotlin.com.intellij.openapi.extensions.impl.ExtensionPointImpl 9 | import org.jetbrains.kotlin.config.CompilerConfiguration 10 | import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor 11 | import org.jetbrains.kotlin.utils.addToStdlib.cast 12 | import java.io.File 13 | 14 | internal data class CompilerContext( 15 | val messageCollector: MessageCollector, 16 | val configuration: CompilerConfiguration 17 | ) { 18 | val codeGenDir = createCodeGenDir(configuration) 19 | } 20 | 21 | private fun createCodeGenDir(configuration: CompilerConfiguration): File { 22 | 23 | val kotlinSourceRoots: List = configuration.kotlinSourceRoots 24 | 25 | fun kotlinValidationDir(parent: File): File { 26 | val file = File(parent, "kotlin-validation") 27 | check(file.exists() || file.mkdirs()) { 28 | "Could not create source generation directory: $file" 29 | } 30 | return file 31 | } 32 | 33 | val oneSourceFile = File(kotlinSourceRoots.first().path) 34 | val parentSequence = generateSequence(oneSourceFile) { it.parentFile } 35 | 36 | // Try to find the src dir. 37 | parentSequence.firstOrNull { it.name == "src" }?.let { 38 | return kotlinValidationDir(File(it.parentFile, "build")) 39 | } 40 | 41 | // If the src dir is not part of the input (incremental build), look for the build dir directly. 42 | parentSequence.firstOrNull { it.name == "build" }?.let { 43 | return kotlinValidationDir(it) 44 | } 45 | 46 | // This's here for testing purposes 47 | parentSequence.firstOrNull { it.name == "sources" }?.let { 48 | return kotlinValidationDir(File(it.parentFile, "build")) 49 | } 50 | 51 | throw IllegalStateException("Could not create source generation directory: $oneSourceFile") 52 | } 53 | 54 | internal fun ProjectExtensionDescriptor.registerExtensionAsFirst(project: Project, extension: T) { 55 | project.extensionArea 56 | .getExtensionPoint(extensionPointName) 57 | .cast>() 58 | .registerExtension(extension, LoadingOrder.LAST) 59 | } 60 | -------------------------------------------------------------------------------- /framework/gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("com.gradle.plugin-publish") version "0.12.0" 5 | `java-gradle-plugin` 6 | } 7 | 8 | apply(plugin = "org.jetbrains.kotlin.jvm") 9 | apply(plugin = "org.jetbrains.kotlin.kapt") 10 | apply(plugin = "com.vanniktech.maven.publish") 11 | 12 | group = "dev.ahmedmourad.validation" 13 | version = "0.1.0-SNAPSHOT" 14 | 15 | repositories { 16 | mavenCentral() 17 | jcenter() 18 | } 19 | 20 | gradlePlugin { 21 | plugins { 22 | create("validationPlugin") { 23 | id = "dev.ahmedmourad.validation.validation-gradle-plugin" 24 | implementationClass = "dev.ahmedmourad.validation.gradle.ValidationGradlePlugin" 25 | } 26 | } 27 | } 28 | 29 | val validationVersion: String by project 30 | 31 | pluginBundle { 32 | website = "http://validation.ahmedmourad.dev/" 33 | vcsUrl = "https://github.com/AhmedMourad0/kotlin-validation" 34 | description = "The Gradle plugin for kotlin-validation, A multiplatform, declarative, flexible and type-safe Kotlin validation framework." 35 | tags = listOf( 36 | "kotlin-compiler", 37 | "gradle-plugin", 38 | "intellij-plugin", 39 | "compiler-plugin", 40 | "data-class", 41 | "value-based", 42 | "annotations", 43 | "kotlin-extensions", 44 | "kotlin", 45 | "kotlin-language", 46 | "kotlin-library", 47 | "kotlin-compiler-plugin", 48 | "inspections", 49 | "kotlin-plugin" 50 | ) 51 | (plugins) { 52 | "validationPlugin" { 53 | displayName = "Kotlin Validation Gradle Plugin" 54 | version = validationVersion 55 | } 56 | } 57 | mavenCoordinates { 58 | groupId = "dev.ahmedmourad.validation" 59 | artifactId = "validation-gradle-plugin" 60 | version = validationVersion 61 | } 62 | } 63 | 64 | val compileKotlin: KotlinCompile by project 65 | compileKotlin.kotlinOptions { 66 | jvmTarget = "1.8" 67 | } 68 | 69 | tasks.jar { 70 | manifest { 71 | attributes["Specification-Title"] = project.name 72 | attributes["Specification-Version"] = project.version 73 | attributes["Implementation-Title"] = "dev.ahmedmourad.validation.validation-gradle-plugin" 74 | attributes["Implementation-Version"] = project.version 75 | } 76 | } 77 | 78 | dependencies { 79 | implementation(kotlin("stdlib")) 80 | implementation(kotlin("gradle-plugin-api")) 81 | implementation(kotlin("reflect")) 82 | compileOnly(kotlin("gradle-plugin")) 83 | } 84 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/ValidationSyntheticResolver.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import dev.ahmedmourad.validation.compiler.utils.fqNameMustBeValid 4 | import dev.ahmedmourad.validation.compiler.utils.error 5 | import dev.ahmedmourad.validation.compiler.utils.hasAnnotation 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.descriptors.ClassDescriptor 8 | import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor 9 | import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi 10 | import org.jetbrains.kotlin.name.Name 11 | import org.jetbrains.kotlin.resolve.BindingContext 12 | import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension 13 | 14 | open class ValidationSyntheticResolveExtension( 15 | private val messageCollector: MessageCollector? 16 | ) : SyntheticResolveExtension { 17 | 18 | override fun generateSyntheticMethods( 19 | thisDescriptor: ClassDescriptor, 20 | name: Name, 21 | bindingContext: BindingContext, 22 | fromSupertypes: List, 23 | result: MutableCollection 24 | ) { 25 | if (thisDescriptor.isData && name.asString() == "copy" && 26 | thisDescriptor.hasAnnotation(fqNameMustBeValid) 27 | ) { 28 | 29 | val generatedCopyMethodIndex = result.findGeneratedCopyMethodIndex(thisDescriptor) 30 | 31 | if (generatedCopyMethodIndex == null) { 32 | messageCollector?.error("Cannot find generated copy method!", null) 33 | return 34 | } 35 | 36 | result.remove(result.elementAt(generatedCopyMethodIndex)) 37 | 38 | } else { 39 | super.generateSyntheticMethods(thisDescriptor, name, bindingContext, fromSupertypes, result) 40 | } 41 | } 42 | } 43 | 44 | private fun Collection.findGeneratedCopyMethodIndex( 45 | classDescriptor: ClassDescriptor 46 | ): Int? { 47 | 48 | if (size == 1) { 49 | return 0 50 | } 51 | 52 | val primaryConstructor = classDescriptor.constructors.firstOrNull { it.isPrimary } ?: return null 53 | val primaryConstructorParameters = primaryConstructor.valueParameters 54 | 55 | val index = this.indexOfLast { 56 | it.name.asString() == "copy" 57 | && it.returnType == classDescriptor.defaultType 58 | && it.valueParameters.size == primaryConstructorParameters.size 59 | && it.valueParameters.filterIndexed { index, descriptor -> 60 | primaryConstructorParameters[index].type != descriptor.type && 61 | primaryConstructorParameters[index].name != descriptor.name 62 | }.isEmpty() 63 | } 64 | return if (index < 0) { 65 | null 66 | } else { 67 | index 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /framework/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/codegen/validations/ValidationContextGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.codegen.validations 2 | 3 | import dev.ahmedmourad.validation.compiler.codegen.CodeSectionGenerator 4 | import dev.ahmedmourad.validation.compiler.descriptors.ValidatorDescriptor 5 | import dev.ahmedmourad.validation.compiler.utils.SUFFIX_VALIDATION_CONTEXT 6 | import org.jetbrains.kotlin.types.TypeProjection 7 | 8 | internal class ValidationContextGenerator : CodeSectionGenerator { 9 | 10 | override fun imports(validatorDescriptor: ValidatorDescriptor) = emptySet() 11 | 12 | override fun generate( 13 | validatorDescriptor: ValidatorDescriptor 14 | ): Set { 15 | return setOf( 16 | generateValidationContext(validatorDescriptor), 17 | generateValidationContextImpl(validatorDescriptor) 18 | ) 19 | } 20 | 21 | private fun generateValidationContext( 22 | validatorDescriptor: ValidatorDescriptor 23 | ): String { 24 | 25 | val supertypes = validatorDescriptor.violations.flatMap { 26 | it.metas 27 | }.mapNotNull { 28 | it.includedValidator 29 | }.map { includedValidator -> 30 | 31 | val includedValidatorTypeArgs = includedValidator.validatorType 32 | .arguments 33 | .map(TypeProjection::toString) 34 | .takeIf(List::isNotEmpty) 35 | ?.joinToString(separator = ", ", prefix = "<", postfix = ">") 36 | .orEmpty() 37 | 38 | val includedSubjectSimpleName = includedValidator.subjectAliasOrSimpleName 39 | 40 | val validationsFileFqName = includedValidator.validationsFileFqName 41 | 42 | "$validationsFileFqName.$includedSubjectSimpleName$SUFFIX_VALIDATION_CONTEXT$includedValidatorTypeArgs" 43 | }.distinct() 44 | .takeIf(List::isNotEmpty) 45 | ?.joinToString(separator = ", ", prefix = " : ") 46 | .orEmpty() 47 | 48 | val validationContextName = validatorDescriptor.validationContextName 49 | val validatorTypeParams = validatorDescriptor.validatorTypeParams.let { 50 | if (supertypes.isNotBlank()) { 51 | it.trim() 52 | } else { 53 | it 54 | } 55 | } 56 | 57 | return "interface $validationContextName$validatorTypeParams$supertypes" 58 | } 59 | 60 | private fun generateValidationContextImpl(validatorDescriptor: ValidatorDescriptor): String { 61 | 62 | val validationContextName = validatorDescriptor.validationContextName 63 | val validationContextImplName = validatorDescriptor.validationContextImplName 64 | 65 | val validatorTypeParams = validatorDescriptor.validatorTypeParams 66 | val validatorTypeParamsAsTypeArgs = validatorDescriptor.validatorTypeParamsAsTypeArgs 67 | 68 | return if (validatorDescriptor.isValidationContextImplAnObject) { 69 | "private object $validationContextImplName : $validationContextName" 70 | } else { 71 | "private class $validationContextImplName$validatorTypeParams: $validationContextName$validatorTypeParamsAsTypeArgs" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/ValidationAnalysisHandlerExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.intellij.openapi.project.Project 4 | import dev.ahmedmourad.validation.compiler.codegen.CodeGenerator 5 | import dev.ahmedmourad.validation.compiler.dsl.DslValidator 6 | import org.jetbrains.kotlin.analyzer.AnalysisResult 7 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 8 | import org.jetbrains.kotlin.container.ComponentProvider 9 | import org.jetbrains.kotlin.context.ProjectContext 10 | import org.jetbrains.kotlin.descriptors.ModuleDescriptor 11 | import org.jetbrains.kotlin.psi.KtFile 12 | import org.jetbrains.kotlin.resolve.BindingContext 13 | import org.jetbrains.kotlin.resolve.BindingTrace 14 | import org.jetbrains.kotlin.resolve.extensions.AnalysisHandlerExtension 15 | 16 | internal fun interface CodeGeneratorFactory { 17 | fun create(bindingContext: BindingContext, dslValidator: DslValidator): CodeGenerator 18 | } 19 | 20 | internal class ValidationAnalysisHandlerExtension( 21 | private val ctx: CompilerContext, 22 | private val codeGeneratorsFactories: List 23 | ) : AnalysisHandlerExtension { 24 | 25 | private var didRecompile = false 26 | 27 | override fun doAnalysis( 28 | project: Project, 29 | module: ModuleDescriptor, 30 | projectContext: ProjectContext, 31 | files: Collection, 32 | bindingTrace: BindingTrace, 33 | componentProvider: ComponentProvider 34 | ): AnalysisResult? { 35 | 36 | 37 | // val resolveSession = componentProvider.get() 38 | // 39 | // resolveSession. 40 | 41 | // Tell the compiler that we have something to do in the analysisCompleted() method if 42 | // necessary. 43 | return null 44 | return if (!didRecompile) AnalysisResult.EMPTY else null 45 | } 46 | 47 | override fun analysisCompleted( 48 | project: Project, 49 | module: ModuleDescriptor, 50 | bindingTrace: BindingTrace, 51 | files: Collection 52 | ): AnalysisResult? { 53 | 54 | // ctx.messageCollector.report( 55 | // CompilerMessageSeverity.ERROR, 56 | // files.size.toString(), 57 | // null 58 | // ) 59 | 60 | if (didRecompile) { 61 | return super.analysisCompleted(project, module, bindingTrace, files) 62 | } 63 | didRecompile = true 64 | 65 | ctx.codeGenDir.listFiles()?.forEach { 66 | check(it.deleteRecursively()) { 67 | "Could not clean file: $it" 68 | } 69 | } 70 | 71 | val dslValidator = DslValidator(bindingTrace.bindingContext, ctx.messageCollector) 72 | 73 | codeGeneratorsFactories.map { 74 | it.create(bindingTrace.bindingContext, dslValidator) 75 | }.forEach { codeGenerator -> 76 | codeGenerator.generate(ctx.codeGenDir, module, files) 77 | } 78 | 79 | return AnalysisResult.RetryWithAdditionalRoots( 80 | bindingContext = bindingTrace.bindingContext, 81 | moduleDescriptor = module, 82 | additionalJavaRoots = emptyList(), 83 | additionalKotlinRoots = listOf(ctx.codeGenDir), 84 | addToEnvironment = true 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/Descriptors.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | @RequiresOptIn(level = RequiresOptIn.Level.ERROR) 4 | @MustBeDocumented 5 | @Retention(AnnotationRetention.BINARY) 6 | annotation class InternalValidationApi 7 | 8 | //TODO: 9 | @RequiresOptIn(level = RequiresOptIn.Level.ERROR) 10 | @MustBeDocumented 11 | @Retention(AnnotationRetention.BINARY) 12 | annotation class UnsafeValidationContext 13 | 14 | @Target(AnnotationTarget.CLASS) 15 | @Retention(AnnotationRetention.BINARY) 16 | annotation class MustBeValid 17 | 18 | @Target(AnnotationTarget.CLASS) 19 | @Retention(AnnotationRetention.BINARY) 20 | annotation class ValidatorConfig(val subjectAlias: String) 21 | 22 | data class ValidatorDescriptor internal constructor( 23 | private val values: List> 24 | ) : List> by values 25 | 26 | data class ConstraintDescriptor internal constructor( 27 | val violation: String, 28 | val includedValidators: List>, 29 | val validations: List>, 30 | val metadata: List> 31 | ) 32 | 33 | data class IncludedValidatorDescriptor>( 34 | val meta: String, 35 | private val binding: SubjectHolder.() -> Pair 36 | ) { 37 | 38 | @InternalValidationApi 39 | fun getBinding(item: T) = getBinding(SubjectHolder(item)) 40 | 41 | @InternalValidationApi 42 | fun getBinding(item: SubjectHolder) = binding.invoke(item) 43 | 44 | @InternalValidationApi 45 | inline fun isValid( 46 | item: T, 47 | crossinline isValid: C.(T1) -> Boolean 48 | ): Boolean = isValid(SubjectHolder(item), isValid) 49 | 50 | @InternalValidationApi 51 | inline fun isValid( 52 | item: SubjectHolder, 53 | crossinline isValid: C.(T1) -> Boolean 54 | ): Boolean { 55 | val (target, validator) = getBinding(item) 56 | return target?.let { 57 | validator.isValid(it) 58 | } ?: true 59 | } 60 | 61 | @InternalValidationApi 62 | inline fun findViolations( 63 | item: T, 64 | crossinline validate: C.(T1) -> Case, T1> 65 | ): Set = findViolations(SubjectHolder(item), validate) 66 | 67 | @InternalValidationApi 68 | inline fun findViolations( 69 | item: SubjectHolder, 70 | crossinline validate: C.(T1) -> Case, T1> 71 | ): Set { 72 | val (target, validator) = getBinding(item) 73 | return target?.let { 74 | validator.validate(it).swap().orElse { emptySet() } 75 | }.orEmpty() 76 | } 77 | } 78 | 79 | //TODO: both, and IncludedValidator should accept a SubjectHolder instead, and 80 | // it should be created once at call site 81 | data class MetadataDescriptor internal constructor( 82 | val name: String, 83 | private val get: SubjectHolder.() -> P 84 | ) { 85 | fun get(item: T) = get(SubjectHolder(item)) 86 | fun get(item: SubjectHolder) = get.invoke(item) 87 | } 88 | 89 | data class ValidationDescriptor
internal constructor( 90 | private val validate: SubjectHolder
.() -> Boolean 91 | ) { 92 | fun validate(item: DT) = validate(SubjectHolder(item)) 93 | fun validate(item: SubjectHolder
) = validate.invoke(item) 94 | } 95 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/MapValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | import dev.ahmedmourad.validation.core.ScopedConstraintBuilder 5 | 6 | fun Constraint>.isEmpty() = validation { 7 | subject.isEmpty() 8 | } 9 | 10 | fun Constraint>.isNotEmpty() = validation { 11 | subject.isNotEmpty() 12 | } 13 | 14 | inline fun > Constraint.minSize( 15 | crossinline min: (DTM) -> Int 16 | ) = validation { 17 | subject.size >= min(subject) 18 | } 19 | 20 | fun Constraint>.minSize(min: Int) = minSize { min } 21 | 22 | inline fun > Constraint.maxSize( 23 | crossinline max: (DTM) -> Int 24 | ) = validation { 25 | subject.size <= max(subject) 26 | } 27 | 28 | fun Constraint>.maxSize(max: Int) = maxSize { max } 29 | 30 | inline fun > Constraint.sizeLessThan( 31 | crossinline maxExclusive: (DTM) -> Int 32 | ) = validation { 33 | subject.size < maxExclusive(subject) 34 | } 35 | 36 | fun Constraint>.sizeLessThan(maxExclusive: Int) = sizeLessThan { maxExclusive } 37 | 38 | inline fun > Constraint.sizeLargerThan( 39 | crossinline minExclusive: (DTM) -> Int 40 | ) = validation { 41 | subject.size > minExclusive(subject) 42 | } 43 | 44 | fun Constraint>.sizeLargerThan(minExclusive: Int) = sizeLargerThan { minExclusive } 45 | 46 | inline fun > Constraint.sizeIn( 47 | crossinline range: (DTM) -> IntRange 48 | ) = validation { 49 | subject.size in range(subject) 50 | } 51 | 52 | fun Constraint>.sizeIn(range: IntRange) = sizeIn { range } 53 | 54 | fun Constraint>.sizeIn(min: Int, max: Int) = sizeIn(min..max) 55 | 56 | inline fun > Constraint.sizeNotIn( 57 | crossinline range: (DTM) -> IntRange 58 | ) = validation { 59 | subject.size !in range(subject) 60 | } 61 | 62 | fun Constraint>.sizeNotIn(range: IntRange) = sizeNotIn { range } 63 | 64 | fun Constraint>.sizeNotIn(min: Int, max: Int) = sizeNotIn(min..max) 65 | 66 | inline fun > Constraint.sizeEqualTo( 67 | crossinline value: (DTM) -> Int 68 | ) = validation { 69 | subject.size == value(subject) 70 | } 71 | 72 | fun Constraint>.sizeEqualTo(value: Int) = sizeEqualTo { value } 73 | 74 | inline fun > Constraint.sizeNotEqualTo( 75 | crossinline value: (DTM) -> Int 76 | ) = validation { 77 | subject.size != value(subject) 78 | } 79 | 80 | fun Constraint>.sizeNotEqualTo(value: Int) = sizeNotEqualTo { value } 81 | 82 | fun Constraint>.keys( 83 | keysConstraint: Constraint>.() -> Unit 84 | ) = on(Map::keys, keysConstraint) 85 | 86 | fun Constraint>.values( 87 | valuesConstraint: Constraint>.() -> Unit 88 | ) = on(Map::values, valuesConstraint) 89 | 90 | fun Constraint>.entries( 91 | entriesConstraint: Constraint>>.() -> Unit 92 | ) = on(Map::entries, entriesConstraint) 93 | 94 | fun Constraint>.key( 95 | keyConstraint: Constraint.() -> Unit 96 | ) = validation { 97 | ScopedConstraintBuilder().apply(keyConstraint).matchesAll(subject.key) 98 | } 99 | 100 | inline fun Constraint>.value( 101 | crossinline valueConstraint: Constraint.() -> Unit 102 | ) = validation { 103 | ScopedConstraintBuilder().apply(valueConstraint).matchesAll(subject.value) 104 | } 105 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/utils/PsiUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.utils 2 | 3 | import org.jetbrains.kotlin.descriptors.DeclarationDescriptor 4 | import org.jetbrains.kotlin.descriptors.annotations.Annotated 5 | import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor 6 | import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi 7 | import org.jetbrains.kotlin.name.FqName 8 | import org.jetbrains.kotlin.psi.* 9 | import org.jetbrains.kotlin.resolve.BindingContext 10 | import org.jetbrains.kotlin.resolve.bindingContextUtil.getAbbreviatedTypeOrType 11 | import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets 12 | import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe 13 | import org.jetbrains.kotlin.types.KotlinType 14 | import org.jetbrains.kotlin.types.typeUtil.isTypeParameter 15 | import org.jetbrains.kotlin.utils.addToStdlib.safeAs 16 | 17 | internal fun KtFile.classesAndInnerClasses(): Sequence { 18 | return generateSequence(findChildrenByClass(KtClassOrObject::class.java).toList()) { list -> 19 | list.flatMap { 20 | it.declarations.filterIsInstance() 21 | }.ifEmpty { null } 22 | }.flatMap { it.asSequence() } 23 | } 24 | 25 | internal fun KtClassOrObject.findSuperType(bindingContext: BindingContext, fqName: FqName): KtSuperTypeListEntry? { 26 | return this.superTypeListEntries.firstOrNull { 27 | it.typeReference?.kotlinType(bindingContext)?.fqNameSafe == fqName 28 | } 29 | } 30 | 31 | internal fun KtClassOrObject.hasSuperType(bindingContext: BindingContext, fqName: FqName): Boolean { 32 | return this.findSuperType(bindingContext, fqName) != null 33 | } 34 | 35 | internal fun KtTypeReference.kotlinType(bindingContext: BindingContext): KotlinType? { 36 | return bindingContext.get(BindingContext.TYPE, this) 37 | } 38 | 39 | internal fun Annotated.hasAnnotation(fqName: FqName): Boolean { 40 | return this.annotations.hasAnnotation(fqName) 41 | } 42 | 43 | internal fun Annotated.findAnnotation(fqName: FqName): AnnotationDescriptor? { 44 | return this.annotations.findAnnotation(fqName) 45 | } 46 | 47 | internal fun DeclarationDescriptor.ktFile(): KtFile? { 48 | return findPsi()?.containingFile.safeAs() 49 | } 50 | 51 | internal val KotlinType.fqNameSafe 52 | get() = this.constructor.declarationDescriptor?.fqNameSafe 53 | 54 | internal fun KotlinType.simpleName(): String? { 55 | return this.fqNameSafe?.shortName()?.asString() 56 | } 57 | 58 | internal fun KotlinType.deepFqName(): String { 59 | 60 | if (this.isTypeParameter()) { 61 | return this.toString() 62 | } 63 | 64 | val thisFqName = this.fqNameSafe?.asString() ?: return this.toString() 65 | 66 | val children = this.arguments.map { 67 | if (it.isStarProjection) { 68 | "*" 69 | } else { 70 | val variance = it.projectionKind.label 71 | "$variance ${it.type.deepFqName()}" 72 | }.trim() 73 | } 74 | 75 | val type = if (children.isNotEmpty()) { 76 | "$thisFqName<${children.joinToString(", ")}>" 77 | } else { 78 | thisFqName 79 | } 80 | 81 | return if (this.isMarkedNullable) { 82 | "$type?" 83 | } else { 84 | type 85 | } 86 | } 87 | 88 | internal fun KtTypeParameter.deepFqName(bindingContext: BindingContext): String { 89 | 90 | val variance = this.variance.label 91 | val name = this.nameAsSafeName.asString() 92 | val bound = this.extendsBound 93 | ?.typeElement 94 | ?.getAbbreviatedTypeOrType(bindingContext) 95 | ?.deepFqName() 96 | 97 | return if (bound == null) { 98 | "$variance $name" 99 | } else { 100 | "$variance $name : $bound" 101 | }.trim() 102 | } 103 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/IncludedValidatorDescriptorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.with 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.BeforeTest 8 | import kotlin.test.Test 9 | 10 | private const val TOO_SHORT = "TooShort" 11 | private const val EXTREMELY_SHORT = "ExtremelyShort" 12 | private const val TOO_LONG = "TooLong" 13 | 14 | @InternalValidationApi 15 | class IncludedValidatorDescriptorTests { 16 | 17 | object IntValidator : Validator { 18 | override val constraints by describe { 19 | constraint(violation = TOO_SHORT) { 20 | min(3) 21 | } 22 | constraint(violation = EXTREMELY_SHORT) { 23 | min(2) 24 | } 25 | constraint(violation = TOO_LONG) { 26 | max(7) 27 | } 28 | } 29 | } 30 | 31 | private lateinit var includedValidator: IncludedValidatorDescriptor> 32 | 33 | @BeforeTest 34 | fun setup() { 35 | includedValidator = IncludedValidatorDescriptor("lengthViolations") { 36 | subject.length to IntValidator 37 | } 38 | } 39 | 40 | @Test 41 | fun isValid_checksIfTheGivenConstraintsMatchTheItemGivenTheIsValidExtensionFunctionForTheValidator() { 42 | includedValidator.with { item: String -> 43 | isValid(item) { this.findViolatedConstraints(it).isEmpty() } 44 | }.allMatch( 45 | "123", 46 | "1234", 47 | "12345", 48 | "123456", 49 | "1234567" 50 | ).allFail( 51 | "", 52 | "1", 53 | "12", 54 | "12345678", 55 | "123456789" 56 | ) 57 | } 58 | 59 | @Test 60 | fun findViolations_checksIfTheGivenConstraintsMatchTheItemGivenTheIsValidExtensionFunctionForTheValidator() { 61 | includedValidator.with { item: Pair> -> 62 | findViolations(item.first) { 63 | this.findViolatedConstraints(it) 64 | .map(ConstraintDescriptor::violation) 65 | .toSet() 66 | .illegal() 67 | }.contentEquals( 68 | ignoreDuplicates = false, 69 | ignoreOrder = true, 70 | other = item.second 71 | ) 72 | }.allMatch( 73 | "123" to emptyList(), 74 | "1234" to emptyList(), 75 | "12345" to emptyList(), 76 | "123456" to emptyList(), 77 | "1234567" to emptyList(), 78 | "" to listOf(TOO_SHORT, EXTREMELY_SHORT), 79 | "1" to listOf(TOO_SHORT, EXTREMELY_SHORT), 80 | "12" to listOf(TOO_SHORT), 81 | "12345678" to listOf(TOO_LONG), 82 | "123456789" to listOf(TOO_LONG) 83 | ).allFail( 84 | "123" to listOf(TOO_SHORT), 85 | "1234" to listOf(TOO_LONG), 86 | "12345" to listOf(TOO_SHORT), 87 | "123456" to listOf(TOO_LONG), 88 | "1234567" to listOf(TOO_SHORT), 89 | "" to emptyList(), 90 | "1" to emptyList(), 91 | "12" to listOf(EXTREMELY_SHORT), 92 | "12345678" to emptyList(), 93 | "123456789" to emptyList() 94 | ) 95 | } 96 | 97 | private fun Validator.findViolatedConstraints(item: T): List> { 98 | return this.constraints.filterNot { constraint -> 99 | constraint.validations.all { validation -> 100 | validation.validate(item) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /validators/src/commonMain/kotlin/dev/ahmedmourad/validation/validators/IsbnValidator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.validators 2 | 3 | import dev.ahmedmourad.validation.core.* 4 | import dev.ahmedmourad.validation.core.validations.* 5 | 6 | @ValidatorConfig(subjectAlias = "Isbn") 7 | class IsbnValidator : Validator { 8 | override val constraints by describe { 9 | val parts = evaluate { subject.split('-') } 10 | constraint(violation = "MalformedIsbnCode") { 11 | meta("actual") { subject.length } 12 | hasCorrectPartsCount() 13 | on(parts) { 14 | forAll { 15 | doesNotContainWhiteSpaces() 16 | isPositiveInteger(orZero = true) 17 | } 18 | } 19 | } 20 | constraint(violation = "InvalidEanPrefix") { 21 | val prefix = evaluate { 22 | if (parts.get().size == 5) { 23 | parts.get().firstOrNull() 24 | } else { 25 | null 26 | } 27 | } 28 | meta("value", prefix) 29 | on(prefix) ifExists { 30 | inValues("978", "979") 31 | } 32 | } 33 | constraint(violation = "InvalidRegistrationGroup") { 34 | val group = evaluate { 35 | if (parts.get().size == 5) { 36 | parts.get().getOrNull(1) 37 | } else { 38 | parts.get().getOrNull(0) 39 | } 40 | } 41 | meta("value", group) 42 | on(group) ifExists { 43 | lengthIn(1, 5) 44 | } 45 | } 46 | constraint(violation = "InvalidRegistrant") { 47 | val registrant = evaluate { 48 | if (parts.get().size == 5) { 49 | parts.get().getOrNull(2) 50 | } else { 51 | parts.get().getOrNull(1) 52 | } 53 | } 54 | meta("value", registrant) 55 | on(registrant) ifExists { 56 | lengthIn(1, 7) 57 | } 58 | } 59 | constraint(violation = "InvalidPublication") { 60 | val publication = evaluate { 61 | if (parts.get().size == 5) { 62 | parts.get().getOrNull(3) 63 | } else { 64 | parts.get().getOrNull(2) 65 | } 66 | } 67 | meta("value", publication) 68 | on(publication) ifExists { 69 | lengthIn(1, 6) 70 | } 71 | } 72 | constraint(violation = "InvalidCheckDigit") { 73 | meta("value") { parts.get().lastOrNull() } 74 | isCheckDigitInRange() 75 | isCorrectCheckDigit() 76 | } 77 | } 78 | } 79 | 80 | fun Constraint.hasCorrectPartsCount() = validation { 81 | val parts = subject.split('-') 82 | when (subject.length) { 83 | 10 + 3 -> parts.size == 4 84 | 13 + 4 -> parts.size == 5 85 | else -> false 86 | } 87 | } 88 | 89 | fun Constraint.isCheckDigitInRange() = validation { 90 | val digit = subject.split('-').last() 91 | when (subject.length) { 92 | 10 + 3 -> digit.equals("x", true) || digit.toIntOrNull() in 0..9 93 | 13 + 4 -> digit.toIntOrNull() in 0..9 94 | else -> false 95 | } 96 | } 97 | 98 | fun Constraint.isCorrectCheckDigit() = validation { 99 | 100 | val digits = subject.replace("-", "") 101 | .map(kotlin.Char::digitToInt) 102 | 103 | if (subject.length == 13) { 104 | digits.mapIndexed { index, n -> 105 | n * (10 - index) 106 | }.sum() % 11 107 | } else { 108 | digits.mapIndexed { index, n -> 109 | n * ((index % 2) * 2 + 1) 110 | }.sum() % 10 111 | } == 0 112 | } 113 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/main/kotlin/dev/ahmedmourad/validation/compiler/descriptors/ValidatorDescriptor.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler.descriptors 2 | 3 | import dev.ahmedmourad.validation.compiler.utils.* 4 | import dev.ahmedmourad.validation.compiler.utils.SUFFIX_VALIDATION_CONTEXT_IMPL 5 | import dev.ahmedmourad.validation.compiler.utils.SUFFIX_VALIDATION_CONTEXT 6 | import dev.ahmedmourad.validation.compiler.utils.SUFFIX_VIOLATIONS_SUPER_CLASS 7 | import dev.ahmedmourad.validation.compiler.utils.simpleName 8 | import dev.ahmedmourad.validation.compiler.dsl.DslValidator 9 | import org.jetbrains.kotlin.descriptors.ConstructorDescriptor 10 | import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor 11 | import org.jetbrains.kotlin.psi.KtClassOrObject 12 | import org.jetbrains.kotlin.psi.KtFile 13 | import org.jetbrains.kotlin.resolve.BindingContext 14 | import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassDescriptor 15 | import org.jetbrains.kotlin.types.KotlinType 16 | import org.jetbrains.kotlin.utils.addToStdlib.cast 17 | import org.jetbrains.kotlin.utils.addToStdlib.safeAs 18 | import java.io.File 19 | 20 | internal class ValidatorDescriptor( 21 | bindingContext: BindingContext, 22 | dslValidator: DslValidator, 23 | val subjectType: KotlinType, 24 | val validatorClassOrObject: KtClassOrObject, 25 | val violations: List 26 | ) { 27 | 28 | val packageName by lazy { validatorClassOrObject.containingFile.cast().packageFqName.asString() } 29 | val packageAsPath by lazy { packageName.replace('.', File.separatorChar) } 30 | 31 | val subjectClass by lazy { 32 | subjectType.constructor 33 | .declarationDescriptor 34 | ?.safeAs() 35 | } 36 | 37 | private val subjectTypeArgs by lazy { 38 | subjectType.arguments 39 | .map { it.toString() } 40 | .takeIf(List::isNotEmpty) 41 | ?.joinToString(separator = ", ", prefix = "<", postfix = ">") 42 | .orEmpty() 43 | } 44 | 45 | val subjectFqName by lazy { 46 | subjectType 47 | .fqNameSafe 48 | ?.asString() 49 | ?.plus(subjectTypeArgs) 50 | } 51 | 52 | private val subjectAlias by lazy { 53 | 54 | val (annotation, entry) = validatorClassOrObject.annotationEntries.firstNotNullOfOrNull { annotationEntry -> 55 | bindingContext.get(BindingContext.ANNOTATION, annotationEntry).takeIf { 56 | it?.fqName == fqNameValidatorConfig 57 | } to annotationEntry 58 | } ?: (null to null) 59 | 60 | annotation?.allValueArguments 61 | ?.get(paramSubjectAlias) 62 | ?.value 63 | ?.safeAs() 64 | ?.let { dslValidator.verifySubjectAlias(it, entry) } 65 | } 66 | 67 | private val subjectAliasOrSimpleName by lazy { 68 | subjectAlias ?: subjectType.simpleName() 69 | } 70 | 71 | val validatorTypeParamsAsTypeArgs by lazy { 72 | validatorClassOrObject 73 | .typeParameters 74 | .map { it.nameAsSafeName.asString() } 75 | .takeIf(List::isNotEmpty) 76 | ?.joinToString(separator = ", ", prefix = "<", postfix = ">") 77 | .orEmpty() 78 | } 79 | 80 | val validatorTypeParams by lazy { 81 | validatorClassOrObject 82 | .typeParameters 83 | .map { it.deepFqName(bindingContext) } 84 | .takeIf(List::isNotEmpty) 85 | ?.joinToString(separator = ", ", prefix = "<", postfix = ">") 86 | ?.plus(" ") 87 | .orEmpty() 88 | } 89 | 90 | val validatorFqName by lazy { 91 | validatorClassOrObject 92 | .fqName 93 | ?.asString() 94 | ?.plus(validatorTypeParamsAsTypeArgs) 95 | } 96 | 97 | val violationsParentName by lazy { subjectAliasOrSimpleName!! + SUFFIX_VIOLATIONS_SUPER_CLASS } 98 | 99 | val validationContextName by lazy { subjectAliasOrSimpleName!! + SUFFIX_VALIDATION_CONTEXT } 100 | val validationContextImplName by lazy { validationContextName + SUFFIX_VALIDATION_CONTEXT_IMPL } 101 | 102 | val isValidationContextImplAnObject by lazy { validatorTypeParams.isBlank() } 103 | } 104 | -------------------------------------------------------------------------------- /validators/src/commonMain/kotlin/dev/ahmedmourad/validation/validators/PasswordValidator.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.validators 2 | 3 | import dev.ahmedmourad.validation.core.* 4 | import dev.ahmedmourad.validation.core.validations.* 5 | 6 | @ValidatorConfig(subjectAlias = "Password") 7 | class PasswordValidator( 8 | private val minLength: Int? = null, 9 | private val maxLength: Int? = null, 10 | private val minUpperCaseLettersCount: Int? = null, 11 | private val minLowerCaseLettersCount: Int? = null, 12 | private val minDigitsCount: Int? = null, 13 | private val minSymbolsCount: Int? = null, 14 | private val minDistinctCount: Int? = null, 15 | private val allowSequence: Boolean = true 16 | ) : Validator { 17 | override val constraints by describe { 18 | val minLength = this@PasswordValidator.minLength 19 | val maxLength = this@PasswordValidator.maxLength 20 | val minUpperCaseLettersCount = this@PasswordValidator.minUpperCaseLettersCount 21 | val minLowerCaseLettersCount = this@PasswordValidator.minLowerCaseLettersCount 22 | val minDigitsCount = this@PasswordValidator.minDigitsCount 23 | val minSymbolsCount = this@PasswordValidator.minSymbolsCount 24 | val minDistinctCount = this@PasswordValidator.minDistinctCount 25 | val allowSequential = this@PasswordValidator.allowSequence 26 | constraint(violation = "TooShort") { 27 | meta("min") { minLength } 28 | meta("actual") { subject.length } 29 | if (minLength != null) { 30 | minLength(minLength) 31 | } 32 | } 33 | constraint(violation = "TooLong") { 34 | meta("max") { maxLength } 35 | meta("actual") { subject.length } 36 | if (maxLength != null) { 37 | maxLength(maxLength) 38 | } 39 | } 40 | constraint(violation = "FewUpperCaseLetters") { 41 | val count = evaluate { subject.count(Char::isUpperCase) } 42 | meta("min") { minUpperCaseLettersCount } 43 | meta("actual", count) 44 | if (minUpperCaseLettersCount != null) { 45 | validation { count.get() >= minUpperCaseLettersCount } 46 | } 47 | } 48 | constraint(violation = "FewLowerCaseLetters") { 49 | val count = evaluate { subject.count(Char::isLowerCase) } 50 | meta("min") { minLowerCaseLettersCount } 51 | meta("actual", count) 52 | if (minLowerCaseLettersCount != null) { 53 | validation { count.get() >= minLowerCaseLettersCount } 54 | } 55 | } 56 | constraint(violation = "FewDigits") { 57 | val count = evaluate { subject.count(Char::isDigit) } 58 | meta("min") { minDigitsCount } 59 | meta("actual", count) 60 | if (minDigitsCount != null) { 61 | validation { count.get() >= minDigitsCount } 62 | } 63 | } 64 | constraint(violation = "FewSymbols") { 65 | val count = evaluate { subject.count { !it.isLetterOrDigit() } } 66 | meta("min") { minSymbolsCount } 67 | meta("actual", count) 68 | if (minSymbolsCount != null) { 69 | validation { count.get() >= minSymbolsCount } 70 | } 71 | } 72 | constraint(violation = "FewDistinctCharacters") { 73 | val count = evaluate { subject.toCharArray().distinct().size } 74 | meta("min") { minDistinctCount } 75 | meta("actual", count) 76 | if (minDistinctCount != null) { 77 | validation { count.get() >= minDistinctCount } 78 | } 79 | } 80 | constraint(violation = "OnlySequentialCharactersOrDigits") { 81 | if (!allowSequential) { 82 | isNotOnlySequentialChars() 83 | } 84 | } 85 | } 86 | } 87 | 88 | fun Constraint.isNotOnlySequentialChars() = validation { 89 | 90 | if (subject.length < 3) { 91 | return@validation true 92 | } 93 | 94 | //TODO: might cause problems for letters 95 | val gap = subject[1].code - subject[0].code 96 | 97 | for (i in 1..subject.lastIndex) { 98 | if (subject[i].code - subject[i - 1].code != gap) { 99 | return@validation true 100 | } 101 | } 102 | false 103 | } 104 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ScopedConstraintBuilderTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.validations.* 4 | import kotlin.test.* 5 | import kotlin.test.Test 6 | 7 | @InternalValidationApi 8 | class ScopedConstraintBuilderTests { 9 | 10 | private data class Model(val n: Int?) 11 | 12 | @Test 13 | fun validation_addsDirectValidationToThisConstraint() { 14 | 15 | val constraint = ScopedConstraintBuilder().apply { 16 | validation { true } 17 | } 18 | 19 | assertTrue(constraint.matchesAll(Unit)) 20 | 21 | val constraint1 = ScopedConstraintBuilder().apply { 22 | validation { false } 23 | } 24 | 25 | assertFalse(constraint1.matchesAll(Unit)) 26 | } 27 | 28 | @Test 29 | fun on_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 30 | 31 | val constraint = ScopedConstraintBuilder().apply { 32 | on(String::length) { 33 | max(5) 34 | } 35 | } 36 | 37 | assertTrue(constraint.matchesAll("12345")) 38 | assertFalse(constraint.matchesAll("123456")) 39 | } 40 | 41 | @Test 42 | fun on1_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 43 | 44 | val constraint = ScopedConstraintBuilder().apply { 45 | on({ subject.length }) { 46 | max(5) 47 | } 48 | } 49 | 50 | assertTrue(constraint.matchesAll("12345")) 51 | assertFalse(constraint.matchesAll("123456")) 52 | } 53 | 54 | @Test 55 | fun on2_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 56 | 57 | val constraint = ScopedConstraintBuilder().apply { 58 | on("1234"::length) { 59 | max(5) 60 | } 61 | } 62 | 63 | assertTrue(constraint.matchesAll("12345")) 64 | assertTrue(constraint.matchesAll("123456")) 65 | } 66 | 67 | @Test 68 | fun ifExists_appliesTheValidationsIfTheValueIsNotNullOtherwiseItSucceeds() { 69 | 70 | val constraint = ScopedConstraintBuilder().apply { 71 | on(Model::n) ifExists { 72 | max(5) 73 | } 74 | } 75 | 76 | assertTrue(constraint.matchesAll(Model(null))) 77 | assertTrue(constraint.matchesAll(Model(3))) 78 | assertFalse(constraint.matchesAll(Model(6))) 79 | } 80 | 81 | @Test 82 | fun mustExists_appliesTheValidationsIfTheValueIsNotNullOtherwiseItFails() { 83 | 84 | val constraint = ScopedConstraintBuilder().apply { 85 | on(Model::n) mustExist { 86 | max(5) 87 | } 88 | } 89 | 90 | assertFalse(constraint.matchesAll(Model(null))) 91 | assertTrue(constraint.matchesAll(Model(3))) 92 | assertFalse(constraint.matchesAll(Model(6))) 93 | } 94 | 95 | @Test 96 | fun validateAll_returnsTrueIfTheItemMatchesAllTheValidations() { 97 | 98 | val constraint = ScopedConstraintBuilder().apply { 99 | max(3) 100 | isEven() 101 | } 102 | 103 | assertTrue(constraint.matchesAll(-2)) 104 | assertTrue(constraint.matchesAll(0)) 105 | assertTrue(constraint.matchesAll(2)) 106 | assertFalse(constraint.matchesAll(1)) 107 | assertFalse(constraint.matchesAll(3)) 108 | assertFalse(constraint.matchesAll(4)) 109 | assertFalse(constraint.matchesAll(5)) 110 | assertFalse(constraint.matchesAll(6)) 111 | } 112 | 113 | @Test 114 | fun validateAny_returnsTrueIfTheItemMatchesAnyOfTheValidations() { 115 | 116 | val constraint = ScopedConstraintBuilder().apply { 117 | max(3) 118 | isEven() 119 | } 120 | 121 | assertTrue(constraint.matchesAny(-2)) 122 | assertTrue(constraint.matchesAny(-1)) 123 | assertTrue(constraint.matchesAny(0)) 124 | assertTrue(constraint.matchesAny(1)) 125 | assertTrue(constraint.matchesAny(2)) 126 | assertTrue(constraint.matchesAny(3)) 127 | assertTrue(constraint.matchesAny(4)) 128 | assertTrue(constraint.matchesAny(6)) 129 | assertFalse(constraint.matchesAny(5)) 130 | assertFalse(constraint.matchesAny(7)) 131 | assertFalse(constraint.matchesAny(9)) 132 | } 133 | 134 | @Test 135 | fun validateNone_returnsTrueIfTheItemMatchesNoneOfTheValidations() { 136 | 137 | val constraint = ScopedConstraintBuilder().apply { 138 | max(3) 139 | isEven() 140 | } 141 | 142 | assertTrue(constraint.matchesNone(5)) 143 | assertTrue(constraint.matchesNone(7)) 144 | assertTrue(constraint.matchesNone(9)) 145 | assertFalse(constraint.matchesNone(-2)) 146 | assertFalse(constraint.matchesNone(-1)) 147 | assertFalse(constraint.matchesNone(0)) 148 | assertFalse(constraint.matchesNone(1)) 149 | assertFalse(constraint.matchesNone(2)) 150 | assertFalse(constraint.matchesNone(3)) 151 | assertFalse(constraint.matchesNone(4)) 152 | assertFalse(constraint.matchesNone(6)) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/CommonValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class CommonValidationsTests { 10 | 11 | @Test 12 | fun isEqualTo_meansThisObjectIsEqualToTheGivenObject() { 13 | constraint { 14 | isEqualTo(5) 15 | }.allMatch( 16 | 5 17 | ).allFail( 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | 6, 23 | 7 24 | ) 25 | } 26 | 27 | @Test 28 | fun isEqualToL_meansThisObjectIsEqualToTheGivenObject() { 29 | constraint { 30 | isEqualTo { 5 } 31 | }.allMatch( 32 | 5 33 | ).allFail( 34 | 1, 35 | 2, 36 | 3, 37 | 4, 38 | 6, 39 | 7 40 | ) 41 | } 42 | 43 | @Test 44 | fun isNotEqualTo_meansThisObjectIsNotEqualToTheGivenObject() { 45 | constraint { 46 | isNotEqualTo(5) 47 | }.allMatch( 48 | 1, 49 | 2, 50 | 3, 51 | 4, 52 | 6, 53 | 7 54 | ).allFail( 55 | 5 56 | ) 57 | } 58 | 59 | @Test 60 | fun isNotEqualToL_meansThisObjectIsNotEqualToTheGivenObject() { 61 | constraint { 62 | isNotEqualTo { 5 } 63 | }.allMatch( 64 | 1, 65 | 2, 66 | 3, 67 | 4, 68 | 6, 69 | 7 70 | ).allFail( 71 | 5 72 | ) 73 | } 74 | 75 | @Test 76 | fun inValues_meansThisObjectEqualsAnyOfTheGivenObjects() { 77 | constraint { 78 | inValues(3, 4, 5) 79 | }.allMatch( 80 | 3, 81 | 4, 82 | 5 83 | ).allFail( 84 | 1, 85 | 2, 86 | 6, 87 | 7 88 | ) 89 | } 90 | 91 | @Test 92 | fun inValuesL_meansThisObjectEqualsAnyOfTheGivenObjects() { 93 | constraint { 94 | inValues(listOf(3, 4, 5)) 95 | }.allMatch( 96 | 3, 97 | 4, 98 | 5 99 | ).allFail( 100 | 1, 101 | 2, 102 | 6, 103 | 7 104 | ) 105 | } 106 | 107 | @Test 108 | fun inValuesLL_meansThisObjectEqualsAnyOfTheGivenObjects() { 109 | constraint { 110 | inValues { (listOf(3, 4, 5)) } 111 | }.allMatch( 112 | 3, 113 | 4, 114 | 5 115 | ).allFail( 116 | 1, 117 | 2, 118 | 6, 119 | 7 120 | ) 121 | } 122 | 123 | @Test 124 | fun notInValues_meansThisObjectDoesNotEqualAnyOfTheGivenObjects() { 125 | constraint { 126 | notInValues(3, 4, 5) 127 | }.allMatch( 128 | 1, 129 | 2, 130 | 6, 131 | 7 132 | ).allFail( 133 | 3, 134 | 4, 135 | 5 136 | ) 137 | } 138 | 139 | @Test 140 | fun notInValuesL_meansThisObjectDoesNotEqualAnyOfTheGivenObjects() { 141 | constraint { 142 | notInValues(listOf(3, 4, 5)) 143 | }.allMatch( 144 | 1, 145 | 2, 146 | 6, 147 | 7 148 | ).allFail( 149 | 3, 150 | 4, 151 | 5 152 | ) 153 | } 154 | 155 | @Test 156 | fun notInValuesLL_meansThisObjectDoesNotEqualAnyOfTheGivenObjects() { 157 | constraint { 158 | notInValues { (listOf(3, 4, 5)) } 159 | }.allMatch( 160 | 1, 161 | 2, 162 | 6, 163 | 7 164 | ).allFail( 165 | 3, 166 | 4, 167 | 5 168 | ) 169 | } 170 | 171 | @Test 172 | fun anyOf_meansTheGivenObjectMatchesAtLeastOneOfTheValidations() { 173 | constraint { 174 | anyOf { 175 | isEqualTo(1) 176 | isEqualTo(2) 177 | isEqualTo(3) 178 | isEqualTo(4) 179 | } 180 | }.allMatch( 181 | 1, 182 | 2, 183 | 3, 184 | 4 185 | ).allFail( 186 | 0, 187 | 5, 188 | 6, 189 | 7 190 | ) 191 | } 192 | 193 | @Test 194 | fun allOf_meansTheGivenObjectMatchesAllTheValidations() { 195 | constraint { 196 | allOf { 197 | min(3) 198 | max(5) 199 | } 200 | }.allMatch( 201 | 3, 202 | 4, 203 | 5 204 | ).allFail( 205 | 0, 206 | 1, 207 | 2, 208 | 6, 209 | 7 210 | ) 211 | } 212 | 213 | @Test 214 | fun noneOf_meansTheGivenObjectMatchesNoneOfTheValidations() { 215 | constraint { 216 | noneOf { 217 | startsWith("a") 218 | endsWith("z") 219 | inValues("Ahmed", "Mourad") 220 | } 221 | }.allMatch( 222 | "Something", 223 | "That's", 224 | "Valid" 225 | ).allFail( 226 | "a", 227 | "anchor", 228 | "Ahmed", 229 | "Mourad", 230 | "Zebraz" 231 | ) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ComparableValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class ComparableValidationsTests { 10 | 11 | @Test 12 | fun min_meansThisComparableIsLargerThanOrEqualToTheGivenComparable() { 13 | constraint { 14 | min(3) 15 | }.allMatch( 16 | 3, 17 | 4, 18 | 5, 19 | 6 20 | ).allFail( 21 | -1, 22 | 0, 23 | 1, 24 | 2 25 | ) 26 | } 27 | 28 | @Test 29 | fun minL_meansThisComparableIsLargerThanOrEqualToTheGivenComparable() { 30 | constraint { 31 | min { 3 } 32 | }.allMatch( 33 | 3, 34 | 4, 35 | 5, 36 | 6 37 | ).allFail( 38 | -1, 39 | 0, 40 | 1, 41 | 2 42 | ) 43 | } 44 | 45 | @Test 46 | fun max_meansThisComparableIsLessThanOrEqualToTheGivenComparable() { 47 | constraint { 48 | max(3) 49 | }.allMatch( 50 | -1, 51 | 0, 52 | 1, 53 | 2, 54 | 3 55 | ).allFail( 56 | 4, 57 | 5, 58 | 6, 59 | 7 60 | ) 61 | } 62 | 63 | @Test 64 | fun maxL_meansThisComparableIsLessThanOrEqualToTheGivenComparable() { 65 | constraint { 66 | max { 3 } 67 | }.allMatch( 68 | -1, 69 | 0, 70 | 1, 71 | 2, 72 | 3 73 | ).allFail( 74 | 4, 75 | 5, 76 | 6, 77 | 7 78 | ) 79 | } 80 | 81 | @Test 82 | fun lessThan_meansThisComparableIsLessThanTheGivenComparable() { 83 | constraint { 84 | lessThan(3) 85 | }.allMatch( 86 | -1, 87 | 0, 88 | 1, 89 | 2 90 | ).allFail( 91 | 3, 92 | 4, 93 | 5, 94 | 6 95 | ) 96 | } 97 | 98 | @Test 99 | fun lessThanL_meansThisComparableIsLessThanTheGivenComparable() { 100 | constraint { 101 | lessThan { 3 } 102 | }.allMatch( 103 | -1, 104 | 0, 105 | 1, 106 | 2 107 | ).allFail( 108 | 3, 109 | 4, 110 | 5, 111 | 6 112 | ) 113 | } 114 | 115 | @Test 116 | fun largerThan_meansThisComparableIsLargerThanTheGivenComparable() { 117 | constraint { 118 | largerThan(3) 119 | }.allMatch( 120 | 4, 121 | 5, 122 | 6, 123 | 7 124 | ).allFail( 125 | -1, 126 | 0, 127 | 1, 128 | 2, 129 | 3 130 | ) 131 | } 132 | 133 | @Test 134 | fun largerThanL_meansThisComparableIsLargerThanTheGivenComparable() { 135 | constraint { 136 | largerThan { 3 } 137 | }.allMatch( 138 | 4, 139 | 5, 140 | 6, 141 | 7 142 | ).allFail( 143 | -1, 144 | 0, 145 | 1, 146 | 2, 147 | 3 148 | ) 149 | } 150 | 151 | @Test 152 | fun inRange_meansThisComparableIsInTheGivenRange() { 153 | constraint { 154 | inRange(3, 5) 155 | }.allMatch( 156 | 3, 157 | 4, 158 | 5 159 | ).allFail( 160 | -1, 161 | 0, 162 | 1, 163 | 2, 164 | 6, 165 | 7 166 | ) 167 | } 168 | 169 | @Test 170 | fun inRangeR_meansThisComparableIsInTheGivenRange() { 171 | constraint { 172 | inRange(3..5) 173 | }.allMatch( 174 | 3, 175 | 4, 176 | 5 177 | ).allFail( 178 | -1, 179 | 0, 180 | 1, 181 | 2, 182 | 6, 183 | 7 184 | ) 185 | } 186 | 187 | @Test 188 | fun inRangeRL_meansThisComparableIsInTheGivenRange() { 189 | constraint { 190 | inRange { 3..5 } 191 | }.allMatch( 192 | 3, 193 | 4, 194 | 5 195 | ).allFail( 196 | -1, 197 | 0, 198 | 1, 199 | 2, 200 | 6, 201 | 7 202 | ) 203 | } 204 | 205 | @Test 206 | fun notInRange_meansThisComparableIsNotInTheGivenRange() { 207 | constraint { 208 | notInRange(3, 5) 209 | }.allMatch( 210 | -1, 211 | 0, 212 | 1, 213 | 2, 214 | 6, 215 | 7 216 | ).allFail( 217 | 3, 218 | 4, 219 | 5 220 | ) 221 | } 222 | 223 | @Test 224 | fun notInRangeR_meansThisComparableIsNotInTheGivenRange() { 225 | constraint { 226 | notInRange(3..5) 227 | }.allMatch( 228 | -1, 229 | 0, 230 | 1, 231 | 2, 232 | 6, 233 | 7 234 | ).allFail( 235 | 3, 236 | 4, 237 | 5 238 | ) 239 | } 240 | 241 | @Test 242 | fun notInRangeRL_meansThisComparableIsNotInTheGivenRange() { 243 | constraint { 244 | notInRange { 3..5 } 245 | }.allMatch( 246 | -1, 247 | 0, 248 | 1, 249 | 2, 250 | 6, 251 | 7 252 | ).allFail( 253 | 3, 254 | 4, 255 | 5 256 | ) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /framework/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /framework/core/src/commonMain/kotlin/dev/ahmedmourad/validation/core/validations/CharSequenceValidations.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core.validations 2 | 3 | import dev.ahmedmourad.validation.core.Constraint 4 | 5 | //TODO: forAll, forAny, forNone 6 | inline fun
Constraint
.minLength( 7 | crossinline min: (DT) -> Int 8 | ) = validation { 9 | subject.length >= min(subject) 10 | } 11 | 12 | fun
Constraint
.minLength(min: Int) = minLength { min } 13 | 14 | inline fun
Constraint
.maxLength( 15 | crossinline max: (DT) -> Int 16 | ) = validation { 17 | subject.length <= max(subject) 18 | } 19 | 20 | fun
Constraint
.maxLength(max: Int) = maxLength { max } 21 | 22 | inline fun
Constraint
.lengthLessThan( 23 | crossinline maxExclusive: (DT) -> Int 24 | ) = validation { 25 | subject.length < maxExclusive(subject) 26 | } 27 | 28 | fun
Constraint
.lengthLessThan(maxExclusive: Int) = lengthLessThan { maxExclusive } 29 | 30 | inline fun
Constraint
.lengthLargerThan( 31 | crossinline minExclusive: (DT) -> Int 32 | ) = validation { 33 | subject.length > minExclusive(subject) 34 | } 35 | 36 | fun
Constraint
.lengthLargerThan(minExclusive: Int) = lengthLargerThan { minExclusive } 37 | 38 | inline fun
Constraint
.lengthIn( 39 | crossinline range: (DT) -> IntRange 40 | ) = validation { 41 | subject.length in range(subject) 42 | } 43 | 44 | fun
Constraint
.lengthIn(range: IntRange) = lengthIn { range } 45 | 46 | fun
Constraint
.lengthIn(min: Int, max: Int) = lengthIn(min..max) 47 | 48 | inline fun
Constraint
.lengthNotIn( 49 | crossinline range: (DT) -> IntRange 50 | ) = validation { 51 | subject.length !in range(subject) 52 | } 53 | 54 | fun
Constraint
.lengthNotIn(range: IntRange) = lengthNotIn { range } 55 | 56 | fun
Constraint
.lengthNotIn(min: Int, max: Int) = lengthNotIn(min..max) 57 | 58 | inline fun
Constraint
.lengthEqualTo( 59 | crossinline value: (DT) -> Int 60 | ) = validation { 61 | subject.length == value(subject) 62 | } 63 | 64 | fun
Constraint
.lengthEqualTo(value: Int) = lengthEqualTo { value } 65 | 66 | inline fun
Constraint
.lengthNotEqualTo( 67 | crossinline value: (DT) -> Int 68 | ) = validation { 69 | subject.length != value(subject) 70 | } 71 | 72 | fun
Constraint
.lengthNotEqualTo(value: Int) = lengthNotEqualTo { value } 73 | 74 | inline fun
Constraint
.contains( 75 | ignoreCase: Boolean = false, 76 | crossinline portion: (DT) -> CharSequence 77 | ) = validation { 78 | subject.contains(portion(subject), ignoreCase) 79 | } 80 | 81 | fun
Constraint
.contains( 82 | portion: CharSequence, 83 | ignoreCase: Boolean = false 84 | ) = contains(ignoreCase) { portion } 85 | 86 | inline fun
Constraint
.containsChar( 87 | ignoreCase: Boolean = false, 88 | crossinline portion: (DT) -> Char 89 | ) = validation { 90 | subject.contains(portion(subject), ignoreCase) 91 | } 92 | 93 | fun
Constraint
.containsChar( 94 | portion: Char, 95 | ignoreCase: Boolean = false 96 | ) = containsChar(ignoreCase) { portion } 97 | 98 | fun
Constraint
.contains(regex: Regex) = validation { 99 | subject.contains(regex) 100 | } 101 | 102 | //TODO: startsWithAnyOf, doesNotStartWith 103 | inline fun
Constraint
.startsWith( 104 | ignoreCase: Boolean = false, 105 | crossinline prefix: (DT) -> CharSequence 106 | ) = validation { 107 | subject.startsWith(prefix(subject), ignoreCase) 108 | } 109 | 110 | fun
Constraint
.startsWith( 111 | prefix: CharSequence, 112 | ignoreCase: Boolean = false 113 | ) = startsWith(ignoreCase) { prefix } 114 | 115 | inline fun
Constraint
.startsWithChar( 116 | ignoreCase: Boolean = false, 117 | crossinline prefix: (DT) -> Char 118 | ) = validation { 119 | subject.startsWith(prefix(subject), ignoreCase) 120 | } 121 | 122 | fun
Constraint
.startsWithChar( 123 | prefix: Char, 124 | ignoreCase: Boolean = false 125 | ) = startsWithChar(ignoreCase) { prefix } 126 | 127 | 128 | inline fun
Constraint
.endsWith( 129 | ignoreCase: Boolean = false, 130 | crossinline suffix: (DT) -> CharSequence 131 | ) = validation { 132 | subject.endsWith(suffix(subject), ignoreCase) 133 | } 134 | 135 | fun
Constraint
.endsWith( 136 | suffix: CharSequence, 137 | ignoreCase: Boolean = false 138 | ) = endsWith(ignoreCase) { suffix } 139 | 140 | //TODO: @OverloadResolutionByLambdaReturnType 141 | inline fun
Constraint
.endsWithChar( 142 | ignoreCase: Boolean = false, 143 | crossinline suffix: (DT) -> Char 144 | ) = validation { 145 | subject.endsWith(suffix(subject), ignoreCase) 146 | } 147 | 148 | fun
Constraint
.endsWithChar( 149 | suffix: Char, 150 | ignoreCase: Boolean = false 151 | ) = endsWithChar(ignoreCase) { suffix } 152 | 153 | fun
Constraint
.matches(regex: Regex) = validation { 154 | subject.matches(regex) 155 | } 156 | 157 | fun
Constraint
.isEmpty() = validation { subject.isEmpty() } 158 | 159 | fun
Constraint
.isNotEmpty() = validation { subject.isNotEmpty() } 160 | 161 | fun
Constraint
.isBlank() = validation { subject.isBlank() } 162 | 163 | fun
Constraint
.isNotBlank() = validation { subject.isNotBlank() } 164 | 165 | inline fun
Constraint
.hasSurrogatePairAt( 166 | crossinline index: (DT) -> Int 167 | ) = validation { 168 | subject.hasSurrogatePairAt(index(subject)) 169 | } 170 | 171 | fun
Constraint
.hasSurrogatePairAt(index: Int) = hasSurrogatePairAt { index } 172 | 173 | fun
Constraint
.regionMatches( 174 | thisOffset: Int, 175 | other: DT, 176 | otherOffset: Int, 177 | length: Int, 178 | ignoreCase: Boolean 179 | ) = validation { 180 | subject.regionMatches(thisOffset, other, otherOffset, length, ignoreCase) 181 | } 182 | 183 | fun
Constraint
.containsWhiteSpaces() = validation { 184 | subject.contains(' ') 185 | } 186 | 187 | fun
Constraint
.doesNotContainWhiteSpaces() = validation { 188 | !subject.contains(' ') 189 | } 190 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/FloatValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class FloatValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisFloatIsDivisibleByTheGivenFloat() { 13 | constraint { 14 | isDivisibleBy(3f) 15 | }.allMatch( 16 | 0f, 17 | 3f, 18 | 6f, 19 | 9f 20 | ).allFail( 21 | Float.NaN, 22 | 1f, 23 | 2f, 24 | 4f, 25 | 5f, 26 | 7f, 27 | 8f 28 | ) 29 | } 30 | 31 | @Test 32 | fun isDivisibleByL_meansThisFloatIsDivisibleByTheGivenFloat() { 33 | constraint { 34 | isDivisibleBy { 3f } 35 | }.allMatch( 36 | 0f, 37 | 3f, 38 | 6f, 39 | 9f 40 | ).allFail( 41 | Float.NaN, 42 | 1f, 43 | 2f, 44 | 4f, 45 | 5f, 46 | 7f, 47 | 8f 48 | ) 49 | } 50 | 51 | @Test 52 | fun isNotDivisibleBy_meansThisFloatIsNotDivisibleByTheGivenFloat() { 53 | constraint { 54 | isNotDivisibleBy(3f) 55 | }.allMatch( 56 | 1f, 57 | 2f, 58 | 4f, 59 | 5f, 60 | 7f, 61 | 8f 62 | ).allFail( 63 | 0f, 64 | 3f, 65 | 6f, 66 | 9f 67 | ) 68 | } 69 | 70 | @Test 71 | fun isNotDivisibleByL_meansThisFloatIsNotDivisibleByTheGivenFloat() { 72 | constraint { 73 | isNotDivisibleBy { 3f } 74 | }.allMatch( 75 | 1f, 76 | 2f, 77 | 4f, 78 | 5f, 79 | 7f, 80 | 8f 81 | ).allFail( 82 | 0f, 83 | 3f, 84 | 6f, 85 | 9f 86 | ) 87 | } 88 | 89 | @Test 90 | fun isPositive_meansThisFloatIsPositive() { 91 | 92 | constraint { 93 | isPositive(false) 94 | }.allMatch( 95 | 1f, 96 | 2f, 97 | 3f, 98 | 4f, 99 | 125f 100 | ).allFail( 101 | Float.NaN, 102 | 0f, 103 | -1f, 104 | -2f, 105 | -3f, 106 | -4f, 107 | -125f 108 | ) 109 | 110 | constraint { 111 | isPositive(true) 112 | }.allMatch( 113 | 0f, 114 | 1f, 115 | 2f, 116 | 3f, 117 | 4f, 118 | 125f 119 | ).allFail( 120 | Float.NaN, 121 | -1f, 122 | -2f, 123 | -3f, 124 | -4f, 125 | -125f 126 | ) 127 | } 128 | 129 | @Test 130 | fun isNegative_meansThisFloatIsNegative() { 131 | 132 | constraint { 133 | isNegative(false) 134 | }.allMatch( 135 | -1f, 136 | -2f, 137 | -3f, 138 | -4f, 139 | -125f 140 | ).allFail( 141 | Float.NaN, 142 | 0f, 143 | 1f, 144 | 2f, 145 | 3f, 146 | 4f, 147 | 125f 148 | ) 149 | 150 | constraint { 151 | isNegative(true) 152 | }.allMatch( 153 | 0f, 154 | -1f, 155 | -2f, 156 | -3f, 157 | -4f, 158 | -125f 159 | ).allFail( 160 | Float.NaN, 161 | 1f, 162 | 2f, 163 | 3f, 164 | 4f, 165 | 125f 166 | ) 167 | } 168 | 169 | @Test 170 | fun isZero_meansThisFloatEqualsZero() { 171 | constraint { 172 | isZero() 173 | }.allMatch( 174 | 0f 175 | ).allFail( 176 | Float.NaN, 177 | -125f, 178 | -3f, 179 | -4f, 180 | -2f, 181 | -1f, 182 | 1f, 183 | 2f, 184 | 3f, 185 | 4f, 186 | 125f 187 | ) 188 | } 189 | 190 | @Test 191 | fun isNotZero_meansThisFloatDoesNotEqualZero() { 192 | constraint { 193 | isNotZero() 194 | }.allMatch( 195 | -125f, 196 | -3f, 197 | -4f, 198 | -2f, 199 | -1f, 200 | 1f, 201 | 2f, 202 | 3f, 203 | 4f, 204 | 125f 205 | ).allFail( 206 | 0f 207 | ) 208 | } 209 | 210 | @Test 211 | fun isNaN_meansThisFloatIsNaN() { 212 | constraint { 213 | isNaN() 214 | }.allMatch( 215 | Float.NaN 216 | ).allFail( 217 | -1f, 218 | 0f, 219 | 1f, 220 | 2f, 221 | Float.MIN_VALUE, 222 | Float.MAX_VALUE, 223 | Float.NEGATIVE_INFINITY, 224 | Float.POSITIVE_INFINITY 225 | ) 226 | } 227 | 228 | @Test 229 | fun isNotNaN_meansThisFloatIsNaN() { 230 | constraint { 231 | isNotNaN() 232 | }.allMatch( 233 | -1f, 234 | 0f, 235 | 1f, 236 | 2f, 237 | Float.MIN_VALUE, 238 | Float.MAX_VALUE, 239 | Float.NEGATIVE_INFINITY, 240 | Float.POSITIVE_INFINITY 241 | ).allFail( 242 | Float.NaN 243 | ) 244 | } 245 | 246 | @Test 247 | fun isInfinite_meansThisFloatIsInfinite() { 248 | constraint { 249 | isInfinite() 250 | }.allMatch( 251 | Float.NEGATIVE_INFINITY, 252 | Float.POSITIVE_INFINITY 253 | ).allFail( 254 | Float.NaN, 255 | -1f, 256 | 0f, 257 | 1f, 258 | 2f, 259 | Float.MIN_VALUE, 260 | Float.MAX_VALUE 261 | ) 262 | } 263 | 264 | @Test 265 | fun isFinite_meansThisFloatIsFinite() { 266 | constraint { 267 | isFinite() 268 | }.allMatch( 269 | -1f, 270 | 0f, 271 | 1f, 272 | 2f, 273 | Float.MIN_VALUE, 274 | Float.MAX_VALUE 275 | ).allFail( 276 | Float.NaN, 277 | Float.NEGATIVE_INFINITY, 278 | Float.POSITIVE_INFINITY 279 | ) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/DoubleValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class DoubleValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisDoubleIsDivisibleByTheGivenDouble() { 13 | constraint { 14 | isDivisibleBy(3.0) 15 | }.allMatch( 16 | 0.0, 17 | 3.0, 18 | 6.0, 19 | 9.0 20 | ).allFail( 21 | Double.NaN, 22 | 1.0, 23 | 2.0, 24 | 4.0, 25 | 5.0, 26 | 7.0, 27 | 8.0 28 | ) 29 | } 30 | 31 | @Test 32 | fun isDivisibleByL_meansThisDoubleIsDivisibleByTheGivenDouble() { 33 | constraint { 34 | isDivisibleBy { 3.0 } 35 | }.allMatch( 36 | 0.0, 37 | 3.0, 38 | 6.0, 39 | 9.0 40 | ).allFail( 41 | Double.NaN, 42 | 1.0, 43 | 2.0, 44 | 4.0, 45 | 5.0, 46 | 7.0, 47 | 8.0 48 | ) 49 | } 50 | 51 | @Test 52 | fun isNotDivisibleBy_meansThisDoubleIsNotDivisibleByTheGivenDouble() { 53 | constraint { 54 | isNotDivisibleBy(3.0) 55 | }.allMatch( 56 | 1.0, 57 | 2.0, 58 | 4.0, 59 | 5.0, 60 | 7.0, 61 | 8.0 62 | ).allFail( 63 | 0.0, 64 | 3.0, 65 | 6.0, 66 | 9.0 67 | ) 68 | } 69 | 70 | @Test 71 | fun isNotDivisibleByL_meansThisDoubleIsNotDivisibleByTheGivenDouble() { 72 | constraint { 73 | isNotDivisibleBy { 3.0 } 74 | }.allMatch( 75 | 1.0, 76 | 2.0, 77 | 4.0, 78 | 5.0, 79 | 7.0, 80 | 8.0 81 | ).allFail( 82 | 0.0, 83 | 3.0, 84 | 6.0, 85 | 9.0 86 | ) 87 | } 88 | 89 | @Test 90 | fun isPositive_meansThisDoubleIsPositive() { 91 | 92 | constraint { 93 | isPositive(false) 94 | }.allMatch( 95 | 1.0, 96 | 2.0, 97 | 3.0, 98 | 4.0, 99 | 125.0 100 | ).allFail( 101 | Double.NaN, 102 | 0.0, 103 | -1.0, 104 | -2.0, 105 | -3.0, 106 | -4.0, 107 | -125.0 108 | ) 109 | 110 | constraint { 111 | isPositive(true) 112 | }.allMatch( 113 | 0.0, 114 | 1.0, 115 | 2.0, 116 | 3.0, 117 | 4.0, 118 | 125.0 119 | ).allFail( 120 | Double.NaN, 121 | -1.0, 122 | -2.0, 123 | -3.0, 124 | -4.0, 125 | -125.0 126 | ) 127 | } 128 | 129 | @Test 130 | fun isNegative_meansThisDoubleIsNegative() { 131 | 132 | constraint { 133 | isNegative(false) 134 | }.allMatch( 135 | -1.0, 136 | -2.0, 137 | -3.0, 138 | -4.0, 139 | -125.0 140 | ).allFail( 141 | Double.NaN, 142 | 0.0, 143 | 1.0, 144 | 2.0, 145 | 3.0, 146 | 4.0, 147 | 125.0 148 | ) 149 | 150 | constraint { 151 | isNegative(true) 152 | }.allMatch( 153 | 0.0, 154 | -1.0, 155 | -2.0, 156 | -3.0, 157 | -4.0, 158 | -125.0 159 | ).allFail( 160 | Double.NaN, 161 | 1.0, 162 | 2.0, 163 | 3.0, 164 | 4.0, 165 | 125.0 166 | ) 167 | } 168 | 169 | @Test 170 | fun isZero_meansThisDoubleEqualsZero() { 171 | constraint { 172 | isZero() 173 | }.allMatch( 174 | 0.0 175 | ).allFail( 176 | Double.NaN, 177 | -125.0, 178 | -3.0, 179 | -4.0, 180 | -2.0, 181 | -1.0, 182 | 1.0, 183 | 2.0, 184 | 3.0, 185 | 4.0, 186 | 125.0 187 | ) 188 | } 189 | 190 | @Test 191 | fun isNotZero_meansThisDoubleDoesNotEqualZero() { 192 | constraint { 193 | isNotZero() 194 | }.allMatch( 195 | -125.0, 196 | -3.0, 197 | -4.0, 198 | -2.0, 199 | -1.0, 200 | 1.0, 201 | 2.0, 202 | 3.0, 203 | 4.0, 204 | 125.0 205 | ).allFail( 206 | 0.0 207 | ) 208 | } 209 | 210 | @Test 211 | fun isNaN_meansThisDoubleIsNaN() { 212 | constraint { 213 | isNaN() 214 | }.allMatch( 215 | Double.NaN 216 | ).allFail( 217 | -1.0, 218 | 0.0, 219 | 1.0, 220 | 2.0, 221 | Double.MIN_VALUE, 222 | Double.MAX_VALUE, 223 | Double.NEGATIVE_INFINITY, 224 | Double.POSITIVE_INFINITY 225 | ) 226 | } 227 | 228 | @Test 229 | fun isNotNaN_meansThisDoubleIsNaN() { 230 | constraint { 231 | isNotNaN() 232 | }.allMatch( 233 | -1.0, 234 | 0.0, 235 | 1.0, 236 | 2.0, 237 | Double.MIN_VALUE, 238 | Double.MAX_VALUE, 239 | Double.NEGATIVE_INFINITY, 240 | Double.POSITIVE_INFINITY 241 | ).allFail( 242 | Double.NaN 243 | ) 244 | } 245 | 246 | @Test 247 | fun isInfinite_meansThisDoubleIsInfinite() { 248 | constraint { 249 | isInfinite() 250 | }.allMatch( 251 | Double.NEGATIVE_INFINITY, 252 | Double.POSITIVE_INFINITY 253 | ).allFail( 254 | Double.NaN, 255 | -1.0, 256 | 0.0, 257 | 1.0, 258 | 2.0, 259 | Double.MIN_VALUE, 260 | Double.MAX_VALUE 261 | ) 262 | } 263 | 264 | @Test 265 | fun isFinite_meansThisDoubleIsFinite() { 266 | constraint { 267 | isFinite() 268 | }.allMatch( 269 | -1.0, 270 | 0.0, 271 | 1.0, 272 | 2.0, 273 | Double.MIN_VALUE, 274 | Double.MAX_VALUE 275 | ).allFail( 276 | Double.NaN, 277 | Double.NEGATIVE_INFINITY, 278 | Double.POSITIVE_INFINITY 279 | ) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/IntValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class IntValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisIntIsDivisibleByTheGivenInt() { 13 | constraint { 14 | isDivisibleBy(3) 15 | }.allMatch( 16 | 0, 17 | 3, 18 | 6, 19 | 9 20 | ).allFail( 21 | 1, 22 | 2, 23 | 4, 24 | 5, 25 | 7, 26 | 8 27 | ) 28 | } 29 | 30 | @Test 31 | fun isDivisibleByL_meansThisIntIsDivisibleByTheGivenInt() { 32 | constraint { 33 | isDivisibleBy { 3 } 34 | }.allMatch( 35 | 0, 36 | 3, 37 | 6, 38 | 9 39 | ).allFail( 40 | 1, 41 | 2, 42 | 4, 43 | 5, 44 | 7, 45 | 8 46 | ) 47 | } 48 | 49 | @Test 50 | fun isNotDivisibleBy_meansThisIntIsNotDivisibleByTheGivenInt() { 51 | constraint { 52 | isNotDivisibleBy(3) 53 | }.allMatch( 54 | 1, 55 | 2, 56 | 4, 57 | 5, 58 | 7, 59 | 8 60 | ).allFail( 61 | 0, 62 | 3, 63 | 6, 64 | 9 65 | ) 66 | } 67 | 68 | @Test 69 | fun isNotDivisibleByL_meansThisIntIsNotDivisibleByTheGivenInt() { 70 | constraint { 71 | isNotDivisibleBy { 3 } 72 | }.allMatch( 73 | 1, 74 | 2, 75 | 4, 76 | 5, 77 | 7, 78 | 8 79 | ).allFail( 80 | 0, 81 | 3, 82 | 6, 83 | 9 84 | ) 85 | } 86 | 87 | @Test 88 | fun isEven_meansThisIntIsEven() { 89 | constraint { 90 | isEven() 91 | }.allMatch( 92 | 0, 93 | 2, 94 | 4, 95 | 6, 96 | 8, 97 | 126 98 | ).allFail( 99 | 1, 100 | 3, 101 | 5, 102 | 7, 103 | 125 104 | ) 105 | } 106 | 107 | @Test 108 | fun isOdd_meansThisIntIsOdd() { 109 | constraint { 110 | isOdd() 111 | }.allMatch( 112 | 1, 113 | 3, 114 | 5, 115 | 7, 116 | 125 117 | ).allFail( 118 | 0, 119 | 2, 120 | 4, 121 | 6, 122 | 8, 123 | 126 124 | ) 125 | } 126 | 127 | @Test 128 | fun isPositive_meansThisIntIsPositive() { 129 | 130 | constraint { 131 | isPositive(false) 132 | }.allMatch( 133 | 1, 134 | 2, 135 | 3, 136 | 4, 137 | 125 138 | ).allFail( 139 | 0, 140 | -1, 141 | -2, 142 | -3, 143 | -4, 144 | -125 145 | ) 146 | 147 | constraint { 148 | isPositive(true) 149 | }.allMatch( 150 | 0, 151 | 1, 152 | 2, 153 | 3, 154 | 4, 155 | 125 156 | ).allFail( 157 | -1, 158 | -2, 159 | -3, 160 | -4, 161 | -125 162 | ) 163 | } 164 | 165 | @Test 166 | fun isNegative_meansThisIntIsNegative() { 167 | 168 | constraint { 169 | isNegative(false) 170 | }.allMatch( 171 | -1, 172 | -2, 173 | -3, 174 | -4, 175 | -125 176 | ).allFail( 177 | 0, 178 | 1, 179 | 2, 180 | 3, 181 | 4, 182 | 125 183 | ) 184 | 185 | constraint { 186 | isNegative(true) 187 | }.allMatch( 188 | 0, 189 | -1, 190 | -2, 191 | -3, 192 | -4, 193 | -125 194 | ).allFail( 195 | 1, 196 | 2, 197 | 3, 198 | 4, 199 | 125 200 | ) 201 | } 202 | 203 | @Test 204 | fun isZero_meansThisIntEqualsZero() { 205 | constraint { 206 | isZero() 207 | }.allMatch( 208 | 0 209 | ).allFail( 210 | -125, 211 | -3, 212 | -4, 213 | -2, 214 | -1, 215 | 1, 216 | 2, 217 | 3, 218 | 4, 219 | 125 220 | ) 221 | } 222 | 223 | @Test 224 | fun isNotZero_meansThisIntDoesNotEqualZero() { 225 | constraint { 226 | isNotZero() 227 | }.allMatch( 228 | -125, 229 | -3, 230 | -4, 231 | -2, 232 | -1, 233 | 1, 234 | 2, 235 | 3, 236 | 4, 237 | 125 238 | ).allFail( 239 | 0 240 | ) 241 | } 242 | 243 | @Test 244 | fun isPrime_meansThisIntIsPrime() { 245 | constraint { 246 | isPrime() 247 | }.allMatch( 248 | 2, 249 | 3, 250 | 5, 251 | 7, 252 | 11, 253 | 67, 254 | 71, 255 | 73, 256 | 79, 257 | 83, 258 | 89, 259 | 97 260 | ).allFail( 261 | -5, 262 | -4, 263 | -3, 264 | -2, 265 | -1, 266 | 0, 267 | 1, 268 | 4, 269 | 6, 270 | 8, 271 | 9, 272 | 56, 273 | 57, 274 | 58, 275 | 60, 276 | 62, 277 | 63 278 | ) 279 | } 280 | 281 | @Test 282 | fun isNotPrime_meansThisIntIsNotPrime() { 283 | constraint { 284 | isNotPrime() 285 | }.allMatch( 286 | -5, 287 | -4, 288 | -3, 289 | -2, 290 | -1, 291 | 0, 292 | 1, 293 | 4, 294 | 6, 295 | 8, 296 | 9, 297 | 56, 298 | 57, 299 | 58, 300 | 60, 301 | 62, 302 | 63 303 | ).allFail( 304 | 2, 305 | 3, 306 | 5, 307 | 7, 308 | 11, 309 | 67, 310 | 71, 311 | 73, 312 | 79, 313 | 83, 314 | 89, 315 | 97 316 | ) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ByteValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class ByteValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisByteIsDivisibleByTheGivenByte() { 13 | constraint { 14 | isDivisibleBy(3) 15 | }.allMatch( 16 | 0, 17 | 3, 18 | 6, 19 | 9 20 | ).allFail( 21 | 1, 22 | 2, 23 | 4, 24 | 5, 25 | 7, 26 | 8 27 | ) 28 | } 29 | 30 | @Test 31 | fun isDivisibleByL_meansThisByteIsDivisibleByTheGivenByte() { 32 | constraint { 33 | isDivisibleBy { 3 } 34 | }.allMatch( 35 | 0, 36 | 3, 37 | 6, 38 | 9 39 | ).allFail( 40 | 1, 41 | 2, 42 | 4, 43 | 5, 44 | 7, 45 | 8 46 | ) 47 | } 48 | 49 | @Test 50 | fun isNotDivisibleBy_meansThisByteIsNotDivisibleByTheGivenByte() { 51 | constraint { 52 | isNotDivisibleBy(3) 53 | }.allMatch( 54 | 1, 55 | 2, 56 | 4, 57 | 5, 58 | 7, 59 | 8 60 | ).allFail( 61 | 0, 62 | 3, 63 | 6, 64 | 9 65 | ) 66 | } 67 | 68 | @Test 69 | fun isNotDivisibleByL_meansThisByteIsNotDivisibleByTheGivenByte() { 70 | constraint { 71 | isNotDivisibleBy { 3 } 72 | }.allMatch( 73 | 1, 74 | 2, 75 | 4, 76 | 5, 77 | 7, 78 | 8 79 | ).allFail( 80 | 0, 81 | 3, 82 | 6, 83 | 9 84 | ) 85 | } 86 | 87 | @Test 88 | fun isEven_meansThisByteIsEven() { 89 | constraint { 90 | isEven() 91 | }.allMatch( 92 | 0, 93 | 2, 94 | 4, 95 | 6, 96 | 8, 97 | 126 98 | ).allFail( 99 | 1, 100 | 3, 101 | 5, 102 | 7, 103 | 125 104 | ) 105 | } 106 | 107 | @Test 108 | fun isOdd_meansThisByteIsOdd() { 109 | constraint { 110 | isOdd() 111 | }.allMatch( 112 | 1, 113 | 3, 114 | 5, 115 | 7, 116 | 125 117 | ).allFail( 118 | 0, 119 | 2, 120 | 4, 121 | 6, 122 | 8, 123 | 126 124 | ) 125 | } 126 | 127 | @Test 128 | fun isPositive_meansThisByteIsPositive() { 129 | 130 | constraint { 131 | isPositive(false) 132 | }.allMatch( 133 | 1, 134 | 2, 135 | 3, 136 | 4, 137 | 125 138 | ).allFail( 139 | 0, 140 | -1, 141 | -2, 142 | -3, 143 | -4, 144 | -125 145 | ) 146 | 147 | constraint { 148 | isPositive(true) 149 | }.allMatch( 150 | 0, 151 | 1, 152 | 2, 153 | 3, 154 | 4, 155 | 125 156 | ).allFail( 157 | -1, 158 | -2, 159 | -3, 160 | -4, 161 | -125 162 | ) 163 | } 164 | 165 | @Test 166 | fun isNegative_meansThisByteIsNegative() { 167 | 168 | constraint { 169 | isNegative(false) 170 | }.allMatch( 171 | -1, 172 | -2, 173 | -3, 174 | -4, 175 | -125 176 | ).allFail( 177 | 0, 178 | 1, 179 | 2, 180 | 3, 181 | 4, 182 | 125 183 | ) 184 | 185 | constraint { 186 | isNegative(true) 187 | }.allMatch( 188 | 0, 189 | -1, 190 | -2, 191 | -3, 192 | -4, 193 | -125 194 | ).allFail( 195 | 1, 196 | 2, 197 | 3, 198 | 4, 199 | 125 200 | ) 201 | } 202 | 203 | @Test 204 | fun isZero_meansThisByteEqualsZero() { 205 | constraint { 206 | isZero() 207 | }.allMatch( 208 | 0 209 | ).allFail( 210 | -125, 211 | -3, 212 | -4, 213 | -2, 214 | -1, 215 | 1, 216 | 2, 217 | 3, 218 | 4, 219 | 125 220 | ) 221 | } 222 | 223 | @Test 224 | fun isNotZero_meansThisByteDoesNotEqualZero() { 225 | constraint { 226 | isNotZero() 227 | }.allMatch( 228 | -125, 229 | -3, 230 | -4, 231 | -2, 232 | -1, 233 | 1, 234 | 2, 235 | 3, 236 | 4, 237 | 125 238 | ).allFail( 239 | 0 240 | ) 241 | } 242 | 243 | @Test 244 | fun isPrime_meansThisByteIsPrime() { 245 | constraint { 246 | isPrime() 247 | }.allMatch( 248 | 2, 249 | 3, 250 | 5, 251 | 7, 252 | 11, 253 | 67, 254 | 71, 255 | 73, 256 | 79, 257 | 83, 258 | 89, 259 | 97 260 | ).allFail( 261 | -5, 262 | -4, 263 | -3, 264 | -2, 265 | -1, 266 | 0, 267 | 1, 268 | 4, 269 | 6, 270 | 8, 271 | 9, 272 | 56, 273 | 57, 274 | 58, 275 | 60, 276 | 62, 277 | 63 278 | ) 279 | } 280 | 281 | @Test 282 | fun isNotPrime_meansThisByteIsNotPrime() { 283 | constraint { 284 | isNotPrime() 285 | }.allMatch( 286 | -5, 287 | -4, 288 | -3, 289 | -2, 290 | -1, 291 | 0, 292 | 1, 293 | 4, 294 | 6, 295 | 8, 296 | 9, 297 | 56, 298 | 57, 299 | 58, 300 | 60, 301 | 62, 302 | 63 303 | ).allFail( 304 | 2, 305 | 3, 306 | 5, 307 | 7, 308 | 11, 309 | 67, 310 | 71, 311 | 73, 312 | 79, 313 | 83, 314 | 89, 315 | 97 316 | ) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/LongValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class LongValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisLongIsDivisibleByTheGivenLong() { 13 | constraint { 14 | isDivisibleBy(3) 15 | }.allMatch( 16 | 0, 17 | 3, 18 | 6, 19 | 9 20 | ).allFail( 21 | 1, 22 | 2, 23 | 4, 24 | 5, 25 | 7, 26 | 8 27 | ) 28 | } 29 | 30 | @Test 31 | fun isDivisibleByL_meansThisLongIsDivisibleByTheGivenLong() { 32 | constraint { 33 | isDivisibleBy { 3 } 34 | }.allMatch( 35 | 0, 36 | 3, 37 | 6, 38 | 9 39 | ).allFail( 40 | 1, 41 | 2, 42 | 4, 43 | 5, 44 | 7, 45 | 8 46 | ) 47 | } 48 | 49 | @Test 50 | fun isNotDivisibleBy_meansThisLongIsNotDivisibleByTheGivenLong() { 51 | constraint { 52 | isNotDivisibleBy(3) 53 | }.allMatch( 54 | 1, 55 | 2, 56 | 4, 57 | 5, 58 | 7, 59 | 8 60 | ).allFail( 61 | 0, 62 | 3, 63 | 6, 64 | 9 65 | ) 66 | } 67 | 68 | @Test 69 | fun isNotDivisibleByL_meansThisLongIsNotDivisibleByTheGivenLong() { 70 | constraint { 71 | isNotDivisibleBy { 3 } 72 | }.allMatch( 73 | 1, 74 | 2, 75 | 4, 76 | 5, 77 | 7, 78 | 8 79 | ).allFail( 80 | 0, 81 | 3, 82 | 6, 83 | 9 84 | ) 85 | } 86 | 87 | @Test 88 | fun isEven_meansThisLongIsEven() { 89 | constraint { 90 | isEven() 91 | }.allMatch( 92 | 0, 93 | 2, 94 | 4, 95 | 6, 96 | 8, 97 | 126 98 | ).allFail( 99 | 1, 100 | 3, 101 | 5, 102 | 7, 103 | 125 104 | ) 105 | } 106 | 107 | @Test 108 | fun isOdd_meansThisLongIsOdd() { 109 | constraint { 110 | isOdd() 111 | }.allMatch( 112 | 1, 113 | 3, 114 | 5, 115 | 7, 116 | 125 117 | ).allFail( 118 | 0, 119 | 2, 120 | 4, 121 | 6, 122 | 8, 123 | 126 124 | ) 125 | } 126 | 127 | @Test 128 | fun isPositive_meansThisLongIsPositive() { 129 | 130 | constraint { 131 | isPositive(false) 132 | }.allMatch( 133 | 1, 134 | 2, 135 | 3, 136 | 4, 137 | 125 138 | ).allFail( 139 | 0, 140 | -1, 141 | -2, 142 | -3, 143 | -4, 144 | -125 145 | ) 146 | 147 | constraint { 148 | isPositive(true) 149 | }.allMatch( 150 | 0, 151 | 1, 152 | 2, 153 | 3, 154 | 4, 155 | 125 156 | ).allFail( 157 | -1, 158 | -2, 159 | -3, 160 | -4, 161 | -125 162 | ) 163 | } 164 | 165 | @Test 166 | fun isNegative_meansThisLongIsNegative() { 167 | 168 | constraint { 169 | isNegative(false) 170 | }.allMatch( 171 | -1, 172 | -2, 173 | -3, 174 | -4, 175 | -125 176 | ).allFail( 177 | 0, 178 | 1, 179 | 2, 180 | 3, 181 | 4, 182 | 125 183 | ) 184 | 185 | constraint { 186 | isNegative(true) 187 | }.allMatch( 188 | 0, 189 | -1, 190 | -2, 191 | -3, 192 | -4, 193 | -125 194 | ).allFail( 195 | 1, 196 | 2, 197 | 3, 198 | 4, 199 | 125 200 | ) 201 | } 202 | 203 | @Test 204 | fun isZero_meansThisLongEqualsZero() { 205 | constraint { 206 | isZero() 207 | }.allMatch( 208 | 0 209 | ).allFail( 210 | -125, 211 | -3, 212 | -4, 213 | -2, 214 | -1, 215 | 1, 216 | 2, 217 | 3, 218 | 4, 219 | 125 220 | ) 221 | } 222 | 223 | @Test 224 | fun isNotZero_meansThisLongDoesNotEqualZero() { 225 | constraint { 226 | isNotZero() 227 | }.allMatch( 228 | -125, 229 | -3, 230 | -4, 231 | -2, 232 | -1, 233 | 1, 234 | 2, 235 | 3, 236 | 4, 237 | 125 238 | ).allFail( 239 | 0 240 | ) 241 | } 242 | 243 | @Test 244 | fun isPrime_meansThisLongIsPrime() { 245 | constraint { 246 | isPrime() 247 | }.allMatch( 248 | 2, 249 | 3, 250 | 5, 251 | 7, 252 | 11, 253 | 67, 254 | 71, 255 | 73, 256 | 79, 257 | 83, 258 | 89, 259 | 97 260 | ).allFail( 261 | -5, 262 | -4, 263 | -3, 264 | -2, 265 | -1, 266 | 0, 267 | 1, 268 | 4, 269 | 6, 270 | 8, 271 | 9, 272 | 56, 273 | 57, 274 | 58, 275 | 60, 276 | 62, 277 | 63 278 | ) 279 | } 280 | 281 | @Test 282 | fun isNotPrime_meansThisLongIsNotPrime() { 283 | constraint { 284 | isNotPrime() 285 | }.allMatch( 286 | -5, 287 | -4, 288 | -3, 289 | -2, 290 | -1, 291 | 0, 292 | 1, 293 | 4, 294 | 6, 295 | 8, 296 | 9, 297 | 56, 298 | 57, 299 | 58, 300 | 60, 301 | 62, 302 | 63 303 | ).allFail( 304 | 2, 305 | 3, 306 | 5, 307 | 7, 308 | 11, 309 | 67, 310 | 71, 311 | 73, 312 | 79, 313 | 83, 314 | 89, 315 | 97 316 | ) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ShortValidationsTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.utils.allFail 4 | import dev.ahmedmourad.validation.core.utils.allMatch 5 | import dev.ahmedmourad.validation.core.utils.constraint 6 | import dev.ahmedmourad.validation.core.validations.* 7 | import kotlin.test.Test 8 | 9 | class ShortValidationsTests { 10 | 11 | @Test 12 | fun isDivisibleBy_meansThisShortIsDivisibleByTheGivenShort() { 13 | constraint { 14 | isDivisibleBy(3) 15 | }.allMatch( 16 | 0, 17 | 3, 18 | 6, 19 | 9 20 | ).allFail( 21 | 1, 22 | 2, 23 | 4, 24 | 5, 25 | 7, 26 | 8 27 | ) 28 | } 29 | 30 | @Test 31 | fun isDivisibleByL_meansThisShortIsDivisibleByTheGivenShort() { 32 | constraint { 33 | isDivisibleBy { 3 } 34 | }.allMatch( 35 | 0, 36 | 3, 37 | 6, 38 | 9 39 | ).allFail( 40 | 1, 41 | 2, 42 | 4, 43 | 5, 44 | 7, 45 | 8 46 | ) 47 | } 48 | 49 | @Test 50 | fun isNotDivisibleBy_meansThisShortIsNotDivisibleByTheGivenShort() { 51 | constraint { 52 | isNotDivisibleBy(3) 53 | }.allMatch( 54 | 1, 55 | 2, 56 | 4, 57 | 5, 58 | 7, 59 | 8 60 | ).allFail( 61 | 0, 62 | 3, 63 | 6, 64 | 9 65 | ) 66 | } 67 | 68 | @Test 69 | fun isNotDivisibleByL_meansThisShortIsNotDivisibleByTheGivenShort() { 70 | constraint { 71 | isNotDivisibleBy { 3 } 72 | }.allMatch( 73 | 1, 74 | 2, 75 | 4, 76 | 5, 77 | 7, 78 | 8 79 | ).allFail( 80 | 0, 81 | 3, 82 | 6, 83 | 9 84 | ) 85 | } 86 | 87 | @Test 88 | fun isEven_meansThisShortIsEven() { 89 | constraint { 90 | isEven() 91 | }.allMatch( 92 | 0, 93 | 2, 94 | 4, 95 | 6, 96 | 8, 97 | 126 98 | ).allFail( 99 | 1, 100 | 3, 101 | 5, 102 | 7, 103 | 125 104 | ) 105 | } 106 | 107 | @Test 108 | fun isOdd_meansThisShortIsOdd() { 109 | constraint { 110 | isOdd() 111 | }.allMatch( 112 | 1, 113 | 3, 114 | 5, 115 | 7, 116 | 125 117 | ).allFail( 118 | 0, 119 | 2, 120 | 4, 121 | 6, 122 | 8, 123 | 126 124 | ) 125 | } 126 | 127 | @Test 128 | fun isPositive_meansThisShortIsPositive() { 129 | 130 | constraint { 131 | isPositive(false) 132 | }.allMatch( 133 | 1, 134 | 2, 135 | 3, 136 | 4, 137 | 125 138 | ).allFail( 139 | 0, 140 | -1, 141 | -2, 142 | -3, 143 | -4, 144 | -125 145 | ) 146 | 147 | constraint { 148 | isPositive(true) 149 | }.allMatch( 150 | 0, 151 | 1, 152 | 2, 153 | 3, 154 | 4, 155 | 125 156 | ).allFail( 157 | -1, 158 | -2, 159 | -3, 160 | -4, 161 | -125 162 | ) 163 | } 164 | 165 | @Test 166 | fun isNegative_meansThisShortIsNegative() { 167 | 168 | constraint { 169 | isNegative(false) 170 | }.allMatch( 171 | -1, 172 | -2, 173 | -3, 174 | -4, 175 | -125 176 | ).allFail( 177 | 0, 178 | 1, 179 | 2, 180 | 3, 181 | 4, 182 | 125 183 | ) 184 | 185 | constraint { 186 | isNegative(true) 187 | }.allMatch( 188 | 0, 189 | -1, 190 | -2, 191 | -3, 192 | -4, 193 | -125 194 | ).allFail( 195 | 1, 196 | 2, 197 | 3, 198 | 4, 199 | 125 200 | ) 201 | } 202 | 203 | @Test 204 | fun isZero_meansThisShortEqualsZero() { 205 | constraint { 206 | isZero() 207 | }.allMatch( 208 | 0 209 | ).allFail( 210 | -125, 211 | -3, 212 | -4, 213 | -2, 214 | -1, 215 | 1, 216 | 2, 217 | 3, 218 | 4, 219 | 125 220 | ) 221 | } 222 | 223 | @Test 224 | fun isNotZero_meansThisShortDoesNotEqualZero() { 225 | constraint { 226 | isNotZero() 227 | }.allMatch( 228 | -125, 229 | -3, 230 | -4, 231 | -2, 232 | -1, 233 | 1, 234 | 2, 235 | 3, 236 | 4, 237 | 125 238 | ).allFail( 239 | 0 240 | ) 241 | } 242 | 243 | @Test 244 | fun isPrime_meansThisShortIsPrime() { 245 | constraint { 246 | isPrime() 247 | }.allMatch( 248 | 2, 249 | 3, 250 | 5, 251 | 7, 252 | 11, 253 | 67, 254 | 71, 255 | 73, 256 | 79, 257 | 83, 258 | 89, 259 | 97 260 | ).allFail( 261 | -5, 262 | -4, 263 | -3, 264 | -2, 265 | -1, 266 | 0, 267 | 1, 268 | 4, 269 | 6, 270 | 8, 271 | 9, 272 | 56, 273 | 57, 274 | 58, 275 | 60, 276 | 62, 277 | 63 278 | ) 279 | } 280 | 281 | @Test 282 | fun isNotPrime_meansThisShortIsNotPrime() { 283 | constraint { 284 | isNotPrime() 285 | }.allMatch( 286 | -5, 287 | -4, 288 | -3, 289 | -2, 290 | -1, 291 | 0, 292 | 1, 293 | 4, 294 | 6, 295 | 8, 296 | 9, 297 | 56, 298 | 57, 299 | 58, 300 | 60, 301 | 62, 302 | 63 303 | ).allFail( 304 | 2, 305 | 3, 306 | 5, 307 | 7, 308 | 11, 309 | 67, 310 | 71, 311 | 73, 312 | 79, 313 | 83, 314 | 89, 315 | 97 316 | ) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /framework/core/src/commonTest/kotlin/dev/ahmedmourad/validation/core/ConstraintBuilderTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.core 2 | 3 | import dev.ahmedmourad.validation.core.validations.max 4 | import kotlin.test.Test 5 | import kotlin.test.* 6 | 7 | @InternalValidationApi 8 | class ConstraintBuilderTests { 9 | 10 | private data class Model(val n: Int?) 11 | 12 | @Test 13 | fun validation_addsDirectValidationToThisConstraint() { 14 | 15 | val expected = ConstraintDescriptor( 16 | "SomeConstraint", 17 | emptyList(), 18 | listOf(ValidationDescriptor { subject }), 19 | emptyList() 20 | ) 21 | 22 | val actual = ConstraintBuilder(expected.violation).apply { 23 | validation { expected.validations.first().validate(subject) } 24 | }.build() 25 | 26 | assertTrue(actual.validations.all { it.validate(true) }) 27 | assertFalse(actual.validations.all { it.validate(false) }) 28 | } 29 | 30 | @Test 31 | fun meta_addsMetadataToThisConstraint() { 32 | 33 | val expectedMeta = MetadataDescriptor("someMeta") { 4 } 34 | 35 | val expected = ConstraintDescriptor( 36 | "SomeConstraint", 37 | emptyList(), 38 | emptyList(), 39 | listOf(expectedMeta) 40 | ) 41 | 42 | val actual = ConstraintBuilder(expected.violation).apply { 43 | meta(expectedMeta.name) { expectedMeta.get(subject) } 44 | }.build() 45 | 46 | val actualMeta = actual.metadata.first() 47 | 48 | assertEquals(expectedMeta.name, actualMeta.name) 49 | assertEquals(expectedMeta.get(5), actualMeta.get(5)) 50 | } 51 | 52 | @Suppress("UNCHECKED_CAST") 53 | @Test 54 | fun include_addsIncludedValidatorToThisConstraint() { 55 | 56 | val intValidator = object : Validator { 57 | override val constraints by describe { 58 | constraint("TooHigh") { 59 | max(5) 60 | } 61 | } 62 | } 63 | 64 | val expectedIncludedValidator = IncludedValidatorDescriptor>( 65 | "someMeta" 66 | ) { subject.n to intValidator } 67 | 68 | val expected = ConstraintDescriptor( 69 | "SomeConstraint", 70 | listOf(expectedIncludedValidator), 71 | emptyList(), 72 | emptyList() 73 | ) 74 | 75 | val actual = ConstraintBuilder(expected.violation).apply { 76 | include(expectedIncludedValidator.meta) { 77 | expectedIncludedValidator.getBinding(subject) 78 | } 79 | }.build() 80 | 81 | val actualIncludedValidator = actual.includedValidators.first() as IncludedValidatorDescriptor> 82 | 83 | assertEquals(expectedIncludedValidator.meta, actualIncludedValidator.meta) 84 | assertEquals( 85 | expectedIncludedValidator.getBinding(Model(5)), 86 | actualIncludedValidator.getBinding(Model(5)) 87 | ) 88 | } 89 | 90 | @Suppress("UNCHECKED_CAST") 91 | @Test 92 | fun include1_addsIncludedValidatorToThisConstraint() { 93 | 94 | val intValidator = object : Validator { 95 | override val constraints by describe { 96 | constraint("TooHigh") { 97 | max(5) 98 | } 99 | } 100 | } 101 | 102 | val expectedIncludedValidator = IncludedValidatorDescriptor>( 103 | "someMeta" 104 | ) { 5 to intValidator } 105 | 106 | val expected = ConstraintDescriptor( 107 | "SomeConstraint", 108 | listOf(expectedIncludedValidator), 109 | emptyList(), 110 | emptyList() 111 | ) 112 | 113 | val actual = ConstraintBuilder(expected.violation).apply { 114 | include(expectedIncludedValidator.meta) { 115 | expectedIncludedValidator.getBinding(subject) 116 | } 117 | }.build() 118 | 119 | val actualIncludedValidator = actual.includedValidators.first() as IncludedValidatorDescriptor> 120 | 121 | assertEquals(expectedIncludedValidator.meta, actualIncludedValidator.meta) 122 | assertEquals( 123 | expectedIncludedValidator.getBinding(5), 124 | actualIncludedValidator.getBinding(5) 125 | ) 126 | } 127 | 128 | @Test 129 | fun include2_addsIncludedValidatorToThisConstraint() { 130 | 131 | val intValidator = object : Validator { 132 | override val constraints by describe { } 133 | } 134 | 135 | val model = Model(5) 136 | 137 | val constraint = ConstraintBuilder("SomeConstraint").apply { 138 | include("someMeta") { 139 | model::n to intValidator 140 | } 141 | 142 | }.build() 143 | 144 | assertTrue(constraint.includedValidators.all { ic -> ic.isValid(7) { (it as Int) < 6 } }) 145 | assertFalse(constraint.includedValidators.all { ic -> ic.isValid(7) { (it as Int) > 6 } }) 146 | } 147 | 148 | @Test 149 | fun on_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 150 | 151 | val constraint = ConstraintBuilder("SomeConstraint").apply { 152 | on(String::length) { 153 | max(5) 154 | } 155 | }.build() 156 | 157 | assertTrue(constraint.validations.all { it.validate("123") }) 158 | assertFalse(constraint.validations.all { it.validate("123456") }) 159 | } 160 | 161 | @Test 162 | fun on1_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 163 | 164 | val constraint = ConstraintBuilder("SomeConstraint").apply { 165 | on({ subject.length }) { 166 | max(5) 167 | } 168 | }.build() 169 | 170 | assertTrue(constraint.validations.all { it.validate("123") }) 171 | assertFalse(constraint.validations.all { it.validate("123456") }) 172 | } 173 | 174 | @Test 175 | fun on2_changesTheConstraintScopeFromTheSubjectTypeToTheGivenItem() { 176 | 177 | val constraint = ConstraintBuilder("SomeConstraint").apply { 178 | on("123"::length) { 179 | max(5) 180 | } 181 | }.build() 182 | 183 | assertTrue(constraint.validations.all { it.validate("123") }) 184 | assertTrue(constraint.validations.all { it.validate("123456") }) 185 | } 186 | 187 | @Test 188 | fun ifExists_appliesTheValidationsIfTheValueIsNotNullOtherwiseItSucceeds() { 189 | 190 | val constraint = ConstraintBuilder("SomeConstraint").apply { 191 | on(Model::n) ifExists { 192 | max(5) 193 | } 194 | }.build() 195 | 196 | assertTrue(constraint.validations.all { it.validate(Model(null)) }) 197 | assertTrue(constraint.validations.all { it.validate(Model(3)) }) 198 | assertFalse(constraint.validations.all { it.validate(Model(6)) }) 199 | } 200 | 201 | @Test 202 | fun mustExists_appliesTheValidationsIfTheValueIsNotNullOtherwiseItFails() { 203 | 204 | val constraint = ConstraintBuilder("SomeConstraint").apply { 205 | on(Model::n) mustExist { 206 | max(5) 207 | } 208 | }.build() 209 | 210 | assertFalse(constraint.validations.all { it.validate(Model(null)) }) 211 | assertTrue(constraint.validations.all { it.validate(Model(3)) }) 212 | assertFalse(constraint.validations.all { it.validate(Model(6)) }) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/test/kotlin/dev/ahmedmourad/validation/compiler/ValidationContextGeneratorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import org.junit.Assert 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.rules.TemporaryFolder 9 | 10 | class ValidationContextGeneratorTests { 11 | 12 | @Rule 13 | @JvmField 14 | var temporaryFolder: TemporaryFolder = TemporaryFolder() 15 | 16 | @Test 17 | fun `A ValidationContext interface is generated for each validator`() { 18 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 19 | 20 | object SomeValidator : Validator { 21 | override val constraints by describe { 22 | constraint("FirstViolation") { } 23 | } 24 | } 25 | 26 | fun main() { 27 | val context: IntValidationContext = object : IntValidationContext { } 28 | } 29 | """)) 30 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 31 | } 32 | 33 | @Test 34 | fun `subjectAlias applies an alias to the subject type`() { 35 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 36 | 37 | @ValidatorConfig(subjectAlias = "DeluxeInt") 38 | object SomeValidator : Validator { 39 | override val constraints by describe { 40 | constraint("FirstViolation") { } 41 | } 42 | } 43 | 44 | fun main() { 45 | val context: DeluxeIntValidationContext = object : DeluxeIntValidationContext { } 46 | } 47 | """ 48 | )) 49 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 50 | } 51 | 52 | @Test 53 | fun `The ValidationContext interface inherits the type params of the validator`() { 54 | 55 | val failedResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 56 | 57 | class SomeValidator, M> : Validator { 58 | override val constraints by describe { 59 | constraint("FirstViolation") { } 60 | } 61 | } 62 | 63 | fun main() { 64 | val context = object : IntValidationContext { } 65 | } 66 | """ 67 | )) 68 | Assert.assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, failedResult.exitCode) 69 | 70 | val failedResult1 = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 71 | 72 | class SomeValidator, M> : Validator { 73 | override val constraints by describe { 74 | constraint("FirstViolation") { } 75 | } 76 | } 77 | 78 | fun main() { 79 | val context = object : IntValidationContext { } 80 | } 81 | """ 82 | )) 83 | Assert.assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, failedResult1.exitCode) 84 | 85 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 86 | 87 | class SomeValidator, M> : Validator { 88 | override val constraints by describe { 89 | constraint("FirstViolation") { } 90 | } 91 | } 92 | 93 | fun main() { 94 | val context = object : IntValidationContext, Int> { } 95 | val context1 = object : IntValidationContext, Int> { } 96 | } 97 | """)) 98 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 99 | } 100 | 101 | @Test 102 | fun `The ValidationContext interface inherits the ValidationContext of every included validator`() { 103 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 104 | 105 | object StringValidator : Validator { 106 | override val constraints by describe { 107 | constraint("FirstViolation") { } 108 | } 109 | } 110 | 111 | class IntValidator : Validator { 112 | override val constraints by describe { 113 | constraint("FirstViolation") { } 114 | } 115 | } 116 | 117 | @ValidatorConfig(subjectAlias = "Int1") 118 | class Int1Validator> : Validator { 119 | override val constraints by describe { 120 | constraint("FirstViolation") { 121 | include("v", { 4 }) { _, _ -> 122 | IntValidator() 123 | } 124 | } 125 | } 126 | } 127 | 128 | @ValidatorConfig(subjectAlias = "Int2") 129 | class Int2Validator> : Validator { 130 | override val constraints by describe { 131 | constraint("FirstViolation") { 132 | include("v", { 4 }) { _, _ -> 133 | Int1Validator() 134 | } 135 | } 136 | } 137 | } 138 | 139 | @ValidatorConfig(subjectAlias = "Int3") 140 | class Int3Validator> : Validator { 141 | override val constraints by describe { 142 | constraint("FirstViolation") { 143 | include("i", { 4 }) { _, _ -> 144 | Int2Validator() 145 | } 146 | include("s", { "4" }) { _, _ -> 147 | StringValidator 148 | } 149 | } 150 | } 151 | } 152 | 153 | object StringValidationContextInstance : StringValidationContext 154 | object IntValidationContextInstance : IntValidationContext 155 | object Int1ValidationContextInstance : Int1ValidationContext> 156 | object Int2ValidationContextInstance : Int2ValidationContext> 157 | object Int3ValidationContextInstance : Int3ValidationContext> 158 | 159 | fun main() { 160 | 161 | val i1: IntValidationContext = Int1ValidationContextInstance 162 | 163 | val i2: IntValidationContext = Int2ValidationContextInstance 164 | val i21: Int1ValidationContext> = Int2ValidationContextInstance 165 | 166 | val i3: IntValidationContext = Int3ValidationContextInstance 167 | val i31: Int1ValidationContext> = Int3ValidationContextInstance 168 | val i32: Int2ValidationContext> = Int3ValidationContextInstance 169 | val i33: StringValidationContext = Int3ValidationContextInstance 170 | } 171 | """ 172 | )) 173 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 174 | } 175 | 176 | private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { 177 | return prepareCompilation(temporaryFolder, *sourceFiles).compile() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /framework/compiler-plugin/src/test/kotlin/dev/ahmedmourad/validation/compiler/ViolationsGeneratorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.ahmedmourad.validation.compiler 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import org.junit.Assert 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.rules.TemporaryFolder 9 | 10 | class ViolationsGeneratorTests { 11 | 12 | @Rule 13 | @JvmField 14 | var temporaryFolder: TemporaryFolder = TemporaryFolder() 15 | 16 | @Test 17 | fun `A violations sealed class should be created for the validator with a child for each constraint`() { 18 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 19 | 20 | object SomeValidator : Validator { 21 | override val constraints by describe { 22 | constraint("FirstViolation") { } 23 | constraint("SecondViolation") { } 24 | constraint("ThirdViolation") { } 25 | } 26 | } 27 | 28 | fun main() { 29 | 30 | val first: IntViolation = IntViolation.FirstViolation 31 | val second: IntViolation = IntViolation.SecondViolation 32 | 33 | val isItReallySealed = when (val third: IntViolation = IntViolation.ThirdViolation) { 34 | IntViolation.FirstViolation -> 1 35 | IntViolation.SecondViolation -> 2 36 | IntViolation.ThirdViolation -> 3 37 | } 38 | } 39 | """)) 40 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 41 | } 42 | 43 | @Test 44 | fun `An object is created when there are no metas and a class is created when there are metas`() { 45 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 46 | 47 | object SomeValidator : Validator { 48 | override val constraints by describe { 49 | constraint("FirstViolation") { 50 | meta("v") { 4 } 51 | } 52 | constraint("SecondViolation") { } 53 | } 54 | } 55 | 56 | fun main() { 57 | 58 | val first: IntViolation = IntViolation.FirstViolation(44) 59 | 60 | val hello = when (val second: IntViolation = IntViolation.SecondViolation) { 61 | is IntViolation.FirstViolation -> 1 62 | IntViolation.SecondViolation -> 2 63 | } 64 | } 65 | """)) 66 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 67 | } 68 | 69 | @Test 70 | fun `subjectAlias applies an alias to the subject type`() { 71 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 72 | 73 | object SomeValidator : Validator { 74 | override val constraints by describe { 75 | constraint("FirstViolation") { } 76 | } 77 | } 78 | 79 | @ValidatorConfig(subjectAlias = "DeluxeInt") 80 | object AnotherValidator : Validator { 81 | override val constraints by describe { 82 | constraint("SecondViolation") { } 83 | } 84 | } 85 | 86 | fun main() { 87 | 88 | val first = when (val f: IntViolation = IntViolation.FirstViolation) { 89 | IntViolation.FirstViolation -> 1 90 | } 91 | 92 | val second = when (val s: DeluxeIntViolation = DeluxeIntViolation.SecondViolation) { 93 | DeluxeIntViolation.SecondViolation -> 2 94 | } 95 | } 96 | """ 97 | )) 98 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 99 | } 100 | 101 | @Test 102 | fun `meta adds a property to the violation of the corresponding constraint`() { 103 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 104 | 105 | object SomeValidator : Validator { 106 | override val constraints by describe { 107 | constraint("FirstViolation") { 108 | meta("value") { 5 } 109 | meta("another") { 4 to "Hi" } 110 | meta("another1") { emptyList() } 111 | } 112 | } 113 | } 114 | 115 | object AnotherValidator : Validator { 116 | override val constraints by describe { 117 | constraint("SecondViolation") { 118 | meta("value") { 5 } 119 | } 120 | } 121 | } 122 | 123 | fun main() { 124 | 125 | val first: IntViolation = IntViolation.FirstViolation( 126 | value = 5, 127 | another = 3 to "Hello", 128 | another1 = emptyList() 129 | ) 130 | val second: StringViolation = StringViolation.SecondViolation( 131 | value = 44 132 | ) 133 | 134 | val f = when (first) { 135 | is IntViolation.FirstViolation -> 1 136 | } 137 | 138 | val s = when (second) { 139 | is StringViolation.SecondViolation -> 2 140 | } 141 | } 142 | """ 143 | )) 144 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 145 | } 146 | 147 | @Test 148 | fun `include adds a list of the violations of the included validator as a property of the violation of the corresponding constraint`() { 149 | val successResult = compile(SourceFile.kotlin("Test.kt", """$PACKAGE_AND_IMPORTS 150 | 151 | object IntValidator : Validator { 152 | override val constraints by describe { 153 | constraint("FirstViolation") { } 154 | } 155 | } 156 | 157 | @ValidatorConfig(subjectAlias = "LongInt") 158 | object LongIntValidator : Validator { 159 | override val constraints by describe { 160 | constraint("FirstViolation") { } 161 | } 162 | } 163 | 164 | object AnotherValidator : Validator { 165 | override val constraints by describe { 166 | constraint("SecondViolation") { 167 | meta("value") { 5 } 168 | include("value1", { 4 }) { _, _ -> 169 | IntValidator 170 | } 171 | include("value2", { 54 }) { _, _ -> 172 | LongIntValidator 173 | } 174 | } 175 | } 176 | } 177 | 178 | fun main() { 179 | val first: StringViolation = StringViolation.SecondViolation( 180 | value = 5, 181 | value1 = emptyList(), 182 | value2 = emptyList() 183 | ) 184 | } 185 | """ 186 | )) 187 | Assert.assertEquals(KotlinCompilation.ExitCode.OK, successResult.exitCode) 188 | } 189 | 190 | private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result { 191 | return prepareCompilation(temporaryFolder, *sourceFiles).compile() 192 | } 193 | } 194 | --------------------------------------------------------------------------------