Added Validators and User's fields

Updated Detekt rule-set in order to match Core's config ( allowing uppercase functions )
Affected: none

MAILAND-719
This commit is contained in:
Davide Farella 2020-06-24 11:13:37 +02:00 committed by Zorica Stojchevska
parent 01101d8d2a
commit de9ab3b028
8 changed files with 375 additions and 35 deletions

View File

@ -375,7 +375,7 @@ naming:
# minimumFunctionNameLength: 3
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
functionPattern: '^([a-zA-Z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
ignoreOverridden: true
FunctionParameterNaming:

View File

@ -23,8 +23,21 @@ import ch.protonmail.android.domain.entity.Validable.Result.Success
/**
* An entity that can be validated
*
* It requires a [Validator]
* @see invoke - without args - extension on [Validator]
* or use standard override
* ```
data class MyValidable(args...) : Validable {
override val validator = { validable: MyValidable ->
// validation logic
}
}
* ```
*
* @see Validated annotation
* One entity can call [requireValid] in its `init` block, for ensure that that given entity is always valid
* NOTE: the `init` block must be declared after the override of [validator]
*
* @author Davide Farella
*/
@ -45,9 +58,16 @@ interface Validable {
object Success : Result()
object Error : Result()
companion object {
operator fun invoke(isSuccess: Boolean) =
if (isSuccess) Result.Success else Result.Error
}
}
}
// region CHECKS
/**
* @return `true` is validation is successful
*/
@ -71,37 +91,18 @@ fun <V : Validable> V.validate() = (validator as Validator<V>).validate(this)
fun <V : Validable> V.requireValid() = (validator as Validator<V>).requireValid(this)
/**
* Returns an implementation of [Validable] that provide the receiver as its [Validator]
* Example:
* ```
val MyValidator = { validable: MyValidable ->
// validation logic that return a Validable.Result
}
data class MyValidable(args..) : Validable by MyValidator()
* ```
* @return [Validable] created in this lambda which is marked as [Validated] if validation is successful,
* otherwise `null`
* Example: `` validOrNull { MyValidable("hello" } ``
*/
operator fun <V : Validable> Validator<V>.invoke() = object : Validable {
override val validator: Validator<*>
get() = this@invoke
}
/**
* Entity that validates an [Validable]
*/
typealias Validator<V> = (V) -> Validable.Result
private fun <V : Validable> Validator<V>.isValid(validable: V) =
validate(validable) is Validable.Result.Success
private fun <V : Validable> Validator<V>.validate(validable: V): Validable.Result =
invoke(validable)
private fun <V : Validable> Validator<V>.requireValid(validable: V) = apply {
validate(validable).also { result ->
if (result is Validable.Result.Error) throw ValidationException(validable)
inline fun <V : Validable> validOrNull(block: () -> V): V? =
try {
block()
} catch (e: ValidationException) {
null
}
}
// endregion
/**
* An exception thrown from [Validable.requireValid] in case the validation fails
@ -109,3 +110,25 @@ private fun <V : Validable> Validator<V>.requireValid(validable: V) = apply {
*/
class ValidationException(validable: Validable) :
Exception("Validable did not validate successfully: $validable")
/**
* Represents an entity that is validated on its initialisation. `` init { requireValid() } ``
* @throws ValidationException if validation fails.
*
* This can be used on an entity that doesn't directly inherit from [Validable], but all its fields are marked as
* [Validated]
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR)
annotation class Validated
private fun <V : Validable> Validator<V>.isValid(validable: V) =
validate(validable) is Success
private fun <V : Validable> Validator<V>.validate(validable: V): Validable.Result =
invoke(validable)
private fun <V : Validable> Validator<V>.requireValid(validable: V) = apply {
validate(validable).also { result ->
if (result is Error) throw ValidationException(validable)
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity
/**
* Entity that validates an [Validable]
* @author Davide Farella
*/
typealias Validator<V> = (V) -> Validable.Result
/**
* Creates a [Validator] using a simple lambda that return `true` if validation is successful
* ```
class PositiveNumber(val number: Int) : Validable by Validator<PositiveNumber>({ number >= 0 })
* ```
*/
fun <V> Validator(successBlock: V.() -> Boolean) = { v: V ->
Validable.Result(successBlock(v))
}.wrap()
/**
* Returns an implementation of [Validable] that provide the receiver as its [Validator]
* Example:
* ```
val MyValidator = { validable: MyValidable ->
// validation logic that return a Validable.Result
}
data class MyValidable(args..) : Validable by MyValidator()
* ```
*/
operator fun <V : Validable> Validator<V>.invoke() = object : Validable {
override val validator: Validator<*>
get() = this@invoke
}
// Mirror of `invoke`. Used internally for readability purpose
private fun <V : Validable> Validator<V>.wrap() = invoke<V>()
/**
* [Validator] that accepts only strings that are not blank
*/
fun NotBlankStringValidator(field: String) = { _: Any ->
Validable.Result(field.isNotBlank())
}.wrap()
/**
* [Validator] that validate using a [Regex]
* @param field [String] field to validate
*/
fun RegexValidator(field: String, regex: Regex) = { _: Any ->
Validable.Result(regex.matches(field))
}.wrap()
/**
* [Validator] that validate using a [Regex]
* @param field [String] field to validate
*/
fun RegexValidator(field: String, regex: String) =
RegexValidator(field, regex.toRegex())

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity
/*
* A set of typed representation of business models that can be described as 'field'.
* e.g. Email, Username, Password
*
* Thanks to this strongly typed paradigm we can about confusion about 'userId' to 'messageId' and similar cases.
*
* Even more, this can allow us to run simple validation on object instantiation, so we can ensure that an 'Email'
* entity respect the proper format since its born to its death
*/
// SORTED FIRSTLY ALPHABETICALLY AND THEN LOGICALLY WHEN 2 OR MORE ARE SO STRONGLY CONNECTED THAT THEY REQUIRES TO
// STAY CLOSE
// IF WE'RE BE GOIN TO APPROACH A BIG SIZE FOR THIS FILE, WE MUST CONSIDER SPLITTING INTO DIFFERENT FILES TO BE PLACED
// INTO A 'field' PACKAGE
/**
* Entity representing an email address
* Implements [Validable] by [RegexValidator]
*/
@Validated
data class EmailAddress(val s: String) : Validable by RegexValidator(s, VALIDATION_REGEX) {
init { requireValid() }
private companion object {
@Suppress("MaxLineLength") // Nobody can read it anyway ¯\_(ツ)_/¯
const val VALIDATION_REGEX = """(?:[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""
}
}
/**
* Entity representing an id
* Implements [Validable] by [NotBlankStringValidator]
*/
@Validated
data class Id(val s: String) : Validable by NotBlankStringValidator(s) {
init { requireValid() }
}
/**
* Entity representing a generic name
* Implements [Validable] by [NotBlankStringValidator]
*/
data class Name(val s: String) : Validable by NotBlankStringValidator(s) {
init { requireValid() }
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.EmailAddress
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Name
import ch.protonmail.android.domain.entity.Validated
/**
* Representation of a server user.
* @author Davide Farella
*/
@Validated
data class User( // TODO: consider calling UserInfo or simial
val id: Id,
val name: Name,
val email: EmailAddress
)

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity
import assert4k.*
import kotlin.test.Test
/**
* Test suite for [EmailAddress]
* @author Davide Farella
*/
internal class FieldsTest {
@Test
fun `verify happy paths`() {
listOf(
"somebody@protonmail.com",
"mail@4cafe.diostu",
"davide@proton.me",
"hello@email.it"
).map { EmailAddress(it) }
}
@Test
fun `verify failing paths`() {
assert that fails<ValidationException> {
EmailAddress("@hello.com")
}
assert that fails<ValidationException> {
EmailAddress("hello.com")
}
assert that fails<ValidationException> {
EmailAddress("hello@com")
}
assert that fails<ValidationException> {
EmailAddress("hello@.com")
}
assert that fails<ValidationException> {
EmailAddress("hello@-.com")
}
}
}

View File

@ -48,11 +48,24 @@ internal class ValidableTest {
EmailTestValidable("invalid").requireValid()
} with "Validable did not validate successfully: EmailTestValidable(s=invalid)"
}
@Test
fun `validOrNull return Validable if success`() {
assert that validOrNull { ValidatedEmail("hello@mail.com") } equals ValidatedEmail("hello@mail.com")
}
@Test
fun `validOrNull return null if failure`() {
assert that validOrNull { ValidatedEmail("hello") } `is` `null`
}
}
private data class EmailTestValidable(val s: String) : Validable by EmailTestValidator()
private val EmailTestValidator = { email: EmailTestValidable ->
if ("@" in email.s && "." in email.s) Validable.Result.Success
else Validable.Result.Error
@Validated
private data class ValidatedEmail(val s: String) : Validable by EmailTestValidator(s) {
init { requireValid() }
}
private data class EmailTestValidable(val s: String) : Validable by EmailTestValidator(s)
// Regex is representative only for this test case and not intended to properly validate an email address
private fun EmailTestValidator(email: String) = RegexValidator(email, ".+@.+\\..+")

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity
import assert4k.*
import kotlin.test.Test
/**
* Test suite for built-in [Validator]s
* @author Davide Farella
*/
internal class ValidatorsTest {
@Test
fun `custom Validator works correctly`() {
class PositiveNumber(val number: Int) : Validable by Validator<PositiveNumber>({ number >= 0 })
assert that PositiveNumber(10).isValid()
assert that ! PositiveNumber(-10).isValid()
}
@Test
fun `NotBlackStringValidator works correctly`() {
class SomeText(string: String) : Validable by NotBlankStringValidator(string) {
init { requireValid() }
}
SomeText("hello")
assert that fails<ValidationException> { SomeText("") }
assert that fails<ValidationException> { SomeText(" ") }
}
@Test
fun `RegexValidator works correctly`() {
// Regex is representative only for this test case and not intended to properly validate an email address
class EmailRegexValidable(string: String) : Validable by RegexValidator(string, "\\w+@[a-z]+\\.[a-z]+")
assert that EmailRegexValidable("somebody@protonmail.com").isValid()
assert that ! EmailRegexValidable("somebody@123.456").isValid()
}
@Test
fun `RegexValidator alt works correctly`() {
// Regex is representative only for this test case and not intended to properly validate an email address
class EmailRegexValidable(string: String) : Validable by RegexValidator(string, "\\w+@[a-z]+\\.[a-z]+") {
init { requireValid() }
}
EmailRegexValidable("somebody@protonmail.com")
assert that fails<ValidationException> { EmailRegexValidable("somebody@123.456") }
}
}