From 7a793ffa068fda8f2146f84fa785328d928dba03 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 12 Sep 2018 15:56:41 -0700 Subject: Move core types into core-types project Note that xyz.driver.core.FutureExtensions was moved to xyz.driver.core.rest as it (only) contained logic that dealt with service exceptions, something that belongs into core-rest and must not be depended upon by core-types. This is a breaking change. --- src/main/scala/xyz/driver/core/core.scala | 124 ------------ src/main/scala/xyz/driver/core/date.scala | 109 ----------- src/main/scala/xyz/driver/core/domain.scala | 73 ------- src/main/scala/xyz/driver/core/rest/package.scala | 12 +- .../scala/xyz/driver/core/tagging/tagging.scala | 62 ------ src/main/scala/xyz/driver/core/time.scala | 209 --------------------- 6 files changed, 11 insertions(+), 578 deletions(-) delete mode 100644 src/main/scala/xyz/driver/core/core.scala delete mode 100644 src/main/scala/xyz/driver/core/date.scala delete mode 100644 src/main/scala/xyz/driver/core/domain.scala delete mode 100644 src/main/scala/xyz/driver/core/tagging/tagging.scala delete mode 100644 src/main/scala/xyz/driver/core/time.scala (limited to 'src/main/scala') diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala deleted file mode 100644 index 2ab4e88..0000000 --- a/src/main/scala/xyz/driver/core/core.scala +++ /dev/null @@ -1,124 +0,0 @@ -package xyz.driver - -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty -import scalaz.{Equal, Monad, OptionT} -import xyz.driver.core.rest.errors.ExternalServiceException -import xyz.driver.core.tagging.Tagged - -import scala.concurrent.{ExecutionContext, Future} - -// TODO: this package seems too complex, look at all the features we need! -import scala.language.{higherKinds, implicitConversions, reflectiveCalls} - -package object core { - - def make[T](v: => T)(f: T => Unit): T = { - val value = v - f(value) - value - } - - def using[R <: { def close() }, P](r: => R)(f: R => P): P = { - val resource = r - try { - f(resource) - } finally { - resource.close() - } - } - - type @@[+V, +Tag] = V with Tagged[V, Tag] - - implicit class OptionTExtensions[H[_]: Monad, T](optionTValue: OptionT[H, T]) { - - def returnUnit: H[Unit] = optionTValue.fold[Unit](_ => (), ()) - - def continueIgnoringNone: OptionT[H, Unit] = - optionTValue.map(_ => ()).orElse(OptionT.some[H, Unit](())) - - def subflatMap[B](f: T => Option[B]): OptionT[H, B] = - OptionT.optionT[H](implicitly[Monad[H]].map(optionTValue.run)(_.flatMap(f))) - } - - implicit class MonadicExtensions[H[_]: Monad, T](monadicValue: H[T]) { - private implicit val monadT = implicitly[Monad[H]] - - def returnUnit: H[Unit] = monadT(monadicValue)(_ => ()) - - def toOptionT: OptionT[H, T] = - OptionT.optionT[H](monadT(monadicValue)(value => Option(value))) - - def toUnitOptionT: OptionT[H, Unit] = - OptionT.optionT[H](monadT(monadicValue)(_ => Option(()))) - } - - implicit class FutureExtensions[T](future: Future[T]) { - def passThroughExternalServiceException(implicit executionContext: ExecutionContext): Future[T] = - future.transform(identity, { - case ExternalServiceException(_, _, Some(e)) => e - case t: Throwable => t - }) - } -} - -package core { - - final case class Id[+Tag](value: String) extends AnyVal { - @inline def length: Int = value.length - override def toString: String = value - } - - @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) - object Id { - implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _) - implicit def idOrdering[T]: Ordering[Id[T]] = Ordering.by[Id[T], String](_.value) - - sealed class Mapper[E, R] { - def apply[T >: E](id: Id[R]): Id[T] = Id[E](id.value) - def apply[T >: R](id: Id[E])(implicit dummy: DummyImplicit): Id[T] = Id[R](id.value) - } - object Mapper { - def apply[E, R] = new Mapper[E, R] - } - implicit def convertRE[R, E](id: Id[R])(implicit mapper: Mapper[E, R]): Id[E] = mapper[E](id) - implicit def convertER[E, R](id: Id[E])(implicit mapper: Mapper[E, R]): Id[R] = mapper[R](id) - } - - final case class Name[+Tag](value: String) extends AnyVal { - @inline def length: Int = value.length - override def toString: String = value - } - - object Name { - implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) - implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(_.value) - - implicit def nameValidator[T, P](implicit stringValidate: Validate[String, P]): Validate[Name[T], P] = { - Validate.instance[Name[T], P, stringValidate.R]( - name => stringValidate.validate(name.value), - name => stringValidate.showExpr(name.value)) - } - } - - final case class NonEmptyName[+Tag](value: String Refined NonEmpty) { - @inline def length: Int = value.value.length - override def toString: String = value.value - } - - object NonEmptyName { - implicit def nonEmptyNameEqual[T]: Equal[NonEmptyName[T]] = - Equal.equal[NonEmptyName[T]](_.value.value == _.value.value) - - implicit def nonEmptyNameOrdering[T]: Ordering[NonEmptyName[T]] = Ordering.by(_.value.value) - } - - final case class Revision[T](id: String) - - object Revision { - implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) - } - - final case class Base64(value: String) - -} diff --git a/src/main/scala/xyz/driver/core/date.scala b/src/main/scala/xyz/driver/core/date.scala deleted file mode 100644 index 5454093..0000000 --- a/src/main/scala/xyz/driver/core/date.scala +++ /dev/null @@ -1,109 +0,0 @@ -package xyz.driver.core - -import java.util.Calendar - -import enumeratum._ -import scalaz.std.anyVal._ -import scalaz.syntax.equal._ - -import scala.collection.immutable.IndexedSeq -import scala.util.Try - -/** - * Driver Date type and related validators/extractors. - * Day, Month, and Year extractors are from ISO 8601 strings => driver...Date integers. - * TODO: Decouple extractors from ISO 8601, as we might want to parse other formats. - */ -object date { - - sealed trait DayOfWeek extends EnumEntry - object DayOfWeek extends Enum[DayOfWeek] { - case object Monday extends DayOfWeek - case object Tuesday extends DayOfWeek - case object Wednesday extends DayOfWeek - case object Thursday extends DayOfWeek - case object Friday extends DayOfWeek - case object Saturday extends DayOfWeek - case object Sunday extends DayOfWeek - - val values: IndexedSeq[DayOfWeek] = findValues - - val All: Set[DayOfWeek] = values.toSet - - def fromString(day: String): Option[DayOfWeek] = withNameInsensitiveOption(day) - } - - type Day = Int @@ Day.type - - object Day { - def apply(value: Int): Day = { - require(1 to 31 contains value, "Day must be in range 1 <= value <= 31") - value.asInstanceOf[Day] - } - - def unapply(dayString: String): Option[Int] = { - require(dayString.length === 2, s"ISO 8601 day string, DD, must have length 2: $dayString") - Try(dayString.toInt).toOption.map(apply) - } - } - - type Month = Int @@ Month.type - - object Month { - def apply(value: Int): Month = { - require(0 to 11 contains value, "Month is zero-indexed: 0 <= value <= 11") - value.asInstanceOf[Month] - } - val JANUARY = Month(Calendar.JANUARY) - val FEBRUARY = Month(Calendar.FEBRUARY) - val MARCH = Month(Calendar.MARCH) - val APRIL = Month(Calendar.APRIL) - val MAY = Month(Calendar.MAY) - val JUNE = Month(Calendar.JUNE) - val JULY = Month(Calendar.JULY) - val AUGUST = Month(Calendar.AUGUST) - val SEPTEMBER = Month(Calendar.SEPTEMBER) - val OCTOBER = Month(Calendar.OCTOBER) - val NOVEMBER = Month(Calendar.NOVEMBER) - val DECEMBER = Month(Calendar.DECEMBER) - - def unapply(monthString: String): Option[Month] = { - require(monthString.length === 2, s"ISO 8601 month string, MM, must have length 2: $monthString") - Try(monthString.toInt).toOption.map(isoM => apply(isoM - 1)) - } - } - - type Year = Int @@ Year.type - - object Year { - def apply(value: Int): Year = value.asInstanceOf[Year] - - def unapply(yearString: String): Option[Int] = { - require(yearString.length === 4, s"ISO 8601 year string, YYYY, must have length 4: $yearString") - Try(yearString.toInt).toOption.map(apply) - } - } - - final case class Date(year: Int, month: Month, day: Int) { - override def toString = f"$year%04d-${month + 1}%02d-$day%02d" - } - - object Date { - implicit def dateOrdering: Ordering[Date] = Ordering.fromLessThan { (date1, date2) => - if (date1.year != date2.year) { - date1.year < date2.year - } else if (date1.month != date2.month) { - date1.month < date2.month - } else { - date1.day < date2.day - } - } - - def fromString(dateString: String): Option[Date] = { - dateString.split('-') match { - case Array(Year(year), Month(month), Day(day)) => Some(Date(year, month, day)) - case _ => None - } - } - } -} diff --git a/src/main/scala/xyz/driver/core/domain.scala b/src/main/scala/xyz/driver/core/domain.scala deleted file mode 100644 index f3b8337..0000000 --- a/src/main/scala/xyz/driver/core/domain.scala +++ /dev/null @@ -1,73 +0,0 @@ -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 - } - - object Email { - implicit val emailEqual: Equal[Email] = Equal.equal { - case (left, right) => left.toString.toLowerCase === right.toString.toLowerCase - } - - def parse(emailString: String): Option[Email] = { - Some(emailString.split("@")) collect { - case Array(username, domain) => Email(username, domain) - } - } - } - - 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[PhoneNumber] val phoneUtil = PhoneNumberUtil.getInstance() - - def parse(phoneNumber: String): Option[PhoneNumber] = { - 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/rest/package.scala b/src/main/scala/xyz/driver/core/rest/package.scala index 3be8f02..34a4a9d 100644 --- a/src/main/scala/xyz/driver/core/rest/package.scala +++ b/src/main/scala/xyz/driver/core/rest/package.scala @@ -15,10 +15,11 @@ import scalaz.Scalaz.{intInstance, stringInstance} import scalaz.syntax.equal._ import scalaz.{Functor, OptionT} import xyz.driver.core.rest.auth.AuthProvider +import xyz.driver.core.rest.errors.ExternalServiceException import xyz.driver.core.rest.headers.Traceparent import xyz.driver.tracing.TracingDirectives -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.util.Try trait Service @@ -72,6 +73,15 @@ object ListResponse { } object `package` { + + implicit class FutureExtensions[T](future: Future[T]) { + def passThroughExternalServiceException(implicit executionContext: ExecutionContext): Future[T] = + future.transform(identity, { + case ExternalServiceException(_, _, Some(e)) => e + case t: Throwable => t + }) + } + implicit class OptionTRestAdditions[T](optionT: OptionT[Future, T]) { def responseOrNotFound(successCode: StatusCodes.Success = StatusCodes.OK)( implicit F: Functor[Future], diff --git a/src/main/scala/xyz/driver/core/tagging/tagging.scala b/src/main/scala/xyz/driver/core/tagging/tagging.scala deleted file mode 100644 index 5b6599e..0000000 --- a/src/main/scala/xyz/driver/core/tagging/tagging.scala +++ /dev/null @@ -1,62 +0,0 @@ -package xyz.driver.core - -import scala.collection.generic.CanBuildFrom -import scala.language.{higherKinds, implicitConversions} - -/** - * @author sergey - * @since 9/11/18 - */ -package object tagging { - - implicit class Taggable[V <: Any](val v: V) extends AnyVal { - def tagged[Tag]: V @@ Tag = v.asInstanceOf[V @@ Tag] - } - -} - -package tagging { - - sealed trait Tagged[+V, +Tag] - - object Tagged { - implicit class TaggedOps[V, Tag](val v: V @@ Tag) extends AnyVal { - def tagless: V = v - } - - implicit def orderingMagnet[V, Tag](implicit ord: Ordering[V]): Ordering[V @@ Tag] = - ord.asInstanceOf[Ordering[V @@ Tag]] - - } - - sealed trait Trimmed - - object Trimmed { - - implicit def apply[V](trimmable: V)(implicit ev: CanBeTrimmed[V]): V @@ Trimmed = { - ev.trim(trimmable).tagged[Trimmed] - } - - sealed trait CanBeTrimmed[T] { - def trim(trimmable: T): T - } - - implicit object StringCanBeTrimmed extends CanBeTrimmed[String] { - def trim(str: String): String = str.trim() - } - - implicit def nameCanBeTrimmed[T]: CanBeTrimmed[Name[T]] = new CanBeTrimmed[Name[T]] { - def trim(name: Name[T]): Name[T] = Name[T](name.value.trim()) - } - - implicit def option2Trimmed[V: CanBeTrimmed](option: Option[V]): Option[V @@ Trimmed] = - option.map(Trimmed(_)) - - implicit def coll2Trimmed[T, C[_] <: Traversable[_]](coll: C[T])( - implicit ev: C[T] <:< Traversable[T], - tr: CanBeTrimmed[T], - bf: CanBuildFrom[Nothing, T @@ Trimmed, C[T @@ Trimmed]]): C[T @@ Trimmed] = - ev(coll).map(Trimmed(_)(tr)).to[C] - } - -} diff --git a/src/main/scala/xyz/driver/core/time.scala b/src/main/scala/xyz/driver/core/time.scala deleted file mode 100644 index 1622068..0000000 --- a/src/main/scala/xyz/driver/core/time.scala +++ /dev/null @@ -1,209 +0,0 @@ -package xyz.driver.core - -import java.text.SimpleDateFormat -import java.time.{Clock, Instant, ZoneId, ZoneOffset} -import java.util._ -import java.util.concurrent.TimeUnit - -import xyz.driver.core.date.Month - -import scala.concurrent.duration._ -import scala.language.implicitConversions -import scala.util.Try - -object time { - - // The most useful time units - val Second = 1000L - val Seconds = Second - val Minute = 60 * Seconds - val Minutes = Minute - val Hour = 60 * Minutes - val Hours = Hour - val Day = 24 * Hours - val Days = Day - val Week = 7 * Days - val Weeks = Week - - final case class Time(millis: Long) extends AnyVal { - - def isBefore(anotherTime: Time): Boolean = implicitly[Ordering[Time]].lt(this, anotherTime) - - def isAfter(anotherTime: Time): Boolean = implicitly[Ordering[Time]].gt(this, anotherTime) - - def advanceBy(duration: Duration): Time = Time(millis + duration.toMillis) - - def durationTo(anotherTime: Time): Duration = Duration.apply(anotherTime.millis - millis, TimeUnit.MILLISECONDS) - - def durationFrom(anotherTime: Time): Duration = Duration.apply(millis - anotherTime.millis, TimeUnit.MILLISECONDS) - - def toDate(timezone: TimeZone): date.Date = { - val cal = Calendar.getInstance(timezone) - cal.setTimeInMillis(millis) - date.Date(cal.get(Calendar.YEAR), date.Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) - } - - def toInstant: Instant = Instant.ofEpochMilli(millis) - } - - object Time { - implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) - - implicit def apply(instant: Instant): Time = Time(instant.toEpochMilli) - } - - /** - * Encapsulates a time and timezone without a specific date. - */ - final case class TimeOfDay(localTime: java.time.LocalTime, timeZone: TimeZone) { - - /** - * Is this time before another time on a specific day. Day light savings safe. These are zero-indexed - * for month/day. - */ - def isBefore(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).before(other.toCalendar(day, month, year)) - } - - /** - * Is this time after another time on a specific day. Day light savings safe. - */ - def isAfter(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).after(other.toCalendar(day, month, year)) - } - - def sameTimeAs(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).equals(other.toCalendar(day, month, year)) - } - - /** - * Enforces the same formatting as expected by [[java.sql.Time]] - * @return string formatted for `java.sql.Time` - */ - def timeString: String = { - localTime.format(TimeOfDay.getFormatter) - } - - /** - * @return a string parsable by [[java.util.TimeZone]] - */ - def timeZoneString: String = { - timeZone.getID - } - - /** - * @return this [[TimeOfDay]] as [[java.sql.Time]] object, [[java.sql.Time.valueOf]] will - * throw when the string is not valid, but this is protected by [[timeString]] method. - */ - def toTime: java.sql.Time = { - java.sql.Time.valueOf(timeString) - } - - private def toCalendar(day: Int, month: Int, year: Int): Calendar = { - val cal = Calendar.getInstance(timeZone) - cal.set(year, month, day, localTime.getHour, localTime.getMinute, localTime.getSecond) - cal.clear(Calendar.MILLISECOND) - cal - } - } - - object TimeOfDay { - def now(): TimeOfDay = { - TimeOfDay(java.time.LocalTime.now(), TimeZone.getDefault) - } - - /** - * Throws when [s] is not parsable by [[java.time.LocalTime.parse]], uses default [[java.util.TimeZone]] - */ - def parseTimeString(tz: TimeZone = TimeZone.getDefault)(s: String): TimeOfDay = { - TimeOfDay(java.time.LocalTime.parse(s), tz) - } - - def fromString(tz: TimeZone)(s: String): Option[TimeOfDay] = { - val op = Try(java.time.LocalTime.parse(s)).toOption - op.map(lt => TimeOfDay(lt, tz)) - } - - def fromStrings(zoneId: String)(s: String): Option[TimeOfDay] = { - val op = Try(TimeZone.getTimeZone(zoneId)).toOption - op.map(tz => TimeOfDay.parseTimeString(tz)(s)) - } - - /** - * Formatter that enforces `HH:mm:ss` which is expected by [[java.sql.Time]] - */ - def getFormatter: java.time.format.DateTimeFormatter = { - java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss") - } - } - - final case class TimeRange(start: Time, end: Time) { - def duration: Duration = FiniteDuration(end.millis - start.millis, MILLISECONDS) - } - - def startOfMonth(time: Time) = { - Time(make(new GregorianCalendar()) { cal => - cal.setTime(new Date(time.millis)) - cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)) - }.getTime.getTime) - } - - def textualDate(timezone: TimeZone)(time: Time): String = - make(new SimpleDateFormat("MMMM d, yyyy"))(_.setTimeZone(timezone)).format(new Date(time.millis)) - - def textualTime(timezone: TimeZone)(time: Time): String = - make(new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"))(_.setTimeZone(timezone)).format(new Date(time.millis)) - - class ChangeableClock(@volatile var instant: Instant, val zone: ZoneId = ZoneOffset.UTC) extends Clock { - - def tick(duration: FiniteDuration): Unit = - instant = instant.plusNanos(duration.toNanos) - - val getZone: ZoneId = zone - - def withZone(zone: ZoneId): Clock = new ChangeableClock(instant, zone = zone) - - override def toString: String = "ChangeableClock(" + instant + "," + zone + ")" - } - - object provider { - - /** - * Time providers are supplying code with current times - * and are extremely useful for testing to check how system is going - * to behave at specific moments in time. - * - * All the calls to receive current time must be made using time - * provider injected to the caller. - */ - @deprecated( - "Use java.time.Clock instead. Note that xyz.driver.core.Time and xyz.driver.core.date.Date will also be deprecated soon!", - "0.13.0") - trait TimeProvider { - def currentTime(): Time - def toClock: Clock - } - - final implicit class ClockTimeProvider(clock: Clock) extends TimeProvider { - def currentTime(): Time = Time(clock.instant().toEpochMilli) - - val toClock: Clock = clock - } - - final class SystemTimeProvider extends TimeProvider { - def currentTime() = Time(System.currentTimeMillis()) - - lazy val toClock: Clock = Clock.systemUTC() - } - - final val SystemTimeProvider = new SystemTimeProvider - - final class SpecificTimeProvider(time: Time) extends TimeProvider { - - def currentTime(): Time = time - - lazy val toClock: Clock = Clock.fixed(time.toInstant, ZoneOffset.UTC) - } - - } -} -- cgit v1.2.3