Current section

Files

Jump to
phony src phony.gleam
Raw

src/phony.gleam

import gleam/bool
import gleam/list
import gleam/option
import gleam/regexp
import gleam/string
import phony/metadata
pub type ValidationResult {
ValidationResult(country: Country, kind: PhoneNumberKind)
}
pub type Country {
Country(name: String, alpha2: String, flag: String, code: String)
}
pub type PhoneNumberKind {
Mobile
Landline
}
pub fn get_countries() {
metadata.by_alpha2
|> list.map(fn(metadata) {
let alpha2 = metadata.0
let #(name, flag, code, _, _, _, _) = metadata.1
Country(name, alpha2, flag, code)
})
}
pub fn validate(phone_number: String) -> Result(ValidationResult, Nil) {
use <- bool.guard(pre_validate(phone_number), Error(Nil))
case string.starts_with(phone_number, "+") {
True -> {
let phone_number = string.drop_start(phone_number, 1)
case string.split_once(phone_number, " ") {
Ok(#(code, phone_number)) ->
case string.length(code) <= 3 {
True -> internal_validate_by_code(phone_number, code)
False -> try_various_code_lengths(code <> phone_number)
}
Error(Nil) -> try_various_code_lengths(phone_number)
}
}
False -> walk_metadata(phone_number)
}
}
pub fn validate_by_country(
phone_number: String,
alpha2: String,
) -> Result(ValidationResult, Nil) {
use <- bool.guard(pre_validate(phone_number), Error(Nil))
case
metadata.by_alpha2
|> list.key_find(alpha2)
{
Ok(metadata) -> {
let #(
country,
flag,
code,
mobile_pattern,
landline_pattern,
possible_lengths,
leading,
) = metadata
let phone_number = case string.starts_with(phone_number, "+") {
True -> string.drop_start(phone_number, string.length(code) + 1)
False -> phone_number
}
let phone_number =
case string.starts_with(phone_number, "0") {
True -> string.drop_start(phone_number, 1)
False -> phone_number
}
|> string.replace(" ", "")
use <- bool.guard(
!list.contains(possible_lengths, string.length(phone_number)),
Error(Nil),
)
use <- matches_leading(phone_number, leading)
let assert Ok(pattern) = regexp.from_string(mobile_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, alpha2, flag, code), Mobile)
|> Ok
False -> {
let assert Ok(pattern) = regexp.from_string(landline_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, alpha2, flag, code), Landline)
|> Ok
False -> Error(Nil)
}
}
}
}
Error(Nil) -> Error(Nil)
}
}
pub fn validate_by_code(
phone_number: String,
code: String,
) -> Result(ValidationResult, Nil) {
use <- bool.guard(pre_validate(phone_number), Error(Nil))
internal_validate_by_code(phone_number, code)
}
fn internal_validate_by_code(phone_number: String, code: String) {
case
metadata.by_code
|> list.filter(fn(metadata) { metadata.0 == code })
{
[] -> Error(Nil)
filtered -> {
list.find_map(filtered, fn(metadata) {
let #(
country,
alpha2,
flag,
mobile_pattern,
landline_pattern,
possible_lengths,
leading,
) = metadata.1
let phone_number = case string.starts_with(phone_number, "+") {
True -> string.drop_start(phone_number, string.length(code) + 1)
False -> phone_number
}
let phone_number =
case string.starts_with(phone_number, "0") {
True -> string.drop_start(phone_number, 1)
False -> phone_number
}
|> string.replace(" ", "")
use <- bool.guard(
!list.contains(possible_lengths, string.length(phone_number)),
Error(Nil),
)
use <- matches_leading(phone_number, leading)
let assert Ok(pattern) = regexp.from_string(mobile_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, alpha2, flag, code), Mobile)
|> Ok
False -> {
let assert Ok(pattern) = regexp.from_string(landline_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, alpha2, flag, code), Landline)
|> Ok
False -> Error(Nil)
}
}
}
})
}
}
}
fn try_various_code_lengths(phone_number: String) {
let code = string.slice(phone_number, 0, 1)
let phone_number = string.drop_start(phone_number, 1)
case internal_validate_by_code(phone_number, code) {
Ok(result) -> Ok(result)
Error(Nil) -> {
let phone_number = code <> phone_number
let code = string.slice(phone_number, 0, 2)
let phone_number = string.drop_start(phone_number, 2)
case internal_validate_by_code(phone_number, code) {
Ok(result) -> Ok(result)
Error(Nil) -> {
let phone_number = code <> phone_number
let code = string.slice(phone_number, 0, 3)
let phone_number = string.drop_start(phone_number, 3)
internal_validate_by_code(phone_number, code)
}
}
}
}
}
fn walk_metadata(phone_number: String) {
metadata.by_alpha2
|> list.find_map(fn(metadata) {
let #(
country,
flag,
code,
mobile_pattern,
landline_pattern,
possible_lengths,
leading,
) = metadata.1
let phone_number = case string.starts_with(phone_number, "+") {
True -> string.drop_start(phone_number, string.length(code) + 1)
False -> phone_number
}
let phone_number =
case string.starts_with(phone_number, "0") {
True -> string.drop_start(phone_number, 1)
False -> phone_number
}
|> string.replace(" ", "")
use <- bool.guard(
!list.contains(possible_lengths, string.length(phone_number)),
Error(Nil),
)
use <- matches_leading(phone_number, leading)
let assert Ok(pattern) = regexp.from_string(mobile_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, metadata.0, flag, code), Mobile)
|> Ok
False -> {
let assert Ok(pattern) = regexp.from_string(landline_pattern)
case regexp.check(pattern, phone_number) {
True ->
ValidationResult(Country(country, metadata.0, flag, code), Landline)
|> Ok
False -> Error(Nil)
}
}
}
})
}
fn pre_validate(phone_number) -> Bool {
let assert Ok(pattern) = regexp.from_string("[A-Za-z]")
regexp.check(pattern, phone_number)
}
fn matches_leading(phone_number: String, leading: option.Option(String), rest) {
case leading {
option.Some(leading) -> {
let assert Ok(leading) = regexp.from_string("^" <> leading)
case regexp.check(leading, phone_number) {
True -> rest()
False -> Error(Nil)
}
}
option.None -> rest()
}
}