├── .gitignore ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── ir │ │ └── amirab │ │ └── validator │ │ └── android │ │ └── TranslatorAdapter.kt │ └── res │ ├── values-fa │ └── strings.xml │ └── values │ └── strings.xml ├── build.gradle.kts ├── core ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── ir │ └── amirab │ └── validator │ ├── Rule.kt │ ├── extensions.kt │ ├── plugins │ ├── common │ │ ├── email.kt │ │ ├── equals.kt │ │ ├── one_of.kt │ │ └── phone_validator.kt │ ├── dependents │ │ └── same_as.kt │ ├── operators │ │ ├── and.kt │ │ ├── combine.kt │ │ └── or.kt │ └── string │ │ ├── absent.kt │ │ ├── at_least_has_n_numbers.kt │ │ ├── at_least_lower_case.kt │ │ ├── at_least_upper_case.kt │ │ ├── empty.kt │ │ ├── ends_with.kt │ │ ├── in_range.kt │ │ ├── not_empty.kt │ │ ├── pattern_match.kt │ │ ├── present.kt │ │ ├── starts_with.kt │ │ └── starts_with_engish_character.kt │ ├── reason │ ├── CompositeReason.kt │ ├── Reason.kt │ ├── ReasonList.kt │ ├── SingleReason.kt │ └── reason_extensions.kt │ ├── result │ ├── ObjectValidationResult.kt │ └── ValidationResult.kt │ ├── rule │ ├── BaseRule.kt │ ├── CompositeRule.kt │ ├── DependentInput.kt │ └── DependentRule.kt │ ├── rule_builders.kt │ └── translate │ ├── DefaultValidatedTranslationAdapter.kt │ ├── DefaultValidatedTranslationAdapterBase.kt │ ├── ValidatedTranslation.kt │ └── ValidatedTranslationAdapter.kt ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── static └── logo.svg /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | 12 | *.log 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

Kotlin Validator

4 |

5 |

6 | validate your inputs or objects with power of the kotlin typesafe builders 7 |

8 | 9 | Kotlin Validator Jitpack 10 | 11 |

12 | 13 | ## Usage 14 | quick introduction 15 | ```kotlin 16 | val result=email.validate("yourEmail@gmail.com") 17 | if(result.isValid){ 18 | println("provided email is valid ,user:${result.user},host:${result.host}") 19 | }else{ 20 | println(result.reason!!.translate()) 21 | } 22 | ``` 23 | or 24 | ```kotlin 25 | val result=compositeRule{ 26 | User::email mustBe email 27 | User::password mustBe inRange(8..64) 28 | }.validate(user) 29 | if(!result.isValid){ 30 | println(result[User::email]?.translate()) 31 | println(result[User::password]?.translate()) 32 | } 33 | ``` 34 | 35 | 36 | # Setup 37 | ## Dependency 38 | put this into your gradle script 39 | ```groovy 40 | repositories { 41 | //... 42 | maven { url "https://jitpack.io" } 43 | } 44 | dependencies { 45 | //... 46 | //android usage 47 | implementation 'com.github.amir1376.kotlin-validator:android:$version' 48 | 49 | //core (jvm) 50 | implementation 'com.github.amir1376.kotlin-validator:core:$version' 51 | } 52 | ``` 53 | 54 | 55 | 56 | ## Android 57 | in your app entry point initialize validator translation 58 | ```kotlin 59 | ValidatedTranslation.initDefaultAndroidAdapter(context) 60 | //include default translations 61 | .applyDefaultTranslations() 62 | 63 | ``` 64 | 65 | 66 | ## JVM 67 | 68 | before any use of translation provide a TranslationAdapter 69 | here is the default adapter 70 | ```kotlin 71 | ValidatedTranslation.initWithDefault() 72 | ``` 73 | 74 | 75 | # Features 76 | 77 | ## Combining rules together 78 | you can validate your input by multiple rules 79 | ### Here is an example 80 | ```kotlin 81 | val result=(email or empty).validate(input) 82 | ``` 83 | 84 | ## Validate nested objects 85 | sometimes you want to validate a model 86 | the library has support this too 87 | for example you have the following models 88 | ```kotlin 89 | data class User( 90 | val login:String, 91 | val name:Name, 92 | val password:String, 93 | val confirmPassword:String, 94 | val gender:String 95 | ) 96 | data class Name( 97 | val first:String, 98 | val last:String, 99 | ) 100 | enum class Gender{ 101 | Male,Female 102 | } 103 | ``` 104 | you can validate this with this rule 105 | ```kotlin 106 | //inside a suspend function 107 | val userValidation=compositeRule{ 108 | User::login mustBe (startWithEnglishCharacter and inRange(6..64)) 109 | User::name mustBe compositeRule{ 110 | Name::first mustBe notEmpty 111 | Name::last mustBe notEmpty 112 | } 113 | User::password mustBe ( 114 | containsAtLeastLowerCase(1) and 115 | containsAtLeastUpperCase(1) and 116 | (containsAtLeastNumber(1)) and 117 | inRange(8..64) 118 | ) 119 | User::confirmPassword mustBe sameAs(User::password) 120 | User::gender mustBe oneOf() 121 | } 122 | //user input 123 | val user:User/*retrieve user object*/ 124 | val result=userValidation.validate(user) 125 | if(!result.isValid){ 126 | // you can get reason for each property 127 | result[User::login] 128 | } 129 | ``` 130 | as you can see in the above code, user password has a complex rule , 131 | but you can extract it to a variable 132 | and because these rules are stateless (they don't store any reference of input) 133 | you can safely use this combination multiple times 134 | ```kotlin 135 | val strongPassword = containsAtLeastLowerCase(1) and 136 | containsAtLeastUpperCase(1) and 137 | (containsAtLeastNumber(1)) and 138 | inRange(8..64) 139 | //.... then replace 140 | User::password mustBe strongPassword 141 | ``` 142 | # Customization 143 | ## Creating your own rules 144 | Of course, you can create your own rules with ease 145 | here is an example 146 | ```kotlin 147 | val phone get() = rule{ input-> 148 | if(phonePattern.matchEntire(input)){ 149 | thenValid() 150 | }else{ 151 | because("your provided phone number is not valid") 152 | } 153 | } 154 | ``` 155 | ## Localization 156 | if your app has support of multiple language 157 | when building your rules ,you have to provide `Reason` 158 | instead of raw string 159 | ```kotlin 160 | object PhoneInvalidReason:SingleReason 161 | val phone get() = rule{ input-> 162 | if(phonePattern.matchEntire(input)){ 163 | thenValid() 164 | }else{ 165 | because(PhoneInvalidReason) 166 | } 167 | } 168 | ``` 169 | then you have to provide PhoneInvalidReason translation to the adapter 170 | ```kotlin 171 | ValidatedTranslation.adapter.apply{ 172 | //declare translation here 173 | //this is up to you that how you want to translate that message 174 | PhoneInvalidReason::class providedBy { 175 | "your provided phone number is not valid" 176 | } 177 | } 178 | ``` 179 | 180 | otherwise, if you are not interested on default translation approach, 181 | then you can create your own translation by implementing `ValidatedTranslationAdapter` 182 | 183 | 184 | 185 | ## Android support 186 | at the moment ,we have separated android module 187 | that contains an Android translation adapter, 188 | it has some useful extensions to provide translation 189 | from string resources 190 | ```kotlin 191 | Validator.android().apply{ 192 | //declare translation here 193 | //this is up to you that how you want to translate that message 194 | PhoneInvalidReason::class providedByResource (R.string.my_validation_phone_invalid) 195 | } 196 | ``` 197 | 198 | if you use this library for android, you can use default translation provided 199 | by the android module. 200 | 201 | 202 | currently supported languages are 203 | * English (default) 204 | * Persian 205 | 206 | 207 | ### Coroutines support 208 | the `validate` method is a suspend function 209 | accordingly, rules are all suspend functions too, 210 | so you can have suspended calls on them 211 | and because of that, you have to call `validate` only in a coroutine scope 212 | the core artifact doesn't have any suspend calls 213 | this aproach is choosen for support coroutines 214 | # Attention 215 | this library is still under beta 216 | so that may have bugs 217 | 218 | ## Contribution 219 | you can consider a pull request, 220 | if you see unexpected behaviors in the library 221 | or write more common plugins 222 | 223 | otherwise, if you have suggestions or have seen something weird out there (😁), 224 | please submit an issue 225 | 226 | ### TODOS 227 | * write tests 228 | * write more plugins 229 | * support kotlin multiplatform 230 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | `maven-publish` 5 | } 6 | // android specific variables 7 | val compileSdk by extra {31} 8 | val minSdk by extra { 19 } 9 | val targetSdk by extra { 31 } 10 | 11 | android { 12 | 13 | compileSdk = extra["compileSdk"] as Int 14 | 15 | defaultConfig { 16 | minSdk = extra["minSdk"] as Int 17 | targetSdk = extra["targetSdk"] as Int 18 | } 19 | 20 | compileOptions { 21 | sourceCompatibility = JavaVersion.VERSION_1_8 22 | targetCompatibility = JavaVersion.VERSION_1_8 23 | } 24 | kotlinOptions { 25 | jvmTarget = "1.8" 26 | } 27 | } 28 | dependencies { 29 | api(project(":core")) 30 | } 31 | //from (android.sourceSets.findByName("main")!!.java.srcDirs) 32 | //classifier="fucking-src" 33 | publishing { 34 | publications { 35 | create("maven", MavenPublication::class) { 36 | afterEvaluate { 37 | from(components["release"]) 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/ir/amirab/validator/android/TranslatorAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.android 2 | 3 | import android.content.Context 4 | import ir.amirab.validator.R 5 | import ir.amirab.validator.plugins.operators.AndInvalidReason 6 | import ir.amirab.validator.plugins.string.InRangeInvalidReason 7 | import ir.amirab.validator.plugins.operators.OrInvalidReason 8 | import ir.amirab.validator.plugins.common.EmailInvalidReason 9 | import ir.amirab.validator.plugins.common.EmailWithHosts 10 | import ir.amirab.validator.plugins.common.EqualsInvalidReason 11 | import ir.amirab.validator.plugins.common.OneOfInvalidReason 12 | import ir.amirab.validator.plugins.dependents.SameAsInvalidReason 13 | import ir.amirab.validator.plugins.string.* 14 | import ir.amirab.validator.reason.SingleReason 15 | import ir.amirab.validator.translate.DefaultValidatedTranslationAdapter 16 | import ir.amirab.validator.translate.TranslateMethod 17 | import ir.amirab.validator.translate.ValidatedTranslation 18 | import kotlin.reflect.KClass 19 | 20 | /** 21 | * android specific translate methods that use string resource 22 | * they are provided in android translation adapter 23 | * @see [AndroidTranslationAdapter.providedByResource] 24 | */ 25 | class FromStringResource(val stringRes: Int, vararg val args: Any?) : TranslateMethod 26 | fun interface CallableReturnsStringResource 27 | : TranslateMethod, (T) -> FromStringResource 28 | 29 | /** 30 | * this is a default translation for android system backed by string resources 31 | * developers can call this to apply default translation for android 32 | * or / then override anything they wish 33 | * by one of [AndroidTranslationAdapter.providedBy] 34 | * or [AndroidTranslationAdapter.providedByResource] methods 35 | */ 36 | fun AndroidTranslationAdapter.applyDefaultTranslations() = apply { 37 | registerStrings() 38 | registerCommons() 39 | registerOperators() 40 | registerDependents() 41 | } 42 | 43 | fun AndroidTranslationAdapter.registerStrings() { 44 | NotEmptyInvalidReason::class providedByResource (R.string.validator_must_not_empty) 45 | EmptyInvalidReason::class providedByResource (R.string.validator_must_empty) 46 | PatternMatchInvalidReason::class providedByResource { 47 | FromStringResource(R.string.validator_must_pattern_patch,pattern) 48 | } 49 | InRangeInvalidReason::class providedByResource { 50 | FromStringResource(R.string.validator_must_in_range, range.first, range.last) 51 | } 52 | ContainsAtLeastLowerCaseInvalidReason::class providedByResource { 53 | FromStringResource(R.string.validator_must_contains_at_least_lowercase, expected) 54 | } 55 | ContainsAtLeastUpperCaseInvalidReason::class providedByResource { 56 | FromStringResource(R.string.validator_must_contains_at_least_upper_case, expected) 57 | } 58 | ContainsAtLeastNumberInvalidReason::class providedByResource { 59 | FromStringResource(R.string.validator_must_contains_at_least_number, expected) 60 | } 61 | PresentInvalidReason::class providedByResource { 62 | FromStringResource(R.string.validator_must_present, present) 63 | } 64 | AbsentInvalidReason::class providedByResource { 65 | FromStringResource(R.string.validator_must_absent, absent) 66 | } 67 | StartsWithInvalidReason::class providedByResource { 68 | FromStringResource(R.string.validator_must_starts_with, prefix) 69 | } 70 | StartsWithEnglishCharacterInvalidReason::class providedByResource R.string.validator_must_starts_with_eng_char 71 | EndsWithInvalidReason::class providedByResource { 72 | FromStringResource(R.string.validator_must_starts_with, postfix) 73 | } 74 | } 75 | 76 | fun AndroidTranslationAdapter.registerCommons() { 77 | EqualsInvalidReason::class providedByResource { 78 | FromStringResource( 79 | R.string.validator_must_be_equal_to, mustBeEqualTo.toString() 80 | ) 81 | } 82 | OneOfInvalidReason::class providedByResource { 83 | FromStringResource( 84 | R.string.validator_must_be_one_of, 85 | enums.joinToString(",").run { "($this)" }) 86 | } 87 | EmailInvalidReason::class providedByResource (R.string.validator_must_email) 88 | EmailWithHosts::class providedByResource { 89 | FromStringResource( 90 | R.string.validator_must_email_host, 91 | list.joinToString(",").run { "($this)" }) 92 | } 93 | } 94 | 95 | fun AndroidTranslationAdapter.registerDependents() { 96 | SameAsInvalidReason::class providedByResource { 97 | FromStringResource(R.string.validator_must_same_as, propName) 98 | } 99 | } 100 | 101 | fun AndroidTranslationAdapter.registerOperators() { 102 | OrInvalidReason::class providedByResource R.string.validator_or 103 | AndInvalidReason::class providedByResource R.string.validator_and 104 | } 105 | 106 | 107 | class AndroidTranslationAdapter( 108 | private val context: Context 109 | ) : DefaultValidatedTranslationAdapter() { 110 | 111 | override fun retrieve(method: TranslateMethod, reason: SingleReason): String { 112 | return when (method) { 113 | is FromStringResource -> { 114 | context.getString(method.stringRes, method.args) 115 | } 116 | is CallableReturnsStringResource<*> -> { 117 | val resource = 118 | (method as CallableReturnsStringResource).invoke(reason) 119 | context.getString(resource.stringRes, *resource.args) 120 | } 121 | else -> super.retrieve(method, reason) 122 | } 123 | } 124 | 125 | inline infix fun KClass.providedByResource(stringRes: Int) = 126 | apply { 127 | extend(T::class, FromStringResource(stringRes)) 128 | } 129 | 130 | @JvmName("providedByResourceBasedOnCallable") 131 | inline infix fun KClass.providedByResource(noinline resourceBasedOnCallable: T.() -> FromStringResource) = 132 | apply { 133 | extend(T::class, CallableReturnsStringResource(resourceBasedOnCallable)) 134 | } 135 | 136 | } 137 | 138 | fun ValidatedTranslation.initDefaultAndroidAdapter(context:Context)= 139 | init(AndroidTranslationAdapter(context)) 140 | fun ValidatedTranslation.android(): AndroidTranslationAdapter { 141 | when (val adapter = adapter) { 142 | is AndroidTranslationAdapter -> { 143 | return adapter 144 | } 145 | else -> { 146 | throw Exception("adapter was set but not android adapter it is ${adapter::class.qualifiedName}") 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /android/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | باید 4 | یا 5 | و 6 | باید ایمیل باشد 7 | باید یکی ازین دامنه ها باشد %s 8 | باید حداقل شامل %s حرف کوچک لاتین باشد 9 | باید حداقل شامل %s حرف بزرگ لاتین باشد 10 | باید حداقل شامل %s رقم باشد 11 | نباید شامل %s باشد 12 | باید شامل %s باشد 13 | باید یکی از این موارد باشد %s 14 | باید شامل این علامت ها باشد %s 15 | طول باید حداقل %s و حداکثر %s باشد 16 | نباید خالی باشد 17 | باید خالی باشد 18 | با الگوی %s سازگاری ندارد 19 | باید با %s شروع شود 20 | باید با کاراکتر انگلیسی شروع شود 21 | باید با %s تمام شود 22 | مقدار باید با ورودی %s یکسان باشد 23 | باید برابر %s باشد 24 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | must 3 | or 4 | and 5 | must be email 6 | email host must be on of %s 7 | must contains at least %s lowercase 8 | must contains at least %s uppercase 9 | must contains at least %s number 10 | must not contains %s 11 | must contains %s 12 | must be one of %s 13 | must contain these symbols %s 14 | must be at least %s and max %s 15 | must not be empty 16 | must empty 17 | invalid input according to %s pattern 18 | must starts with %s 19 | must starts with english character 20 | must ends with %s 21 | value must be same as value of %s property 22 | must be equal to %s 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `maven-publish` 3 | } 4 | allprojects { 5 | group = "ir.amirab.validator" 6 | version = "0.0.1-beta.3" 7 | repositories { 8 | google() 9 | mavenCentral() 10 | } 11 | } 12 | buildscript { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | val libs = project.extensions.getByType().named("libs") as org.gradle.accessors.dm.LibrariesForLibs 18 | dependencies{ 19 | classpath (libs.kotlin.pluginGradle) 20 | classpath (libs.android.pluginGradle) 21 | } 22 | } -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | kotlin("jvm") 4 | `maven-publish` 5 | } 6 | 7 | java { 8 | withJavadocJar() 9 | withSourcesJar() 10 | sourceCompatibility = JavaVersion.VERSION_1_8 11 | targetCompatibility = JavaVersion.VERSION_1_8 12 | } 13 | dependencies{ 14 | // nothing here 15 | } 16 | 17 | publishing { 18 | publications { 19 | create("maven", MavenPublication::class) { 20 | afterEvaluate { 21 | from(components["java"]) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/Rule.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator 2 | 3 | import ir.amirab.validator.result.ValidationResult 4 | 5 | interface Rule{ 6 | suspend fun validate(input:INPUT):RESULT 7 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/extensions.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator 2 | 3 | import ir.amirab.validator.result.ValidationResult 4 | 5 | suspend fun Rule.validateOrNull(input: T): T? { 6 | return when { 7 | validate(input).isValid -> input 8 | else -> null 9 | } 10 | } 11 | 12 | 13 | fun ValidationResult.ensureIsValid() { 14 | if (!isValid) { 15 | throw IllegalStateException("${this::class.simpleName} is not valid!") 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/common/email.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.common 2 | 3 | import ir.amirab.validator.result.ValidationResult 4 | import ir.amirab.validator.ensureIsValid 5 | import ir.amirab.validator.reason.Reason 6 | import ir.amirab.validator.reason.SingleReason 7 | import ir.amirab.validator.rule 8 | import ir.amirab.validator.rule.BaseRule 9 | 10 | class EmailValidationResult( 11 | override val isValid: Boolean, 12 | override val reason: Reason?, 13 | user: String?, 14 | host: String? 15 | ) : ValidationResult { 16 | private val _host = host 17 | private val _user = user 18 | 19 | val user: String 20 | get() { 21 | ensureIsValid() 22 | return _user!! 23 | } 24 | 25 | val host: String 26 | get() { 27 | ensureIsValid() 28 | return _host!! 29 | } 30 | 31 | } 32 | /** 33 | * Email validation 34 | */ 35 | private val emailRegex by lazy { "([a-zA-Z0-9-._]+)@([a-zA-Z0-9-._]+\\.[a-zA-Z]+)".toRegex() } 36 | object EmailInvalidReason:SingleReason 37 | val email get() = rule { 38 | val match = emailRegex.matchEntire(it) 39 | val isValid = match != null 40 | EmailValidationResult( 41 | isValid = isValid, 42 | reason = if (isValid) null else EmailInvalidReason, 43 | user = match?.groupValues?.getOrNull(1), 44 | host = match?.groupValues?.getOrNull(2) 45 | ) 46 | } 47 | class EmailWithHosts(val list: List) :SingleReason 48 | fun BaseRule.withHosts(list: List) = 49 | rule { 50 | // Be careful not use validate without label ,can stick into recursive calls 51 | val upstream = this@withHosts.validate(it) 52 | if (!upstream.isValid) { 53 | upstream 54 | } else { 55 | val isValid = upstream.host in list 56 | EmailValidationResult( 57 | isValid = isValid, 58 | reason = if (isValid) null else EmailWithHosts(list), 59 | user = upstream.user, 60 | host = upstream.host 61 | ) 62 | } 63 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/common/equals.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.common 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | data class EqualsInvalidReason(val mustBeEqualTo:T):SingleReason 9 | 10 | fun equals(expected:T)= rule { 11 | if (expected==it) 12 | thenValid() 13 | else 14 | because(EqualsInvalidReason(expected)) 15 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/common/one_of.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.common 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | class OneOfInvalidReason(val enums: List) : SingleReason 9 | 10 | /** 11 | * check input for existence in [list] 12 | */ 13 | fun oneOf(list: List) = rule { 14 | if (it in list) { 15 | thenValid() 16 | } else { 17 | because(OneOfInvalidReason(list)) 18 | } 19 | } 20 | 21 | /** 22 | * check input for existence in [items] 23 | */ 24 | fun oneOf(vararg items: T) = oneOf(items.toList()) 25 | 26 | /** 27 | * check input for existence in provided enum [E] 28 | * C the type of enum 29 | * R expected type 30 | * usage : 31 | * enum class Gender{ Male ,Female} 32 | * onOf("Female"){it.name} 33 | * 34 | * note this is just for example 35 | * you can use [oneOf] that has one type parameter it uses EnumType::name transformation by default 36 | * @param transform transform enum to reach the expected property 37 | */ 38 | inline fun , R> oneOf(crossinline transform: (E) -> R) = with( 39 | enumValues().map(transform) 40 | ) { 41 | oneOf(this) 42 | } 43 | 44 | 45 | /** 46 | * shorthand for oneOf with name transformation 47 | */ 48 | inline fun > oneOf() = oneOf { it.name } 49 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/common/phone_validator.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.common 2 | 3 | 4 | /** 5 | * TODO make a list for all countries and pattern for each one and hydrate result in phoneValidationResult 6 | * phone validation 7 | */ 8 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/dependents/same_as.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.dependents 2 | 3 | import ir.amirab.validator.dependentRule 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.result 6 | import ir.amirab.validator.result.ValidationResult 7 | import ir.amirab.validator.rule.DependentRule 8 | import kotlin.reflect.KProperty1 9 | 10 | data class SameAsInvalidReason(val propName: String) : SingleReason 11 | 12 | fun sameAs(prop: KProperty1): DependentRule { 13 | return dependentRule { 14 | val isValid = it.actualInput == prop.get(it.receiver) 15 | result( 16 | isValid, 17 | if (isValid) { 18 | null 19 | } else { 20 | SameAsInvalidReason(prop.name) 21 | } 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/operators/and.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.operators 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.plugins.operators.AndInvalidReason 5 | import ir.amirab.validator.reason.SingleReason 6 | 7 | /** 8 | * logical and for combine rules 9 | * if you have only two rules for simplicity you can use [Rule.and] 10 | * for example 11 | * val newRule = and(rule1,rule2,rule3) 12 | */ 13 | fun and(vararg rules: Rule) = combine( 14 | rules = rules, 15 | check = { results -> 16 | results.all { it.isValid } 17 | }, 18 | getErrors = { results -> 19 | val errors = results.mapNotNull { it.reason } 20 | join(errors, AndInvalidReason) 21 | } 22 | ) 23 | 24 | /** 25 | * logical and for combine two rules 26 | * if you have more than one consider using [and] 27 | * for example 28 | * val newRule = rule1 and rule2 29 | */ 30 | infix fun Rule.and(other: Rule) = and(this@and, other) 31 | 32 | object AndInvalidReason: SingleReason -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/operators/combine.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.operators 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.result.ValidationResult 5 | import ir.amirab.validator.because 6 | import ir.amirab.validator.reason.Reason 7 | import ir.amirab.validator.reason.ReasonList 8 | import ir.amirab.validator.rule 9 | import ir.amirab.validator.thenValid 10 | 11 | /** 12 | * 13 | */ 14 | fun combine( 15 | vararg rules: Rule, 16 | check: (results: List) -> Boolean, 17 | getErrors: Rule.(results: List) -> Reason, 18 | ) = rule { 19 | val results = ArrayList(rules.size) 20 | for ((index, rule) in rules.withIndex()) { 21 | // this is in order 22 | // if we plan to change structure we MUST to change list to static array 23 | results.add(rule.validate(it)) 24 | } 25 | val result = check(results) 26 | if (result) { 27 | thenValid() 28 | } else { 29 | because(getErrors(results)) 30 | } 31 | } 32 | 33 | /** 34 | * join reason with a separator reason 35 | * this is used for combining operands with operator itself 36 | * implementation may change 37 | */ 38 | fun join(errors: List, separator: Reason): ReasonList { 39 | return ReasonList( 40 | errors.flatMapIndexed { index: Int, reason: Reason -> 41 | if (index == 0) { 42 | listOf(reason) 43 | } else { 44 | listOf(separator, reason) 45 | } 46 | } 47 | ) 48 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/operators/or.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.operators 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.reason.SingleReason 5 | 6 | /** 7 | * logical or for combine rules 8 | * if you have only two rules for simplicity you can use [Rule.or] 9 | * for example 10 | * val newRule = or(rule1,rule2,rule3) 11 | */ 12 | fun or(vararg rules: Rule)=combine( 13 | rules = rules, 14 | check = { results -> 15 | results.any { result -> 16 | result.isValid 17 | } 18 | }, 19 | getErrors = { results -> 20 | val errors = results.mapNotNull { it.reason } 21 | join(errors, OrInvalidReason) 22 | } 23 | ) 24 | 25 | /** 26 | * logical or for combine two rules 27 | * if you have more than two rules consider using [or] 28 | * for example 29 | * val newRule = rule1 or rule2 30 | */ 31 | infix fun Rule.or(other: Rule)=or(this@or, other) 32 | 33 | object OrInvalidReason: SingleReason -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/absent.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | class AbsentInvalidReason(val absent:String): SingleReason 9 | 10 | fun absent (value:String)= rule { 11 | val valid = !it.contains(value) 12 | if (valid) 13 | thenValid() 14 | else { 15 | because(AbsentInvalidReason(value)) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/at_least_has_n_numbers.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.plugins.operators.and 5 | import ir.amirab.validator.reason.SingleReason 6 | import ir.amirab.validator.rule 7 | import ir.amirab.validator.thenValid 8 | 9 | val numericCharacters by lazy { "[0-9]".toRegex() } 10 | 11 | class ContainsAtLeastNumberInvalidReason(val expected:Int, val contains:Int): SingleReason 12 | 13 | fun containsAtLeastNumber(expected: Int)= notEmpty and rule { 14 | val r = numericCharacters.findAll(it) 15 | val count = r.count() 16 | val valid = count >= expected 17 | if (valid) 18 | thenValid() 19 | else { 20 | because(ContainsAtLeastNumberInvalidReason(expected, count)) 21 | } 22 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/at_least_lower_case.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | private val lowerCaseCharacters by lazy { "[a-z]".toRegex() } 9 | 10 | class ContainsAtLeastLowerCaseInvalidReason(val contains:Int, val expected:Int): SingleReason 11 | 12 | fun containsAtLeastLowerCase(expected: Int) = rule { 13 | val r = lowerCaseCharacters.findAll(it) 14 | val count = r.count() 15 | val valid = count >= expected 16 | if (valid) 17 | thenValid() 18 | else { 19 | because(ContainsAtLeastLowerCaseInvalidReason(count, expected)) 20 | } 21 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/at_least_upper_case.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | private val upperCaseCharacters by lazy { "[A-Z]".toRegex() } 9 | 10 | class ContainsAtLeastUpperCaseInvalidReason(val contains:Int, val expected:Int): SingleReason 11 | 12 | fun containsAtLeastUpperCase(i: Int) = rule { 13 | val r = upperCaseCharacters.findAll(it) 14 | val count = r.count() 15 | val valid = count >= i 16 | if (valid) 17 | thenValid() 18 | else { 19 | because(ContainsAtLeastUpperCaseInvalidReason(count, i)) 20 | } 21 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/empty.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | object EmptyInvalidReason : SingleReason 9 | val empty = rule { 10 | if (it.isEmpty()) thenValid() 11 | else because(EmptyInvalidReason) 12 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/ends_with.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | class EndsWithInvalidReason(val postfix:String): SingleReason 9 | 10 | fun endsWith (postfix:String)= rule { 11 | val valid = it.endsWith(postfix) 12 | if (valid) 13 | thenValid() 14 | else { 15 | because(EndsWithInvalidReason(postfix)) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/in_range.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | fun inRange(intRange: IntRange) = rule { 9 | val length = it.length 10 | if (length in intRange) { 11 | thenValid() 12 | } else { 13 | because(InRangeInvalidReason(intRange)) 14 | } 15 | } 16 | 17 | //plugins 18 | class InRangeInvalidReason(val range:IntRange): SingleReason -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/not_empty.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | object NotEmptyInvalidReason : SingleReason 9 | val notEmpty = rule { 10 | if (it.isNotEmpty()) thenValid() 11 | else { 12 | because(NotEmptyInvalidReason) 13 | } 14 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/pattern_match.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | data class PatternMatchInvalidReason(val pattern: String) : SingleReason 9 | fun patternMatch(pattern: String) = rule { 10 | val value = it 11 | val valid = pattern.toRegex().matches(value) 12 | if (valid) { 13 | thenValid() 14 | } else { 15 | because(PatternMatchInvalidReason(pattern)) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/present.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | class PresentInvalidReason(val present:String): SingleReason 9 | 10 | fun present (value:String)= rule { 11 | val valid = it.contains(value) 12 | if (valid) 13 | thenValid() 14 | else { 15 | because(PresentInvalidReason(value)) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/starts_with.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.because 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | import ir.amirab.validator.thenValid 7 | 8 | class StartsWithInvalidReason(val prefix:String): SingleReason 9 | 10 | fun startsWith (prefix:String)= rule { 11 | val valid = it.startsWith(prefix) 12 | if (valid) 13 | thenValid() 14 | else { 15 | because(AbsentInvalidReason(prefix)) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/plugins/string/starts_with_engish_character.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.plugins.string 2 | 3 | import ir.amirab.validator.* 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.rule 6 | 7 | 8 | private val startWithEngChar by lazy { "^[a-zA-Z]+".toRegex() } 9 | object StartsWithEnglishCharacterInvalidReason:SingleReason 10 | fun startsWithEnglishCharacter ()=rule{ 11 | val valid = startWithEngChar.matchEntire(it) 12 | if (valid!=null) 13 | thenValid() 14 | else { 15 | because(StartsWithEnglishCharacterInvalidReason) 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/reason/CompositeReason.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.reason 2 | 3 | import kotlin.reflect.KProperty1 4 | 5 | 6 | /** 7 | * this is for composite validation 8 | * result of an object that have nested rule on its properties 9 | * map key is name of that property 10 | * map value is reason for why that property is invalid 11 | */ 12 | data class CompositeReason( 13 | val map: Map 14 | ) : Reason, Map by map{ 15 | operator fun get(property:KProperty1):Reason?{ 16 | return get(property.name) 17 | } 18 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/reason/Reason.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.reason 2 | 3 | /** 4 | * base interface for all possible reason types 5 | */ 6 | sealed interface Reason 7 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/reason/ReasonList.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.reason 2 | 3 | /** 4 | * this is reason produced by logical operators 5 | * represents a list of [Reason] 6 | */ 7 | data class ReasonList( 8 | val reasons:List 9 | ) : Reason -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/reason/SingleReason.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.reason 2 | 3 | /** 4 | * this is a primitive reason 5 | * other reason may contain one or more of this 6 | */ 7 | interface SingleReason: Reason{ 8 | /** 9 | * this is fallback reason if no translation provided for this rule 10 | * [rawReason] is provided to translation adapter 11 | */ 12 | val rawReason: String get() = requireNotNull(this::class.qualifiedName){ 13 | "Single reason subtype is local or a class of an anonymous object " + 14 | "you have to override SingleReason::rawReason if you want to use anonymous object" 15 | } 16 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/reason/reason_extensions.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.reason 2 | 3 | /** 4 | * make reason flatten to [SingleReason] 5 | * this is used for translation 6 | */ 7 | internal fun Reason.flattenToSingleReason(): List { 8 | return when(this){ 9 | is SingleReason ->{ 10 | listOf(this) 11 | } 12 | is ReasonList ->{ 13 | reasons.map { 14 | it.flattenToSingleReason() 15 | }.flatten() 16 | } 17 | // WARNING composite reason keys is ignored this method is only for simple rules 18 | is CompositeReason<*> ->{ 19 | map.values.map { 20 | it.flattenToSingleReason() 21 | }.flatten() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/result/ObjectValidationResult.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.result 2 | 3 | import ir.amirab.validator.reason.CompositeReason 4 | import ir.amirab.validator.reason.Reason 5 | import kotlin.reflect.KProperty1 6 | 7 | class ObjectValidationResult( 8 | override val isValid: Boolean, 9 | override val reason: CompositeReason? 10 | ) : ValidationResult{ 11 | operator fun get(property: KProperty1): Reason? { 12 | @Suppress("UNCHECKED_CAST") 13 | return reason?.let { 14 | it[property] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/result/ValidationResult.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.result 2 | 3 | import ir.amirab.validator.reason.Reason 4 | 5 | /** 6 | * every rule after validation returns a ValidationResult 7 | * or a subclass of it 8 | */ 9 | interface ValidationResult{ 10 | val isValid: Boolean 11 | val reason: Reason? 12 | } 13 | 14 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/rule/BaseRule.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.rule 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.result.ValidationResult 5 | 6 | 7 | /** 8 | * BaseRule class 9 | * please consider use [rule] function overloads to add your own rules 10 | * this is for future compatibility 11 | * it has simple responsibility gets input [T] returns a [ValidationResult] 12 | * @param constrainBlock checks input and validates it 13 | * then returns a [ValidationResult] to corresponding to 14 | * passed input parameter 15 | */ 16 | open class BaseRule constructor( 17 | protected val constrainBlock:suspend BaseRule< T,R>.(input: T) -> R 18 | ) : Rule { 19 | override suspend fun validate(input: T): R = constrainBlock(input) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/rule/CompositeRule.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.rule 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.result.ValidationResult 5 | import ir.amirab.validator.reason.CompositeReason 6 | import ir.amirab.validator.result.ObjectValidationResult 7 | import kotlin.reflect.KProperty1 8 | 9 | 10 | /** 11 | * it contains two types of rules [DependentRule] , [BaseRule] 12 | * on validation it hydrates [DependentRule] types with [C] object 13 | * then create an [ObjectValidationResult] 14 | */ 15 | class CompositeRule : Rule> { 16 | private val rules: MutableList, Rule>> = 17 | mutableListOf() 18 | 19 | inner class Scope { 20 | @Suppress("UNCHECKED_CAST") 21 | private fun add(ruleWithKey: Pair, Rule>) { 22 | rules.add(ruleWithKey as Pair, Rule>) 23 | } 24 | 25 | @JvmName("unaryPlusCTCTValidationResult") 26 | @Suppress("UNCHECKED_CAST") 27 | private fun add(ruleWithKey: Pair, DependentRule>) { 28 | rules.add(ruleWithKey as Pair, Rule>) 29 | } 30 | 31 | 32 | infix fun KProperty1.mustBe(rule: Rule) { 33 | add(this to rule) 34 | } 35 | infix fun KProperty1.mustBe(rule: DependentRule) { 36 | add(this to rule) 37 | } 38 | } 39 | 40 | 41 | override suspend fun validate(input: C): ObjectValidationResult { 42 | val results = rules.associate { (prop, rule) -> 43 | // this is object that have multiple rules applied to each of its properties 44 | val receiver = input 45 | when (rule) { 46 | // dependent rules go here 47 | // we pass whole object model to this rule as input parameter 48 | // when we pass DependentInput to DependentRule it can extract 49 | // other properties by passed object receiver 50 | is DependentRule<*, *, *> -> { 51 | prop.name to rule.validate( 52 | DependentInput( 53 | receiver = receiver, 54 | prop = prop, 55 | ) 56 | ) 57 | } 58 | // base rules go through here 59 | // they are just simple rules that depend on nothing 60 | // only give input to validator 61 | else -> { 62 | prop.name to rule.validate(prop.get(receiver)) 63 | } 64 | } 65 | } 66 | val isValid = results.all { it.value.isValid } 67 | val reason = if (!isValid) { 68 | CompositeReason( 69 | results 70 | .filter { (_, result) -> result.reason != null } 71 | .mapValues { (_, result) -> result.reason!! } 72 | ) 73 | } else null 74 | return ObjectValidationResult(isValid, reason) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/rule/DependentInput.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.rule 2 | 3 | import kotlin.reflect.KProperty1 4 | 5 | data class DependentInput( 6 | val receiver: C, 7 | val prop: (C)-> T, 8 | ) { 9 | val actualInput get():T = prop(receiver) 10 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/rule/DependentRule.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.rule 2 | 3 | import ir.amirab.validator.Rule 4 | import ir.amirab.validator.result.ValidationResult 5 | 6 | /** 7 | * it means that input of these rules depends on another property 8 | */ 9 | class DependentRule( 10 | val constraintBlock: suspend DependentRule.(DependentInput) -> R 11 | ) : Rule, R> { 12 | override suspend fun validate(input: DependentInput): R=constraintBlock(input) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/rule_builders.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator 2 | 3 | import ir.amirab.validator.reason.Reason 4 | import ir.amirab.validator.reason.SingleReason 5 | import ir.amirab.validator.result.ValidationResult 6 | import ir.amirab.validator.rule.BaseRule 7 | import ir.amirab.validator.rule.CompositeRule 8 | import ir.amirab.validator.rule.DependentInput 9 | import ir.amirab.validator.rule.DependentRule 10 | 11 | /** 12 | * create [BaseRule] with simple [ValidationResult] 13 | */ 14 | @JvmName("baseRuleWithValidationResult") 15 | fun rule(constrainBlock: suspend BaseRule.(input: T) -> ValidationResult): BaseRule { 16 | return BaseRule(constrainBlock) 17 | } 18 | 19 | /** 20 | * create [BaseRule] with custom (subclass of) [ValidationResult] 21 | */ 22 | @JvmName("baseRuleWithSubclassOfValidationResult") 23 | fun rule(constrainBlock: suspend BaseRule.(input: T) -> R): BaseRule { 24 | return BaseRule(constrainBlock) 25 | } 26 | /** 27 | * create [BaseRule] with custom (subclass of) [ValidationResult] 28 | */ 29 | @JvmName("baseRuleWithSubclassOfValidationResult") 30 | fun dependentRule(constrainBlock: suspend DependentRule.(input: DependentInput) -> R): DependentRule { 31 | return DependentRule(constrainBlock) 32 | } 33 | 34 | 35 | /** 36 | * create [CompositeRule] this is a combination of 37 | * multiple rules to apply on an object properties 38 | * they can depend on together with [rule] 39 | */ 40 | fun compositeRule(validateFn: CompositeRule.Scope.() -> Unit): CompositeRule { 41 | return CompositeRule().apply { 42 | Scope().apply(validateFn) 43 | } 44 | } 45 | 46 | private class ValidationResultImpl( 47 | override val isValid: Boolean, 48 | override val reason: Reason?, 49 | ) : ValidationResult 50 | 51 | 52 | fun Rule<*, ValidationResult>.result(isValid: Boolean, reason: Reason?): ValidationResult { 53 | return ValidationResultImpl(isValid = isValid, reason = reason) 54 | } 55 | fun Rule<*, ValidationResult>.thenValid(): ValidationResult { 56 | return result(isValid=true,reason=null) 57 | } 58 | 59 | fun Rule<*, ValidationResult>.because(reason: Reason): ValidationResult { 60 | return result(isValid = false, reason = reason) 61 | } 62 | 63 | fun Rule<*, ValidationResult>.because(rawReason: String) = 64 | because( 65 | reason = object : SingleReason { 66 | override val rawReason get() = rawReason 67 | } 68 | ) 69 | 70 | inline fun Rule<*, ValidationResult>.because(crossinline reasonBuilder: () -> String) = 71 | because(reason = object : SingleReason { 72 | override val rawReason get() = reasonBuilder() 73 | }) -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/translate/DefaultValidatedTranslationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.translate 2 | 3 | import ir.amirab.validator.reason.SingleReason 4 | import kotlin.reflect.KClass 5 | 6 | data class FromRawString(val string: String) : TranslateMethod 7 | fun interface FromCallable : TranslateMethod,(T)->String 8 | 9 | open class DefaultValidatedTranslationAdapter : DefaultValidatedTranslationBaseAdapter() { 10 | 11 | inline infix fun KClass.providedBy(noinline block: (T)-> String) = 12 | apply { 13 | extend(T::class, FromCallable(block)) 14 | } 15 | inline infix fun KClass.providedBy(string: String) = 16 | apply { 17 | extend(T::class, FromRawString(string)) 18 | } 19 | 20 | override fun retrieve(method: TranslateMethod, reason: SingleReason): String { 21 | return when (method) { 22 | is FromRawString -> return method.string 23 | is FromCallable<*> -> { 24 | // this is SingleReason 25 | @Suppress("UNCHECKED_CAST") 26 | (method as FromCallable).invoke(reason) 27 | } 28 | else -> { 29 | super.retrieve(method, reason) 30 | } 31 | } 32 | } 33 | } 34 | fun ValidatedTranslation.initWithDefault()= init(DefaultValidatedTranslationAdapter()) 35 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/translate/DefaultValidatedTranslationAdapterBase.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.translate 2 | 3 | import ir.amirab.validator.reason.SingleReason 4 | import kotlin.reflect.KClass 5 | 6 | interface TranslateMethod 7 | open class DefaultValidatedTranslationBaseAdapter : ValidatorTranslationAdapter { 8 | protected val installedTranslations = mutableMapOf, TranslateMethod>() 9 | 10 | fun extend(clazz: KClass, translateMethod: TranslateMethod) { 11 | installedTranslations[clazz] = translateMethod 12 | } 13 | 14 | 15 | protected open fun retrieve(method: TranslateMethod, reason: SingleReason): String { 16 | throw NotImplementedError(method::class.qualifiedName + " is not supported by " + this::class.qualifiedName + " if you create new translate method please consider implement this class too") 17 | } 18 | 19 | 20 | override fun translate(reason: SingleReason): String { 21 | val foundTranslator = installedTranslations[reason::class] 22 | return if (foundTranslator != null) { 23 | retrieve(foundTranslator, reason) 24 | } else { 25 | reason.rawReason 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/translate/ValidatedTranslation.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.translate 2 | 3 | /** 4 | * before translating any [Reason] you have to initialize [adapter] 5 | */ 6 | object ValidatedTranslation { 7 | lateinit var adapter: ValidatorTranslationAdapter 8 | private set 9 | /** 10 | * after creating or using an existing adapter 11 | * you have to provide it (before any translation request) to initialize library 12 | */ 13 | fun init(adapter: T):T{ 14 | synchronized(this){ 15 | this.adapter=adapter 16 | } 17 | return adapter 18 | } 19 | } -------------------------------------------------------------------------------- /core/src/main/java/ir/amirab/validator/translate/ValidatedTranslationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.amirab.validator.translate 2 | 3 | import ir.amirab.validator.reason.CompositeReason 4 | import ir.amirab.validator.reason.Reason 5 | import ir.amirab.validator.reason.SingleReason 6 | import ir.amirab.validator.reason.flattenToSingleReason 7 | import ir.amirab.validator.translate.ValidatedTranslation.adapter 8 | 9 | /** 10 | * use for simple inline reasons 11 | */ 12 | fun Reason.translate(): List { 13 | return flattenToSingleReason().map { 14 | adapter.translate(it) 15 | } 16 | } 17 | 18 | /** 19 | * translate a reason and join their result with a separator 20 | */ 21 | fun Reason.translateRawString(separator:String=" ")=translate().joinToString(separator) 22 | 23 | /** 24 | * use for composite reasons 25 | */ 26 | fun CompositeReason<*>.translate(): Map> { 27 | return map.mapValues { 28 | it.value.translate() 29 | } 30 | } 31 | 32 | /** 33 | * @return translate by deep checking for possible composite rules 34 | */ 35 | fun CompositeReason<*>.translateDeepComposite(): Map { 36 | return map.mapValues { 37 | when (val r = it.value) { 38 | is CompositeReason<*> -> { 39 | r.translateDeepComposite() 40 | } 41 | else -> { 42 | r.translate() 43 | } 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * this api provide translation mapping 50 | * if you want custom translation you can provide your own 51 | * by implementing this interface 52 | */ 53 | interface ValidatorTranslationAdapter { 54 | /** 55 | * this method gives a single reason 56 | * that have 57 | * - a key for translation 58 | * - some optional parameters 59 | */ 60 | fun translate(reason: SingleReason): String 61 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | coroutines = "1.5.2" 3 | kotlin = "1.6.10" 4 | 5 | [libraries] 6 | 7 | android-pluginGradle = "com.android.tools.build:gradle:7.0.4" 8 | 9 | junit = "junit:junit:4.13.2" 10 | 11 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } 12 | kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 13 | kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 14 | kotlin-pluginGradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 15 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 16 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir1376/kotlin-validator/6769bbe93494c7a557933838db662abc2ba28285/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "validator" 2 | 3 | enableFeaturePreview("VERSION_CATALOGS") 4 | 5 | include(":core") 6 | include(":android") -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------