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:
parent
01101d8d2a
commit
de9ab3b028
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
|
@ -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() }
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, ".+@.+\\..+")
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue