aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Nastich <nastich@users.noreply.github.com>2018-09-19 13:57:53 -0400
committerGitHub <noreply@github.com>2018-09-19 13:57:53 -0400
commit1b979318d85ea6035084253596cf076151cef309 (patch)
treed5f64a3893b2581807020a88c8c2b28f277fbd53
parent60ad2abd17a50c8bd73bfe75084984b4de27bd79 (diff)
downloaddriver-core-1b979318d85ea6035084253596cf076151cef309.tar.gz
driver-core-1b979318d85ea6035084253596cf076151cef309.tar.bz2
driver-core-1b979318d85ea6035084253596cf076151cef309.zip
Improve PhoneNumber (#222)
* Add support for extensions * Add PathMatcher and allow parsing JSON from string * Add a number of convenience methods which are to be used instead of `toString`
-rw-r--r--src/main/scala/xyz/driver/core/domain.scala45
-rw-r--r--src/main/scala/xyz/driver/core/json.scala16
-rw-r--r--src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala12
-rw-r--r--src/test/scala/xyz/driver/core/JsonTest.scala15
-rw-r--r--src/test/scala/xyz/driver/core/PhoneNumberTest.scala38
5 files changed, 116 insertions, 10 deletions
diff --git a/src/main/scala/xyz/driver/core/domain.scala b/src/main/scala/xyz/driver/core/domain.scala
index 59bed54..f3b8337 100644
--- a/src/main/scala/xyz/driver/core/domain.scala
+++ b/src/main/scala/xyz/driver/core/domain.scala
@@ -1,13 +1,20 @@
package xyz.driver.core
import com.google.i18n.phonenumbers.PhoneNumberUtil
+import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat
import scalaz.Equal
import scalaz.std.string._
import scalaz.syntax.equal._
+import scala.util.Try
+import scala.util.control.NonFatal
+
object domain {
final case class Email(username: String, domain: String) {
+
+ def value: String = toString
+
override def toString: String = username + "@" + domain
}
@@ -23,18 +30,44 @@ object domain {
}
}
- final case class PhoneNumber(countryCode: String = "1", number: String) {
- override def toString: String = s"+$countryCode $number"
+ final case class PhoneNumber(countryCode: String, number: String, extension: Option[String] = None) {
+
+ def hasExtension: Boolean = extension.isDefined
+
+ /** This is a more human-friendly alias for #toE164String() */
+ def toCompactString: String = s"+$countryCode$number${extension.fold("")(";ext=" + _)}"
+
+ /** Outputs the phone number in a E.164-compliant way, e.g. +14151234567 */
+ def toE164String: String = toCompactString
+
+ /**
+ * Outputs the phone number in a "readable" way, e.g. "+1 415-123-45-67 ext. 1234"
+ * @throws IllegalStateException if the contents of this object is not a valid phone number
+ */
+ @throws[IllegalStateException]
+ def toHumanReadableString: String =
+ try {
+ val phoneNumber = PhoneNumber.phoneUtil.parse(toE164String, "US")
+ PhoneNumber.phoneUtil.format(phoneNumber, PhoneNumberFormat.INTERNATIONAL)
+ } catch {
+ case NonFatal(e) => throw new IllegalStateException(s"$toString is not a valid number", e)
+ }
+
+ override def toString: String = s"+$countryCode $number${extension.fold("")(" ext. " + _)}"
}
object PhoneNumber {
- private val phoneUtil = PhoneNumberUtil.getInstance()
+ private[PhoneNumber] val phoneUtil = PhoneNumberUtil.getInstance()
def parse(phoneNumber: String): Option[PhoneNumber] = {
- val validated =
- util.Try(phoneUtil.parseAndKeepRawInput(phoneNumber, "US")).toOption.filter(phoneUtil.isValidNumber)
- validated.map(pn => PhoneNumber(pn.getCountryCode.toString, pn.getNationalNumber.toString))
+ val validated = Try(phoneUtil.parseAndKeepRawInput(phoneNumber, "US")).toOption.filter(phoneUtil.isValidNumber)
+ validated.map { pn =>
+ PhoneNumber(
+ pn.getCountryCode.toString,
+ pn.getNationalNumber.toString,
+ Option(pn.getExtension).filter(_.nonEmpty))
+ }
}
}
}
diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala
index 4daf127..edc2347 100644
--- a/src/main/scala/xyz/driver/core/json.scala
+++ b/src/main/scala/xyz/driver/core/json.scala
@@ -181,10 +181,18 @@ object json extends PathMatchers with Unmarshallers {
}
implicit object phoneNumberFormat extends RootJsonFormat[PhoneNumber] {
- private val basicFormat = jsonFormat2(PhoneNumber.apply)
- override def write(obj: PhoneNumber): JsValue = basicFormat.write(obj)
- override def read(json: JsValue): PhoneNumber = {
- PhoneNumber.parse(basicFormat.read(json).toString).getOrElse(deserializationError("Invalid phone number"))
+
+ private val basicFormat = jsonFormat3(PhoneNumber.apply)
+
+ def write(obj: PhoneNumber): JsValue = basicFormat.write(obj)
+
+ def read(json: JsValue): PhoneNumber = {
+ val maybePhone = json match {
+ case JsString(number) => PhoneNumber.parse(number)
+ case obj: JsObject => PhoneNumber.parse(basicFormat.read(obj).toString)
+ case _ => None
+ }
+ maybePhone.getOrElse(deserializationError("Invalid phone number"))
}
}
diff --git a/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala b/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala
index 183ad9a..218c9ae 100644
--- a/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala
+++ b/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala
@@ -10,6 +10,7 @@ import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched}
import akka.http.scaladsl.server.{PathMatcher, PathMatcher1, PathMatchers => AkkaPathMatchers}
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.refineV
+import xyz.driver.core.domain.PhoneNumber
import xyz.driver.core.time.Time
import scala.util.control.NonFatal
@@ -70,4 +71,15 @@ trait PathMatchers {
Some(Revision[T](string))
}
+ def PhoneInPath: PathMatcher1[PhoneNumber] = new PathMatcher1[PhoneNumber] {
+ def apply(path: Path) = path match {
+ case Path.Segment(segment, tail) =>
+ PhoneNumber
+ .parse(segment)
+ .map(parsed => Matched(tail, Tuple1(parsed)))
+ .getOrElse(Unmatched)
+ case _ => Unmatched
+ }
+ }
+
}
diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala
index 2aa3572..fd693f9 100644
--- a/src/test/scala/xyz/driver/core/JsonTest.scala
+++ b/src/test/scala/xyz/driver/core/JsonTest.scala
@@ -247,6 +247,21 @@ class JsonTest extends WordSpec with Matchers with Inspectors {
json.phoneNumberFormat.read(phoneJson)
}.getMessage shouldBe "Invalid phone number"
}
+
+ "parse phone number from string" in {
+ JsString("+14243039608").convertTo[PhoneNumber] shouldBe PhoneNumber("1", "4243039608")
+ }
+ }
+
+ "Path matcher for PhoneNumber" should {
+ "read valid phone number" in {
+ val string = "+14243039608x23"
+ val phone = PhoneNumber("1", "4243039608", Some("23"))
+
+ val matcher = PathMatcher("foo") / PhoneInPath
+
+ matcher(Uri.Path("foo") / string / "bar") shouldBe Matched(Uri.Path./("bar"), Tuple1(phone))
+ }
}
"Json format for ADT mappings" should {
diff --git a/src/test/scala/xyz/driver/core/PhoneNumberTest.scala b/src/test/scala/xyz/driver/core/PhoneNumberTest.scala
index 384c7be..729302b 100644
--- a/src/test/scala/xyz/driver/core/PhoneNumberTest.scala
+++ b/src/test/scala/xyz/driver/core/PhoneNumberTest.scala
@@ -44,6 +44,21 @@ class PhoneNumberTest extends FlatSpec with Matchers {
PhoneNumber.parse("+86 134 52 52 2256") shouldBe Some(PhoneNumber("86", "13452522256"))
}
+ it should "parse numbers with extensions in different formats" in {
+ // format: off
+ val numbers = List(
+ "+1 800 525 22 25 x23",
+ "+18005252225 ext. 23",
+ "+18005252225,23"
+ )
+ // format: on
+
+ val parsed = numbers.flatMap(PhoneNumber.parse)
+
+ parsed should have size numbers.size
+ parsed should contain only PhoneNumber("1", "8005252225", Some("23"))
+ }
+
it should "return None on numbers that are shorter than the minimum number of digits for the country (i.e. US - 10, AR - 11)" in {
withClue("US and CN numbers are 10 digits - 9 digit (and shorter) numbers should not fit") {
// format: off
@@ -76,4 +91,27 @@ class PhoneNumberTest extends FlatSpec with Matchers {
List(PhoneNumber("45", "27452522"), PhoneNumber("86", "13452522256"))
}
+ "PhoneNumber.toCompactString/toE164String" should "produce phone number in international format without whitespaces" in {
+ PhoneNumber.parse("+1 800 5252225").get.toCompactString shouldBe "+18005252225"
+ PhoneNumber.parse("+1 800 5252225").get.toE164String shouldBe "+18005252225"
+
+ PhoneNumber.parse("+1 800 5252225 x23").get.toCompactString shouldBe "+18005252225;ext=23"
+ PhoneNumber.parse("+1 800 5252225 x23").get.toE164String shouldBe "+18005252225;ext=23"
+ }
+
+ "PhoneNumber.toHumanReadableString" should "produce nice readable result for different countries" in {
+ PhoneNumber.parse("+14154234567").get.toHumanReadableString shouldBe "+1 415-423-4567"
+ PhoneNumber.parse("+14154234567,23").get.toHumanReadableString shouldBe "+1 415-423-4567 ext. 23"
+
+ PhoneNumber.parse("+78005252225").get.toHumanReadableString shouldBe "+7 800 525-22-25"
+
+ PhoneNumber.parse("+41219437898").get.toHumanReadableString shouldBe "+41 21 943 78 98"
+ }
+
+ it should "throw an IllegalArgumentException if the PhoneNumber object is not parsable/valid" in {
+ intercept[IllegalStateException] {
+ PhoneNumber("+123", "1238123120938120938").toHumanReadableString
+ }.getMessage should include("+123 1238123120938120938 is not a valid number")
+ }
+
}