Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide Built-in Generic Validables #19

Merged
merged 8 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions validable/src/main/java/tech/devscast/validable/CardSchemeValidable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package tech.devscast.validable

import java.util.regex.Pattern

/**
* Validates that a card number belongs to a specified scheme.
*/
class CardSchemeValidable(scheme: CardScheme, message: String = "") : BaseValidable(
validator = { value ->
scheme.patterns.any { pattern -> Pattern.matches(pattern, value) }
},
errorFor = { _ -> message.ifBlank { "Unsupported card type or invalid card number." } }
)

/**
* see https://en.wikipedia.org/wiki/Payment_card_number
* see https://www.regular-expressions.info/creditcard.html
*/
open class CardScheme(val patterns: List<String>) {

companion object {
fun merge(vararg schemes: CardScheme): CardScheme {
return CardScheme(schemes.flatMap { scheme -> scheme.patterns })
}
}

/**
* American Express card numbers start with 34 or 37 and have 15 digits.
*/
data object Amex : CardScheme(
listOf("/^3[47][0-9]{13}\$/")
)

/**
* China UnionPay cards start with 62 and have between 16 and 19 digits.
* Please note that these cards do not follow Luhn Algorithm as a checksum.
*/
data object ChinaUnionPay : CardScheme(
listOf("/^62[0-9]{14,17}\$/")
)

/**
* Diners Club card numbers begin with 300 through 305, 36 or 38. All have 14 digits.
* There are Diners Club cards that begin with 5 and have 16 digits.
* These are a joint venture between Diners Club and MasterCard, and should be processed like a MasterCard.
*/
data object Diners : CardScheme(
listOf("/^3(?:0[0-5]|[68][0-9])[0-9]{11}\$/")
)

/**
* Discover card numbers begin with 6011, 622126 through 622925, 644 through 649 or 65.
* All have 16 digits.
*/
data object Discover : CardScheme(
listOf(
"/^6011[0-9]{12}$/",
"/^64[4-9][0-9]{13}$/",
"/^65[0-9]{14}$/",
"/^622(12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|91[0-9]|92[0-5])[0-9]{10}$/"
)
)

/**
* InstaPayment cards begin with 637 through 639 and have 16 digits.
*/
data object InstaPayment : CardScheme(
listOf("/^63[7-9][0-9]{13}\$/")
)

/**
* JCB cards beginning with 2131 or 1800 have 15 digits.
* JCB cards beginning with 35 have 16 digits.
*/
data object Jcb : CardScheme(
listOf("/^(?:2131|1800|35[0-9]{3})[0-9]{11}\$/")
)

/**
* Laser cards begin with either 6304, 6706, 6709 or 6771 and have between 16 and 19 digits.
*/
data object Laser : CardScheme(
listOf("/^(6304|670[69]|6771)[0-9]{12,15}\$/")
)

/**
* Maestro international cards begin with 675900..675999 and have between 12 and 19 digits.
* Maestro UK cards begin with either 500000..509999 or 560000..699999 and have between 12 and 19 digits.
*/
data object Maestro : CardScheme(
listOf(
"/^(6759[0-9]{2})[0-9]{6,13}$/",
"/^(50[0-9]{4})[0-9]{6,13}$/",
"/^5[6-9][0-9]{10,17}$/",
"/^6[0-9]{11,18}$/"
)
)

/**
* All MasterCard numbers start with the numbers 51 through 55. All have 16 digits.
* October 2016 MasterCard numbers can also start with 222100 through 272099.
*/
data object MasterCard : CardScheme(
listOf(
"/^5[1-5][0-9]{14}$/",
"/^2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12})$/"
)
)

/**
* Payment system MIR numbers start with 220, then 1 digit from 0 to 4, then between 12 and 15 digits
*/
data object Mir : CardScheme(
listOf("/^220[0-4][0-9]{12,15}\$/")
)

/**
* All UATP card numbers start with a 1 and have a length of 15 digits.
*/
data object Uatp : CardScheme(
listOf("/^1[0-9]{14}\$/")
)

/**
* All Visa card numbers start with a 4 and have a length of 13, 16, or 19 digits.
*/
data object Visa : CardScheme(
listOf("/^4([0-9]{12}|[0-9]{15}|[0-9]{18})\$/")
)
}
19 changes: 9 additions & 10 deletions validable/src/main/java/tech/devscast/validable/EmailValidable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package tech.devscast.validable

import java.util.regex.Pattern

class EmailValidable : BaseValidable(validator = ::isEmailValid, errorFor = ::emailValidationError)

private fun isEmailValid(email: String): Boolean {
return Pattern.matches("^[A-Za-z](.*)([@]{1})(.+)(\\.)(.+)", email)
}

/**
* Returns an error to be displayed or null if no error was found
* Validates that a value is a valid email address.
*/
private fun emailValidationError(email: String): String {
return "Invalid email: $email"
}
class EmailValidable(message: String = "") : BaseValidable(
validator = { value ->
Pattern.matches("^[A-Za-z](.*)([@]{1})(.+)(\\.)(.+)", value)
},
errorFor = { value ->
message.ifBlank { "$value is not a valid email address." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tech.devscast.validable

/**
* Validates that a value is equal to another value,
* To force that a value is not equal, see [NotEqualToValidable].
*/
class EqualToValidable(comparedValue: String, message: String = "") : BaseValidable(
validator = { value -> value === comparedValue },
errorFor = { value ->
message.ifBlank { "$value should be equal to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tech.devscast.validable

/**
* Validates that a value is greater than or equal to another value.
* To force that a value is greater than another value, see [GreaterThanValidable].
*/
class GreaterThanOrEqualValidable(comparedValue: Int, message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() >= comparedValue
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be greater than or equal to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.devscast.validable

/**
* Validates that a value is greater than another value
* To force that a value is greater than or equal to another value, see [GreaterThanOrEqualValidable].
* To force a value is less than another value, see [LessThanValidable].
*/
class GreaterThanValidable(comparedValue: Int, message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() > comparedValue
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be equal to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tech.devscast.validable

import java.util.regex.Pattern

private const val HOSTNAME_PATTERN =
"^(?!-)(?!.*\\d+\\.\\d+\\.\\d+\\.\\d+)(?!.*\\.example\$)(?!.*\\.invalid\$)(?!.*\\.localhost\$)(?!.*\\.test\$)[A-Za-z0-9-]{1,63}(?<!-)(\\.[A-Za-z0-9-]{1,63})*\$"

/**
* Validates that the given value is a valid host name
* To forces that value is an IP address see [IpValidable]
*
* The following top-level domains (TLD) are reserved according to RFC 2606
* and that's why hostnames containing them are not considered valid: .example, .invalid, .localhost, and .test.
*/
class HostnameValidable(message: String = "") : BaseValidable(
validator = { value -> Pattern.matches(HOSTNAME_PATTERN, value) },
errorFor = { value -> message.ifBlank { "$value is not a valid hostname" } }
)
14 changes: 14 additions & 0 deletions validable/src/main/java/tech/devscast/validable/IpValidable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tech.devscast.validable

import java.util.regex.Pattern

private const val IP_V4_PATTERN = "^(?:(?: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]?)\$"

/**
* Validates that a value is a valid IP address
* TODO: add support for IP v6
*/
class IpValidable(message: String = "") : BaseValidable(
validator = { value -> Pattern.matches(IP_V4_PATTERN, value) },
errorFor = { value -> message.ifBlank { "$value is not a valid IP address." } }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tech.devscast.validable

/**
* Validates that a value is less than or equal to another value.
* To force that a value is less than another value, see [LessThanValidable].
*/
class LessThanOrEqualValidable(comparedValue: Int, message: String = "") : BaseValidable(
validator = { value ->
try {
value.toInt() <= comparedValue
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be less than or equal to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.devscast.validable

/**
* Validates that a value is less than another value.
* To force that a value is less than or equal to another value, see [LessThanOrEqualValidable].
* To force a value is greater than another value, see [GreaterThanValidable].
*/
class LessThanValidable(comparedValue: Int, message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() < comparedValue
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be less than to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tech.devscast.validable

/**
* Validates that a value is a negative number or equal to zero.
* If you don't want to allow zero as value, use [NegativeValidable] instead.
*/
class NegativeOrZeroValidable(message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() <= 0.0
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be a negative number or zero." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.devscast.validable

/**
* Validates that a value is a negative number.
* Zero is neither positive nor negative,
* so you must use [NegativeOrZeroValidable] if you want to allow zero as value.
*/
class NegativeValidable(message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() < 0.0
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be a negative number." }
}
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package tech.devscast.validable

class NotEmptyValidable : BaseValidable(validator = ::isFieldValid, errorFor = ::fieldValidationError)

private fun isFieldValid(text: String): Boolean {
return text.isNotBlank()
}

private fun fieldValidationError(text: String): String {
return "This field is required"
}
/**
* Validates that a value is not blank - meaning not equal to a blank string.
*/
class NotEmptyValidable(message: String = "") : BaseValidable(
validator = { value -> value.isNotBlank() },
errorFor = { _ ->
message.ifBlank { "This field is required" }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tech.devscast.validable

/**
* Validates that a value is not equal to another value,
* To force that a value is equal, see [EqualToValidable].
*/
class NotEqualToValidable(comparedValue: String, message: String = "") : BaseValidable(
validator = { value -> value !== comparedValue },
errorFor = { value ->
message.ifBlank { "$value should not be equal to $comparedValue." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tech.devscast.validable

/**
* Validates that a value is a positive number or equal to zero.
* If you don't want to allow zero as value, use [PositiveValidable] instead.
*/
class PositiveOrZeroValidable(message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() >= 0.0
} catch (e: Exception) {
false
}
},
errorFor = { text ->
message.ifBlank { "$text should be a positive number or zero." }
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.devscast.validable

/**
* Validates that a value is a positive number.
* Zero is neither positive nor negative,
* so you must use [PositiveOrZeroValidable] if you want to allow zero as value.
*/
class PositiveValidable(message: String = "") : BaseValidable(
validator = { value ->
try {
value.toFloat() > 0.0
} catch (e: Exception) {
false
}
},
errorFor = { value ->
message.ifBlank { "$value should be a positive number." }
}
)
Loading
Loading