diff options
author | Jakob Odersky <jakob@driver.xyz> | 2018-06-29 17:56:06 -0700 |
---|---|---|
committer | Jakob Odersky <jakob@driver.xyz> | 2018-07-01 16:16:52 -0700 |
commit | b6859c8560af601d716729d29094a156c9c01503 (patch) | |
tree | 031929da54c659ff5cd58835962c53459b498629 /jvm | |
parent | 901b02274fdfc08030443aac2f1760fc479b3816 (diff) | |
download | driver-core-b6859c8560af601d716729d29094a156c9c01503.tar.gz driver-core-b6859c8560af601d716729d29094a156c9c01503.tar.bz2 driver-core-b6859c8560af601d716729d29094a156c9c01503.zip |
Move shared classes (IDs, Formats, etc) to shared source folder
* The JSON format object was split into traits and akka-specific
unmarshallers are moved into a separate 'Directives' trait.
* The singleton object xyz.driver.core.json is now deprecated.
These changes should be source compatible, although they are not
binary compatible.
Diffstat (limited to 'jvm')
18 files changed, 124 insertions, 1336 deletions
diff --git a/jvm/src/main/scala/xyz/driver/core/auth.scala b/jvm/src/main/scala/xyz/driver/core/auth.scala deleted file mode 100644 index 896bd89..0000000 --- a/jvm/src/main/scala/xyz/driver/core/auth.scala +++ /dev/null @@ -1,43 +0,0 @@ -package xyz.driver.core - -import xyz.driver.core.domain.Email -import xyz.driver.core.time.Time -import scalaz.Equal - -object auth { - - trait Permission - - final case class Role(id: Id[Role], name: Name[Role]) { - - def oneOf(roles: Role*): Boolean = roles.contains(this) - - def oneOf(roles: Set[Role]): Boolean = roles.contains(this) - } - - object Role { - implicit def idEqual: Equal[Role] = Equal.equal[Role](_ == _) - } - - trait User { - def id: Id[User] - } - - final case class AuthToken(value: String) - - final case class AuthTokenUserInfo( - id: Id[User], - email: Email, - emailVerified: Boolean, - audience: String, - roles: Set[Role], - expirationTime: Time) - extends User - - final case class RefreshToken(value: String) - final case class PermissionsToken(value: String) - - final case class PasswordHash(value: String) - - final case class AuthCredentials(identifier: String, password: String) -} diff --git a/jvm/src/main/scala/xyz/driver/core/core.scala b/jvm/src/main/scala/xyz/driver/core/core.scala deleted file mode 100644 index 72237b9..0000000 --- a/jvm/src/main/scala/xyz/driver/core/core.scala +++ /dev/null @@ -1,128 +0,0 @@ -package xyz.driver - -import scalaz.{Equal, Monad, OptionT} -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty -import xyz.driver.core.rest.errors.ExternalServiceException - -import scala.concurrent.{ExecutionContext, Future} - -package object core { - - import scala.language.reflectiveCalls - - 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() - } - } - - object tagging { - private[core] trait Tagged[+V, +Tag] - - implicit class Taggable[V <: Any](val v: V) extends AnyVal { - def tagged[Tag]: V @@ Tag = v.asInstanceOf[V @@ Tag] - } - } - type @@[+V, +Tag] = V with tagging.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/jvm/src/main/scala/xyz/driver/core/date.scala b/jvm/src/main/scala/xyz/driver/core/date.scala deleted file mode 100644 index 5454093..0000000 --- a/jvm/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/jvm/src/main/scala/xyz/driver/core/domain.scala b/jvm/src/main/scala/xyz/driver/core/domain.scala deleted file mode 100644 index fa3b5c4..0000000 --- a/jvm/src/main/scala/xyz/driver/core/domain.scala +++ /dev/null @@ -1,46 +0,0 @@ -package xyz.driver.core - -import com.google.i18n.phonenumbers.PhoneNumberUtil -import scalaz.Equal -import scalaz.std.string._ -import scalaz.syntax.equal._ - -object domain { - - final case class Email(username: String, domain: String) { - 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 = "1", number: String) { - override def toString: String = s"+$countryCode $number" - } - - object PhoneNumber { - - private val phoneUtil = PhoneNumberUtil.getInstance() - - def parse(phoneNumber: String): Option[PhoneNumber] = { - val phone = scala.util.Try(phoneUtil.parseAndKeepRawInput(phoneNumber, "US")).toOption - - val validated = phone match { - case None => None - case Some(pn) => - if (!phoneUtil.isValidNumber(pn)) None - else Some(pn) - } - validated.map(pn => PhoneNumber(pn.getCountryCode.toString, pn.getNationalNumber.toString)) - } - } -} diff --git a/jvm/src/main/scala/xyz/driver/core/domain/package.scala b/jvm/src/main/scala/xyz/driver/core/domain/package.scala new file mode 100644 index 0000000..a76e83c --- /dev/null +++ b/jvm/src/main/scala/xyz/driver/core/domain/package.scala @@ -0,0 +1,24 @@ +package xyz.driver.core + +import com.google.i18n.phonenumbers.PhoneNumberUtil + +package object domain { + + private val phoneUtil = PhoneNumberUtil.getInstance() + + /** Enhances the PhoneNumber companion object with methods only available on the JVM. */ + implicit class JvmPhoneNumber(val number: PhoneNumber.type) extends AnyVal { + def parse(phoneNumber: String): Option[PhoneNumber] = { + val phone = scala.util.Try(phoneUtil.parseAndKeepRawInput(phoneNumber, "US")).toOption + + val validated = phone match { + case None => None + case Some(pn) => + if (!phoneUtil.isValidNumber(pn)) None + else Some(pn) + } + validated.map(pn => PhoneNumber(pn.getCountryCode.toString, pn.getNationalNumber.toString)) + } + } + +} diff --git a/jvm/src/main/scala/xyz/driver/core/future.scala b/jvm/src/main/scala/xyz/driver/core/future.scala deleted file mode 100644 index 1ee3576..0000000 --- a/jvm/src/main/scala/xyz/driver/core/future.scala +++ /dev/null @@ -1,87 +0,0 @@ -package xyz.driver.core - -import com.typesafe.scalalogging.Logger - -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success, Try} - -object future { - val log = Logger("Driver.Future") - - implicit class RichFuture[T](f: Future[T]) { - def mapAll[U](pf: PartialFunction[Try[T], U])(implicit executionContext: ExecutionContext): Future[U] = { - val p = Promise[U]() - f.onComplete(r => p.complete(Try(pf(r)))) - p.future - } - - def failFastZip[U](that: Future[U])(implicit executionContext: ExecutionContext): Future[(T, U)] = { - future.failFastZip(f, that) - } - } - - def failFastSequence[T](t: Iterable[Future[T]])(implicit ec: ExecutionContext): Future[Seq[T]] = { - t.foldLeft(Future.successful(Nil: List[T])) { (f, i) => - failFastZip(f, i).map { case (tail, h) => h :: tail } - } - .map(_.reverse) - } - - /** - * Standard scala zip waits forever on the left side, even if the right side fails - */ - def failFastZip[T, U](ft: Future[T], fu: Future[U])(implicit ec: ExecutionContext): Future[(T, U)] = { - type State = Either[(T, Promise[U]), (U, Promise[T])] - val middleState = Promise[State]() - - ft.onComplete { - case f @ Failure(err) => - if (!middleState.tryFailure(err)) { - // the right has already succeeded - middleState.future.foreach { - case Right((_, pt)) => pt.complete(f) - case Left((t1, _)) => // This should never happen - log.error(s"Logic error: tried to set Failure($err) but Left($t1) already set") - } - } - case Success(t) => - // Create the next promise: - val pu = Promise[U]() - if (!middleState.trySuccess(Left((t, pu)))) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Right((_, pt)) => pt.success(t) - case Left((t1, _)) => // This should never happen - log.error(s"Logic error: tried to set Left($t) but Left($t1) already set") - } - } - } - fu.onComplete { - case f @ Failure(err) => - if (!middleState.tryFailure(err)) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Left((_, pu)) => pu.complete(f) - case Right((u1, _)) => // This should never happen - log.error(s"Logic error: tried to set Failure($err) but Right($u1) already set") - } - } - case Success(u) => - // Create the next promise: - val pt = Promise[T]() - if (!middleState.trySuccess(Right((u, pt)))) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Left((_, pu)) => pu.success(u) - case Right((u1, _)) => // This should never happen - log.error(s"Logic error: tried to set Right($u) but Right($u1) already set") - } - } - } - - middleState.future.flatMap { - case Left((t, pu)) => pu.future.map((t, _)) - case Right((u, pt)) => pt.future.map((_, u)) - } - } -} diff --git a/jvm/src/main/scala/xyz/driver/core/generators.scala b/jvm/src/main/scala/xyz/driver/core/generators.scala deleted file mode 100644 index d57980e..0000000 --- a/jvm/src/main/scala/xyz/driver/core/generators.scala +++ /dev/null @@ -1,138 +0,0 @@ -package xyz.driver.core - -import enumeratum._ -import java.math.MathContext -import java.util.UUID - -import xyz.driver.core.time.{Time, TimeOfDay, TimeRange} -import xyz.driver.core.date.{Date, DayOfWeek} - -import scala.reflect.ClassTag -import scala.util.Random -import eu.timepit.refined.refineV -import eu.timepit.refined.api.Refined -import eu.timepit.refined.collection._ - -object generators { - - private val random = new Random - import random._ - private val secureRandom = new java.security.SecureRandom() - - private val DefaultMaxLength = 10 - private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet - private val NonAmbigiousCharacters = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" - private val Numbers = "0123456789" - - private def nextTokenString(length: Int, chars: IndexedSeq[Char]): String = { - val builder = new StringBuilder - for (_ <- 0 until length) { - builder += chars(secureRandom.nextInt(chars.length)) - } - builder.result() - } - - /** Creates a random invitation token. - * - * This token is meant fo human input and avoids using ambiguous characters such as 'O' and '0'. It - * therefore contains less entropy and is not meant to be used as a cryptographic secret. */ - @deprecated( - "The term 'token' is too generic and security and readability conventions are not well defined. " + - "Services should implement their own version that suits their security requirements.", - "1.11.0" - ) - def nextToken(length: Int): String = nextTokenString(length, NonAmbigiousCharacters) - - @deprecated( - "The term 'token' is too generic and security and readability conventions are not well defined. " + - "Services should implement their own version that suits their security requirements.", - "1.11.0" - ) - def nextNumericToken(length: Int): String = nextTokenString(length, Numbers) - - def nextInt(maxValue: Int, minValue: Int = 0): Int = random.nextInt(maxValue - minValue) + minValue - - def nextBoolean(): Boolean = random.nextBoolean() - - def nextDouble(): Double = random.nextDouble() - - def nextId[T](): Id[T] = Id[T](nextUuid().toString) - - def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) - - def nextNumericId[T](): Id[T] = Id[T](nextLong.abs.toString) - - def nextNumericId[T](maxValue: Int): Id[T] = Id[T](nextInt(maxValue).toString) - - def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) - - def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] = - NonEmptyName[T](nextNonEmptyString(maxLength)) - - def nextUuid(): UUID = java.util.UUID.randomUUID - - def nextRevision[T](): Revision[T] = Revision[T](nextUuid().toString) - - def nextString(maxLength: Int = DefaultMaxLength): String = - (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString - - def nextNonEmptyString(maxLength: Int = DefaultMaxLength): String Refined NonEmpty = { - refineV[NonEmpty]( - (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString - ).right.get - } - - def nextOption[T](value: => T): Option[T] = if (nextBoolean()) Option(value) else None - - def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right) - - def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third) - - def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis)) - - def nextTimeOfDay: TimeOfDay = TimeOfDay(java.time.LocalTime.MIN.plusSeconds(nextLong), java.util.TimeZone.getDefault) - - def nextTimeRange(): TimeRange = { - val oneTime = nextTime() - val anotherTime = nextTime() - - TimeRange( - Time(scala.math.min(oneTime.millis, anotherTime.millis)), - Time(scala.math.max(oneTime.millis, anotherTime.millis))) - } - - def nextDate(): Date = nextTime().toDate(java.util.TimeZone.getTimeZone("UTC")) - - def nextDayOfWeek(): DayOfWeek = oneOf(DayOfWeek.All) - - def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = - BigDecimal(multiplier * nextDouble, new MathContext(precision)) - - def oneOf[T](items: T*): T = oneOf(items.toSet) - - def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size)) - - def oneOf[T <: EnumEntry](enum: Enum[T]): T = oneOf(enum.values: _*) - - def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Array[T] = - Array.fill(nextInt(maxLength, minLength))(generator) - - def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Seq[T] = - Seq.fill(nextInt(maxLength, minLength))(generator) - - def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Vector[T] = - Vector.fill(nextInt(maxLength, minLength))(generator) - - def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): List[T] = - List.fill(nextInt(maxLength, minLength))(generator) - - def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Set[T] = - seqOf(generator, maxLength, minLength).toSet - - def mapOf[K, V]( - keyGenerator: => K, - valueGenerator: => V, - maxLength: Int = DefaultMaxLength, - minLength: Int = 0): Map[K, V] = - seqOf(nextPair(keyGenerator, valueGenerator), maxLength, minLength).toMap -} diff --git a/jvm/src/main/scala/xyz/driver/core/json.scala b/jvm/src/main/scala/xyz/driver/core/json.scala index de1df31..ca5a47b 100644 --- a/jvm/src/main/scala/xyz/driver/core/json.scala +++ b/jvm/src/main/scala/xyz/driver/core/json.scala @@ -1,401 +1,20 @@ package xyz.driver.core -import java.net.InetAddress -import java.util.{TimeZone, UUID} - -import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} -import akka.http.scaladsl.model.Uri.Path -import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} -import akka.http.scaladsl.server._ import akka.http.scaladsl.unmarshalling.Unmarshaller import enumeratum._ -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty -import eu.timepit.refined.refineV -import spray.json._ -import xyz.driver.core.auth.AuthCredentials -import xyz.driver.core.date.{Date, DayOfWeek, Month} -import xyz.driver.core.domain.{Email, PhoneNumber} -import xyz.driver.core.rest.errors._ -import xyz.driver.core.time.{Time, TimeOfDay} - -import scala.reflect.runtime.universe._ -import scala.util.Try - -object json { - import DefaultJsonProtocol._ - - private def UuidInPath[T]: PathMatcher1[Id[T]] = - PathMatchers.JavaUUID.map((id: UUID) => Id[T](id.toString.toLowerCase)) - - def IdInPath[T]: PathMatcher1[Id[T]] = UuidInPath[T] | new PathMatcher1[Id[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => Matched(tail, Tuple1(Id[T](segment))) - case _ => Unmatched - } - } - - implicit def paramUnmarshaller[T](implicit reader: JsonReader[T]): Unmarshaller[String, T] = - Unmarshaller.firstOf( - Unmarshaller.strict((JsString(_: String)) andThen reader.read), - stringToValueUnmarshaller[T] - ) - - implicit def idFormat[T]: RootJsonFormat[Id[T]] = new RootJsonFormat[Id[T]] { - def write(id: Id[T]) = JsString(id.value) - - def read(value: JsValue): Id[T] = value match { - case JsString(id) if Try(UUID.fromString(id)).isSuccess => Id[T](id.toLowerCase) - case JsString(id) => Id[T](id) - case _ => throw DeserializationException("Id expects string") - } - } - - implicit def taggedFormat[F, T](implicit underlying: JsonFormat[F]): JsonFormat[F @@ T] = new JsonFormat[F @@ T] { - import tagging._ - - override def write(obj: F @@ T): JsValue = underlying.write(obj) - - override def read(json: JsValue): F @@ T = underlying.read(json).tagged[T] - } - - def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => Matched(tail, Tuple1(Name[T](segment))) - case _ => Unmatched - } - } - - implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { - def write(name: Name[T]) = JsString(name.value) - - def read(value: JsValue): Name[T] = value match { - case JsString(name) => Name[T](name) - case _ => throw DeserializationException("Name expects string") - } - } - - def TimeInPath: PathMatcher1[Time] = - PathMatcher("""[+-]?\d*""".r) flatMap { string => - try Some(Time(string.toLong)) - catch { case _: IllegalArgumentException => None } - } - - implicit val timeFormat = new RootJsonFormat[Time] { - def write(time: Time) = JsObject("timestamp" -> JsNumber(time.millis)) - - def read(value: JsValue): Time = value match { - case JsObject(fields) => - fields - .get("timestamp") - .flatMap { - case JsNumber(millis) => Some(Time(millis.toLong)) - case _ => None - } - .getOrElse(throw DeserializationException("Time expects number")) - case _ => throw DeserializationException("Time expects number") - } - } - - implicit object localTimeFormat extends JsonFormat[java.time.LocalTime] { - private val formatter = TimeOfDay.getFormatter - def read(json: JsValue): java.time.LocalTime = json match { - case JsString(chars) => - java.time.LocalTime.parse(chars) - case _ => deserializationError(s"Expected time string got ${json.toString}") - } - - def write(obj: java.time.LocalTime): JsValue = { - JsString(obj.format(formatter)) - } - } - - implicit object timeZoneFormat extends JsonFormat[java.util.TimeZone] { - override def write(obj: TimeZone): JsValue = { - JsString(obj.getID()) - } - - override def read(json: JsValue): TimeZone = json match { - case JsString(chars) => - java.util.TimeZone.getTimeZone(chars) - case _ => deserializationError(s"Expected time zone string got ${json.toString}") - } - } - - implicit val timeOfDayFormat: RootJsonFormat[TimeOfDay] = jsonFormat2(TimeOfDay.apply) - - implicit val dayOfWeekFormat: JsonFormat[DayOfWeek] = new enumeratum.EnumJsonFormat(DayOfWeek) - - implicit val dateFormat = new RootJsonFormat[Date] { - def write(date: Date) = JsString(date.toString) - def read(value: JsValue): Date = value match { - case JsString(dateString) => - Date - .fromString(dateString) - .getOrElse( - throw DeserializationException(s"Misformated ISO 8601 Date. Expected YYYY-MM-DD, but got $dateString.")) - case _ => throw DeserializationException(s"Date expects a string, but got $value.") - } - } - - implicit val monthFormat = new RootJsonFormat[Month] { - def write(month: Month) = JsNumber(month) - def read(value: JsValue): Month = value match { - case JsNumber(month) if 0 <= month && month <= 11 => Month(month.toInt) - case _ => throw DeserializationException("Expected a number from 0 to 11") - } - } - - def RevisionInPath[T]: PathMatcher1[Revision[T]] = - PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string => - Some(Revision[T](string)) - } - - implicit def revisionFromStringUnmarshaller[T]: Unmarshaller[String, Revision[T]] = - Unmarshaller.strict[String, Revision[T]](Revision[T]) - - implicit def revisionFormat[T]: RootJsonFormat[Revision[T]] = new RootJsonFormat[Revision[T]] { - def write(revision: Revision[T]) = JsString(revision.id.toString) - - def read(value: JsValue): Revision[T] = value match { - case JsString(revision) => Revision[T](revision) - case _ => throw DeserializationException("Revision expects uuid string") - } - } - - implicit val base64Format = new RootJsonFormat[Base64] { - def write(base64Value: Base64) = JsString(base64Value.value) - - def read(value: JsValue): Base64 = value match { - case JsString(base64Value) => Base64(base64Value) - case _ => throw DeserializationException("Base64 format expects string") - } - } - - implicit val emailFormat = new RootJsonFormat[Email] { - def write(email: Email) = JsString(email.username + "@" + email.domain) - def read(json: JsValue): Email = json match { - case JsString(value) => - Email.parse(value).getOrElse { - deserializationError("Expected '@' symbol in email string as Email, but got " + json.toString) - } - - case _ => - deserializationError("Expected string as Email, but got " + json.toString) - } - } - - implicit val phoneNumberFormat = jsonFormat2(PhoneNumber.apply) - - implicit val authCredentialsFormat = new RootJsonFormat[AuthCredentials] { - override def read(json: JsValue): AuthCredentials = { - json match { - case JsObject(fields) => - val emailField = fields.get("email") - val identifierField = fields.get("identifier") - val passwordField = fields.get("password") - - (emailField, identifierField, passwordField) match { - case (_, _, None) => - deserializationError("password field must be set") - case (Some(JsString(em)), _, Some(JsString(pw))) => - val email = Email.parse(em).getOrElse(throw deserializationError(s"failed to parse email $em")) - AuthCredentials(email.toString, pw) - case (_, Some(JsString(id)), Some(JsString(pw))) => AuthCredentials(id.toString, pw.toString) - case (None, None, _) => deserializationError("identifier must be provided") - case _ => deserializationError(s"failed to deserialize ${json.prettyPrint}") - } - case _ => deserializationError(s"failed to deserialize ${json.prettyPrint}") - } - } - - override def write(obj: AuthCredentials): JsValue = JsObject( - "identifier" -> JsString(obj.identifier), - "password" -> JsString(obj.password) - ) - } - - implicit object inetAddressFormat extends JsonFormat[InetAddress] { - override def read(json: JsValue): InetAddress = json match { - case JsString(ipString) => - Try(InetAddress.getByName(ipString)) - .getOrElse(deserializationError(s"Invalid IP Address: $ipString")) - case _ => deserializationError(s"Expected string for IP Address, got $json") - } - - override def write(obj: InetAddress): JsValue = - JsString(obj.getHostAddress) - } +@deprecated( + "Using static JSON formats from singleton objects can require to many wildcard imports. It is " + + "recommended to stack format traits into a single protocol.", + "driver-core 1.11.5" +) +object json extends CoreJsonFormats with rest.Unmarshallers with rest.PathMatchers { self => object enumeratum { - def enumUnmarshaller[T <: EnumEntry](enum: Enum[T]): Unmarshaller[String, T] = - Unmarshaller.strict { value => - enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) - } - - trait HasJsonFormat[T <: EnumEntry] { enum: Enum[T] => - - implicit val format: JsonFormat[T] = new EnumJsonFormat(enum) - - implicit val unmarshaller: Unmarshaller[String, T] = - Unmarshaller.strict { value => - enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) - } - } - - class EnumJsonFormat[T <: EnumEntry](enum: Enum[T]) extends JsonFormat[T] { - override def read(json: JsValue): T = json match { - case JsString(name) => enum.withNameOption(name).getOrElse(unrecognizedValue(name, enum.values)) - case _ => deserializationError("Expected string as enumeration value, but got " + json.toString) - } - - override def write(obj: T): JsValue = JsString(obj.entryName) - } - - private def unrecognizedValue(value: String, possibleValues: Seq[Any]): Nothing = - deserializationError(s"Unexpected value $value. Expected one of: ${possibleValues.mkString("[", ", ", "]")}") + rest.Directives.enumUnmarshaller(enum) + type HasJsonFormat[T <: EnumEntry] = self.HasJsonFormat[T] + type EnumJsonFormat[T <: EnumEntry] = self.EnumJsonFormat[T] } - class EnumJsonFormat[T](mapping: (String, T)*) extends RootJsonFormat[T] { - private val map = mapping.toMap - - override def write(value: T): JsValue = { - map.find(_._2 == value).map(_._1) match { - case Some(name) => JsString(name) - case _ => serializationError(s"Value $value is not found in the mapping $map") - } - } - - override def read(json: JsValue): T = json match { - case JsString(name) => - map.getOrElse(name, throw DeserializationException(s"Value $name is not found in the mapping $map")) - case _ => deserializationError("Expected string as enumeration value, but got " + json.toString) - } - } - - class ValueClassFormat[T: TypeTag](writeValue: T => BigDecimal, create: BigDecimal => T) extends JsonFormat[T] { - def write(valueClass: T) = JsNumber(writeValue(valueClass)) - def read(json: JsValue): T = json match { - case JsNumber(value) => create(value) - case _ => deserializationError(s"Expected number as ${typeOf[T].getClass.getName}, but got " + json.toString) - } - } - - class GadtJsonFormat[T: TypeTag]( - typeField: String, - typeValue: PartialFunction[T, String], - jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) - extends RootJsonFormat[T] { - - def write(value: T): JsValue = { - - val valueType = typeValue.applyOrElse(value, { v: T => - deserializationError(s"No Value type for this type of ${typeOf[T].getClass.getName}: " + v.toString) - }) - - val valueFormat = - jsonFormat.applyOrElse(valueType, { f: String => - deserializationError(s"No Json format for this type of $valueType") - }) - - valueFormat.asInstanceOf[JsonFormat[T]].write(value) match { - case JsObject(fields) => JsObject(fields ++ Map(typeField -> JsString(valueType))) - case _ => serializationError(s"${typeOf[T].getClass.getName} serialized not to a JSON object") - } - } - - def read(json: JsValue): T = json match { - case JsObject(fields) => - val valueJson = JsObject(fields.filterNot(_._1 == typeField)) - fields(typeField) match { - case JsString(valueType) => - val valueFormat = jsonFormat.applyOrElse(valueType, { t: String => - deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") - }) - valueFormat.read(valueJson) - case _ => - deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") - } - case _ => - deserializationError(s"Expected Json Object as ${typeOf[T].getClass.getName}, but got " + json.toString) - } - } - - object GadtJsonFormat { - - def create[T: TypeTag](typeField: String)(typeValue: PartialFunction[T, String])( - jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) = { - - new GadtJsonFormat[T](typeField, typeValue, jsonFormat) - } - } - - /** - * Provides the JsonFormat for the Refined types provided by the Refined library. - * - * @see https://github.com/fthomas/refined - */ - implicit def refinedJsonFormat[T, Predicate]( - implicit valueFormat: JsonFormat[T], - validate: Validate[T, Predicate]): JsonFormat[Refined[T, Predicate]] = - new JsonFormat[Refined[T, Predicate]] { - def write(x: T Refined Predicate): JsValue = valueFormat.write(x.value) - def read(value: JsValue): T Refined Predicate = { - refineV[Predicate](valueFormat.read(value))(validate) match { - case Right(refinedValue) => refinedValue - case Left(refinementError) => deserializationError(refinementError) - } - } - } - - def NonEmptyNameInPath[T]: PathMatcher1[NonEmptyName[T]] = new PathMatcher1[NonEmptyName[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => - refineV[NonEmpty](segment) match { - case Left(_) => Unmatched - case Right(nonEmptyString) => Matched(tail, Tuple1(NonEmptyName[T](nonEmptyString))) - } - case _ => Unmatched - } - } - - implicit def nonEmptyNameFormat[T](implicit nonEmptyStringFormat: JsonFormat[Refined[String, NonEmpty]]) = - new RootJsonFormat[NonEmptyName[T]] { - def write(name: NonEmptyName[T]) = JsString(name.value.value) - - def read(value: JsValue): NonEmptyName[T] = - NonEmptyName[T](nonEmptyStringFormat.read(value)) - } - - implicit val serviceExceptionFormat: RootJsonFormat[ServiceException] = - GadtJsonFormat.create[ServiceException]("type") { - case _: InvalidInputException => "InvalidInputException" - case _: InvalidActionException => "InvalidActionException" - case _: ResourceNotFoundException => "ResourceNotFoundException" - case _: ExternalServiceException => "ExternalServiceException" - case _: ExternalServiceTimeoutException => "ExternalServiceTimeoutException" - case _: DatabaseException => "DatabaseException" - } { - case "InvalidInputException" => jsonFormat(InvalidInputException, "message") - case "InvalidActionException" => jsonFormat(InvalidActionException, "message") - case "ResourceNotFoundException" => jsonFormat(ResourceNotFoundException, "message") - case "ExternalServiceException" => - jsonFormat(ExternalServiceException, "serviceName", "serviceMessage", "serviceException") - case "ExternalServiceTimeoutException" => jsonFormat(ExternalServiceTimeoutException, "message") - case "DatabaseException" => jsonFormat(DatabaseException, "message") - } - - val jsValueToStringMarshaller: Marshaller[JsValue, String] = - Marshaller.strict[JsValue, String](value => Marshalling.Opaque[String](() => value.compactPrint)) - - def valueToStringMarshaller[T](implicit jsonFormat: JsonWriter[T]): Marshaller[T, String] = - jsValueToStringMarshaller.compose[T](jsonFormat.write) - - val stringToJsValueUnmarshaller: Unmarshaller[String, JsValue] = - Unmarshaller.strict[String, JsValue](value => value.parseJson) - - def stringToValueUnmarshaller[T](implicit jsonFormat: JsonReader[T]): Unmarshaller[String, T] = - stringToJsValueUnmarshaller.map[T](jsonFormat.read) } diff --git a/jvm/src/main/scala/xyz/driver/core/rest/Directives.scala b/jvm/src/main/scala/xyz/driver/core/rest/Directives.scala new file mode 100644 index 0000000..8e5983b --- /dev/null +++ b/jvm/src/main/scala/xyz/driver/core/rest/Directives.scala @@ -0,0 +1,83 @@ +package xyz.driver.core.rest + +import java.util.UUID + +import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} +import akka.http.scaladsl.model.Uri.Path +import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} +import akka.http.scaladsl.server.{PathMatchers => AkkaPathMatchers, Directives => AkkaDirectives, _} +import akka.http.scaladsl.unmarshalling.Unmarshaller +import enumeratum._ +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.refineV +import spray.json._ +import xyz.driver.core.time.Time +import xyz.driver.core.{CoreJsonFormats, Id, Name, NonEmptyName} + +trait Directives extends AkkaDirectives with Unmarshallers with PathMatchers +object Directives extends Directives + +trait PathMatchers { + + private def UuidInPath[T]: PathMatcher1[Id[T]] = + AkkaPathMatchers.JavaUUID.map((id: UUID) => Id[T](id.toString.toLowerCase)) + + def IdInPath[T]: PathMatcher1[Id[T]] = UuidInPath[T] | new PathMatcher1[Id[T]] { + def apply(path: Path) = path match { + case Path.Segment(segment, tail) => Matched(tail, Tuple1(Id[T](segment))) + case _ => Unmatched + } + } + + def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { + def apply(path: Path) = path match { + case Path.Segment(segment, tail) => Matched(tail, Tuple1(Name[T](segment))) + case _ => Unmatched + } + } + + def TimeInPath: PathMatcher1[Time] = + PathMatcher("""[+-]?\d*""".r) flatMap { string => + try Some(Time(string.toLong)) + catch { case _: IllegalArgumentException => None } + } + + def NonEmptyNameInPath[T]: PathMatcher1[NonEmptyName[T]] = new PathMatcher1[NonEmptyName[T]] { + def apply(path: Path) = path match { + case Path.Segment(segment, tail) => + refineV[NonEmpty](segment) match { + case Left(_) => Unmatched + case Right(nonEmptyString) => Matched(tail, Tuple1(NonEmptyName[T](nonEmptyString))) + } + case _ => Unmatched + } + } + +} + +trait Unmarshallers { + + implicit def paramUnmarshaller[T](implicit reader: JsonReader[T]): Unmarshaller[String, T] = + Unmarshaller.firstOf( + Unmarshaller.strict((JsString(_: String)) andThen reader.read), + stringToValueUnmarshaller[T] + ) + + def enumUnmarshaller[T <: EnumEntry](enum: Enum[T]): Unmarshaller[String, T] = + Unmarshaller.strict { value => + enum.withNameOption(value).getOrElse(CoreJsonFormats.unrecognizedValue(value, enum.values)) + } + + val jsValueToStringMarshaller: Marshaller[JsValue, String] = + Marshaller.strict[JsValue, String](value => Marshalling.Opaque[String](() => value.compactPrint)) + + def valueToStringMarshaller[T](implicit jsonFormat: JsonWriter[T]): Marshaller[T, String] = + jsValueToStringMarshaller.compose[T](jsonFormat.write) + + val stringToJsValueUnmarshaller: Unmarshaller[String, JsValue] = + Unmarshaller.strict[String, JsValue](value => value.parseJson) + + def stringToValueUnmarshaller[T](implicit jsonFormat: JsonReader[T]): Unmarshaller[String, T] = + stringToJsValueUnmarshaller.map[T](jsonFormat.read) + +} diff --git a/jvm/src/main/scala/xyz/driver/core/rest/RestService.scala b/jvm/src/main/scala/xyz/driver/core/rest/RestService.scala index 8d46d72..ca10c08 100644 --- a/jvm/src/main/scala/xyz/driver/core/rest/RestService.scala +++ b/jvm/src/main/scala/xyz/driver/core/rest/RestService.scala @@ -1,16 +1,16 @@ package xyz.driver.core.rest +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.model._ import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} import akka.stream.Materializer import scala.concurrent.{ExecutionContext, Future} import scalaz.{ListT, OptionT} +import spray.json._ +import xyz.driver.core.CoreJsonFormats -trait RestService extends Service { - - import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ - import spray.json._ +trait RestService extends Service with CoreJsonFormats with Unmarshallers with SprayJsonSupport { protected implicit val exec: ExecutionContext protected implicit val materializer: Materializer diff --git a/jvm/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala b/jvm/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala deleted file mode 100644 index 1a5e9be..0000000 --- a/jvm/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.ServiceRequestContext - -import scala.concurrent.Future - -trait Authorization[U <: User] { - def userHasPermissions(user: U, permissions: Seq[Permission])( - implicit ctx: ServiceRequestContext): Future[AuthorizationResult] -} diff --git a/jvm/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala b/jvm/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala deleted file mode 100644 index efe28c9..0000000 --- a/jvm/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala +++ /dev/null @@ -1,22 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, PermissionsToken} - -import scalaz.Scalaz.mapMonoid -import scalaz.Semigroup -import scalaz.syntax.semigroup._ - -final case class AuthorizationResult(authorized: Map[Permission, Boolean], token: Option[PermissionsToken]) -object AuthorizationResult { - val unauthorized: AuthorizationResult = AuthorizationResult(authorized = Map.empty, None) - - implicit val authorizationSemigroup: Semigroup[AuthorizationResult] = new Semigroup[AuthorizationResult] { - private implicit val authorizedBooleanSemigroup = Semigroup.instance[Boolean](_ || _) - private implicit val permissionsTokenSemigroup = - Semigroup.instance[Option[PermissionsToken]]((a, b) => b.orElse(a)) - - override def append(a: AuthorizationResult, b: => AuthorizationResult): AuthorizationResult = { - AuthorizationResult(a.authorized |+| b.authorized, a.token |+| b.token) - } - } -} diff --git a/jvm/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/jvm/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala deleted file mode 100644 index db289de..0000000 --- a/jvm/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.driver.core.rest.errors - -sealed abstract class ServiceException(val message: String) extends Exception(message) - -final case class InvalidInputException(override val message: String = "Invalid input") extends ServiceException(message) - -final case class InvalidActionException(override val message: String = "This action is not allowed") - extends ServiceException(message) - -final case class ResourceNotFoundException(override val message: String = "Resource not found") - extends ServiceException(message) - -final case class ExternalServiceException( - serviceName: String, - serviceMessage: String, - serviceException: Option[ServiceException]) - extends ServiceException(s"Error while calling '$serviceName': $serviceMessage") - -final case class ExternalServiceTimeoutException(serviceName: String) - extends ServiceException(s"$serviceName took too long to respond") - -final case class DatabaseException(override val message: String = "Database access error") - extends ServiceException(message) diff --git a/jvm/src/main/scala/xyz/driver/core/rest/package.scala b/jvm/src/main/scala/xyz/driver/core/rest/package.scala index f85c39a..550a71f 100644 --- a/jvm/src/main/scala/xyz/driver/core/rest/package.scala +++ b/jvm/src/main/scala/xyz/driver/core/rest/package.scala @@ -11,7 +11,6 @@ import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.Materializer import akka.stream.scaladsl.Flow import akka.util.ByteString -import xyz.driver.tracing.TracingDirectives import scala.concurrent.Future import scala.util.Try @@ -19,8 +18,6 @@ import scalaz.{Functor, OptionT} import scalaz.Scalaz.{intInstance, stringInstance} import scalaz.syntax.equal._ -trait Service - trait HttpClient { def makeRequest(request: HttpRequest): Future[HttpResponse] } @@ -33,40 +30,6 @@ trait ServiceTransport { implicit mat: Materializer): Future[Unmarshal[ResponseEntity]] } -sealed trait SortingOrder -object SortingOrder { - case object Asc extends SortingOrder - case object Desc extends SortingOrder -} - -final case class SortingField(name: String, sortingOrder: SortingOrder) -final case class Sorting(sortingFields: Seq[SortingField]) - -final case class Pagination(pageSize: Int, pageNumber: Int) { - require(pageSize > 0, "Page size must be greater than zero") - require(pageNumber > 0, "Page number must be greater than zero") - - def offset: Int = pageSize * (pageNumber - 1) -} - -final case class ListResponse[+T](items: Seq[T], meta: ListResponse.Meta) - -object ListResponse { - - def apply[T](items: Seq[T], size: Int, pagination: Option[Pagination]): ListResponse[T] = - ListResponse( - items = items, - meta = ListResponse.Meta(size, pagination.fold(1)(_.pageNumber), pagination.fold(size)(_.pageSize))) - - final case class Meta(itemsCount: Int, pageNumber: Int, pageSize: Int) - - object Meta { - def apply(itemsCount: Int, pagination: Pagination): Meta = - Meta(itemsCount, pagination.pageNumber, pagination.pageSize) - } - -} - object `package` { implicit class OptionTRestAdditions[T](optionT: OptionT[Future, T]) { def responseOrNotFound(successCode: StatusCodes.Success = StatusCodes.OK)( @@ -76,27 +39,6 @@ object `package` { } } - object ContextHeaders { - val AuthenticationTokenHeader: String = "Authorization" - val PermissionsTokenHeader: String = "Permissions" - val AuthenticationHeaderPrefix: String = "Bearer" - val ClientFingerprintHeader: String = "X-Client-Fingerprint" - val TrackingIdHeader: String = "X-Trace" - val StacktraceHeader: String = "X-Stacktrace" - val OriginatingIpHeader: String = "X-Forwarded-For" - val ResourceCount: String = "X-Resource-Count" - val PageCount: String = "X-Page-Count" - val TraceHeaderName: String = TracingDirectives.TraceHeaderName - val SpanHeaderName: String = TracingDirectives.SpanHeaderName - } - - object AuthProvider { - val AuthenticationTokenHeader: String = ContextHeaders.AuthenticationTokenHeader - val PermissionsTokenHeader: String = ContextHeaders.PermissionsTokenHeader - val SetAuthenticationTokenHeader: String = "set-authorization" - val SetPermissionsTokenHeader: String = "set-permissions" - } - val AllowedHeaders: Seq[String] = Seq( "Origin", diff --git a/jvm/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala b/jvm/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala deleted file mode 100644 index 775106e..0000000 --- a/jvm/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala +++ /dev/null @@ -1,74 +0,0 @@ -package xyz.driver.core.rest - -import java.net.InetAddress - -import xyz.driver.core.auth.{AuthToken, PermissionsToken, User} -import xyz.driver.core.generators - -import scalaz.Scalaz.{mapEqual, stringInstance} -import scalaz.syntax.equal._ - -class ServiceRequestContext( - val trackingId: String = generators.nextUuid().toString, - val originatingIp: Option[InetAddress] = None, - val contextHeaders: Map[String, String] = Map.empty[String, String]) { - def authToken: Option[AuthToken] = - contextHeaders.get(AuthProvider.AuthenticationTokenHeader).map(AuthToken.apply) - - def permissionsToken: Option[PermissionsToken] = - contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) - - def withAuthToken(authToken: AuthToken): ServiceRequestContext = - new ServiceRequestContext( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value) - ) - - def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedServiceRequestContext[U] = - new AuthorizedServiceRequestContext( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), - user - ) - - override def hashCode(): Int = - Seq[Any](trackingId, originatingIp, contextHeaders) - .foldLeft(31)((result, obj) => 31 * result + obj.hashCode()) - - override def equals(obj: Any): Boolean = obj match { - case ctx: ServiceRequestContext => - trackingId === ctx.trackingId && - originatingIp == originatingIp && - contextHeaders === ctx.contextHeaders - case _ => false - } - - override def toString: String = s"ServiceRequestContext($trackingId, $contextHeaders)" -} - -class AuthorizedServiceRequestContext[U <: User]( - override val trackingId: String = generators.nextUuid().toString, - override val originatingIp: Option[InetAddress] = None, - override val contextHeaders: Map[String, String] = Map.empty[String, String], - val authenticatedUser: U) - extends ServiceRequestContext { - - def withPermissionsToken(permissionsToken: PermissionsToken): AuthorizedServiceRequestContext[U] = - new AuthorizedServiceRequestContext[U]( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.PermissionsTokenHeader, permissionsToken.value), - authenticatedUser) - - override def hashCode(): Int = 31 * super.hashCode() + authenticatedUser.hashCode() - - override def equals(obj: Any): Boolean = obj match { - case ctx: AuthorizedServiceRequestContext[U] => super.equals(ctx) && ctx.authenticatedUser == authenticatedUser - case _ => false - } - - override def toString: String = - s"AuthorizedServiceRequestContext($trackingId, $contextHeaders, $authenticatedUser)" -} diff --git a/jvm/src/main/scala/xyz/driver/core/time.scala b/jvm/src/main/scala/xyz/driver/core/time.scala deleted file mode 100644 index 6dbd173..0000000 --- a/jvm/src/main/scala/xyz/driver/core/time.scala +++ /dev/null @@ -1,175 +0,0 @@ -package xyz.driver.core - -import java.text.SimpleDateFormat -import java.util._ -import java.util.concurrent.TimeUnit - -import xyz.driver.core.date.Month - -import scala.concurrent.duration._ -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)) - } - } - - /** - * 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") - } - } - - object Time { - - implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) - } - - 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)) - - 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. - */ - trait TimeProvider { - def currentTime(): Time - } - - final class SystemTimeProvider extends TimeProvider { - def currentTime() = Time(System.currentTimeMillis()) - } - final val SystemTimeProvider = new SystemTimeProvider - - final class SpecificTimeProvider(time: Time) extends TimeProvider { - def currentTime() = time - } - } -} diff --git a/jvm/src/test/scala/xyz/driver/core/JsonTest.scala b/jvm/src/test/scala/xyz/driver/core/JsonTest.scala index fed2a9d..b8922ae 100644 --- a/jvm/src/test/scala/xyz/driver/core/JsonTest.scala +++ b/jvm/src/test/scala/xyz/driver/core/JsonTest.scala @@ -142,7 +142,7 @@ class JsonTest extends FlatSpec with Matchers { case object Val2 extends EnumVal case object Val3 extends EnumVal - val format = new EnumJsonFormat[EnumVal]("a" -> Val1, "b" -> Val2, "c" -> Val3) + val format = new EnumJsonFormat2[EnumVal]("a" -> Val1, "b" -> Val2, "c" -> Val3) val referenceEnumValue1 = Val2 val referenceEnumValue2 = Val3 @@ -226,29 +226,6 @@ class JsonTest extends FlatSpec with Matchers { }.getMessage shouldBe "Unexpected value Val4. Expected one of: [Val1, Val 2, Val/3]" } - // Should be defined outside of case to have a TypeTag - case class CustomWrapperClass(value: Int) - - "Json format for Value classes" should "read and write correct JSON" in { - - val format = new ValueClassFormat[CustomWrapperClass](v => BigDecimal(v.value), d => CustomWrapperClass(d.toInt)) - - val referenceValue1 = CustomWrapperClass(-2) - val referenceValue2 = CustomWrapperClass(10) - - val writtenJson1 = format.write(referenceValue1) - writtenJson1.prettyPrint should be("-2") - - val writtenJson2 = format.write(referenceValue2) - writtenJson2.prettyPrint should be("10") - - val parsedValue1 = format.read(writtenJson1) - val parsedValue2 = format.read(writtenJson2) - - parsedValue1 should be(referenceValue1) - parsedValue2 should be(referenceValue2) - } - "Json format for classes GADT" should "read and write correct JSON" in { import CustomGADT._ diff --git a/jvm/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala b/jvm/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala index d32fefd..247dc5a 100644 --- a/jvm/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala +++ b/jvm/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala @@ -4,16 +4,15 @@ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers.Connection import akka.http.scaladsl.server.Directives.{complete => akkaComplete} -import akka.http.scaladsl.server.{Directives, Rejection, RejectionHandler, Route} +import akka.http.scaladsl.server.{RejectionHandler, Route} import akka.http.scaladsl.testkit.ScalatestRouteTest import com.typesafe.scalalogging.Logger import org.scalatest.{AsyncFlatSpec, Matchers} -import xyz.driver.core.logging.NoLogger -import xyz.driver.core.json.serviceExceptionFormat import xyz.driver.core.FutureExtensions +import xyz.driver.core.json.serviceExceptionFormat +import xyz.driver.core.logging.NoLogger import xyz.driver.core.rest.errors._ -import scala.collection.immutable import scala.concurrent.Future class DriverRouteTest |