Skip to content
This repository has been archived by the owner on Sep 12, 2021. It is now read-only.

Commit

Permalink
Unify formats and make them composable (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
akkie committed Apr 7, 2017
1 parent 22e03f6 commit 9218c73
Show file tree
Hide file tree
Showing 20 changed files with 191 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package silhouette.jwt

import java.time.Instant

import org.jose4j.jwt.{ NumericDate, JwtClaims => JJwtClaims }
import silhouette.exceptions.JwtException
import silhouette.jwt.Jose4JJwtFormat._
Expand Down Expand Up @@ -58,9 +60,12 @@ final class Jose4JJwtFormat(
case Some(Nil) => None
case s => s
},
expirationTime = Option(claims.getExpirationTime).map(_.getValue),
notBefore = Option(claims.getNotBefore).map(_.getValue),
issuedAt = Option(claims.getIssuedAt).map(_.getValue),
expirationTime = Option(claims.getExpirationTime).map(_.getValue)
.map(seconds => Instant.ofEpochSecond(seconds)),
notBefore = Option(claims.getNotBefore).map(_.getValue)
.map(seconds => Instant.ofEpochSecond(seconds)),
issuedAt = Option(claims.getIssuedAt).map(_.getValue)
.map(seconds => Instant.ofEpochSecond(seconds)),
jwtID = Option(claims.getJwtId),
custom = claims.getClaimsMap(ReservedClaims.asJava).asScala match {
case l if l.isEmpty => JObject()
Expand Down Expand Up @@ -88,9 +93,9 @@ final class Jose4JJwtFormat(
claims.issuer.foreach(result.setIssuer)
claims.subject.foreach(result.setSubject)
claims.audience.foreach(v => result.setAudience(v.asJava))
claims.expirationTime.foreach(v => result.setExpirationTime(NumericDate.fromSeconds(v)))
claims.notBefore.foreach(v => result.setNotBefore(NumericDate.fromSeconds(v)))
claims.issuedAt.foreach(v => result.setIssuedAt(NumericDate.fromSeconds(v)))
claims.expirationTime.foreach(v => result.setExpirationTime(NumericDate.fromSeconds(v.getEpochSecond)))
claims.notBefore.foreach(v => result.setNotBefore(NumericDate.fromSeconds(v.getEpochSecond)))
claims.issuedAt.foreach(v => result.setIssuedAt(NumericDate.fromSeconds(v.getEpochSecond)))
claims.jwtID.foreach(result.setJwtId)
transformCustomClaims(claims.custom).asScala.foreach {
case (k, v) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
*/
package silhouette.jwt

import java.time.ZonedDateTime
import java.time.Clock
import java.time.temporal.ChronoUnit

import org.jose4j.jwa.AlgorithmConstraints
import org.jose4j.jws.AlgorithmIdentifiers._
Expand Down Expand Up @@ -53,15 +54,15 @@ class Jose4jJwtFormatSpec extends Specification with WithBouncyCastle {
}

"generate a JWT with an `exp` claim" in new Context {
generate(JwtClaims(expirationTime = Some(ZonedDateTime.now().toEpochSecond)))
generate(JwtClaims(expirationTime = Some(Clock.systemUTC().instant())))
}

"generate a JWT with a `nbf` claim" in new Context {
generate(JwtClaims(notBefore = Some(ZonedDateTime.now().toEpochSecond)))
generate(JwtClaims(notBefore = Some(Clock.systemUTC().instant())))
}

"generate a JWT with an `iat` claim" in new Context {
generate(JwtClaims(issuedAt = Some(ZonedDateTime.now().toEpochSecond)))
generate(JwtClaims(issuedAt = Some(Clock.systemUTC().instant())))
}

"generate a JWT with a `jti` claim" in new Context {
Expand All @@ -77,9 +78,9 @@ class Jose4jJwtFormatSpec extends Specification with WithBouncyCastle {
issuer = Some("test"),
subject = Some("test"),
audience = Some(List("test1", "test2")),
expirationTime = Some(ZonedDateTime.now().toEpochSecond),
notBefore = Some(ZonedDateTime.now().toEpochSecond),
issuedAt = Some(ZonedDateTime.now().toEpochSecond),
expirationTime = Some(Clock.systemUTC().instant()),
notBefore = Some(Clock.systemUTC().instant()),
issuedAt = Some(Clock.systemUTC().instant()),
jwtID = Some("test"),
custom = customClaims
))
Expand Down Expand Up @@ -191,7 +192,11 @@ class Jose4jJwtFormatSpec extends Specification with WithBouncyCastle {
protected def generate(claims: JwtClaims): MatchResult[Any] = {
generator.write(claims) must beSuccessfulTry.like {
case jwt =>
generator.read(jwt) must beSuccessfulTry.withValue(claims)
generator.read(jwt) must beSuccessfulTry.withValue(claims.copy(
expirationTime = claims.expirationTime.map(_.truncatedTo(ChronoUnit.SECONDS)),
notBefore = claims.notBefore.map(_.truncatedTo(ChronoUnit.SECONDS)),
issuedAt = claims.issuedAt.map(_.truncatedTo(ChronoUnit.SECONDS))
))
}
}

Expand Down
1 change: 1 addition & 0 deletions silhouette/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ libraryDependencies ++= Seq(
Library.inject,
Library.commonCodec,
Library.Circe.core,
Library.Circe.generic,
Library.Circe.parser,
Library.scalaXml
)
Expand Down
67 changes: 29 additions & 38 deletions silhouette/src/main/scala/silhouette/http/Transport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,30 @@
*/
package silhouette.http

import silhouette.util.Format
import silhouette.util.{ Reads, Writes }

import scala.language.implicitConversions
import scala.util.Try

/**
* Transforms a string into an instance of `T`.
*
* @tparam T The target type on the read operation.
*/
trait TransportReads[T] extends Reads[String, Try[T]]

/**
* Transforms an instance of `T` into a string.
*
* @tparam T The source type on the write operation
*/
trait TransportWrites[T] extends Writes[T, Try[String]]

/**
* Transport transformer combinator.
*
* @tparam T The target type on the read operation and the source type on the write operation.
*/
trait TransportFormat[T] extends TransportReads[T] with TransportWrites[T]

/**
* Marker trait for the transport settings.
Expand All @@ -30,11 +51,8 @@ trait TransportSettings
* Defines the way how payload can be transported in an HTTP request or an HTTP response.
*
* The transport is divided into two parts. The [[RequestTransport]] and the [[ResponseTransport]].
*
* @tparam A The internal type every transport implementation handles.
* @tparam B The type of the payload to transport.
*/
trait Transport[A, B] {
trait Transport {

/**
* The type of the concrete implementation of this abstract type.
Expand All @@ -51,43 +69,19 @@ trait Transport[A, B] {
*/
protected val settings: Settings

/**
* The format to transform between the transport specific value and the payload.
*/
protected val format: Format[A, B]

/**
* Gets a transport initialized with a new settings object.
*
* @param f A function which gets the settings passed and returns different settings.
* @return An instance of the transport initialized with new settings.
*/
def withSettings(f: Settings => Settings): Self

/**
* Transforms the transport specific value into the payload.
*
* @param value The transport specific value to read from.
* @return The transformed payload or an exception if an error occurred during transformation.
*/
implicit protected def read(value: A): B = format.read(value).get

/**
* Transforms the payload into the transport specific value.
*
* @param payload The payload that should be transformed into the transport specific value.
* @return The transport specific value.
*/
implicit protected def write(payload: B): A = format.write(payload)
}

/**
* The request transport handles payload which can be transported in a request.
*
* @tparam A The internal type every transport implementation handles.
* @tparam B The type of the payload to transport.
*/
trait RequestTransport[A, B] extends Transport[A, B] {
trait RequestTransport extends Transport {

/**
* Retrieves payload from the given request.
Expand All @@ -96,7 +90,7 @@ trait RequestTransport[A, B] extends Transport[A, B] {
* @tparam R The type of the request.
* @return Some value or None if no payload could be found in request.
*/
def retrieve[R](request: RequestPipeline[R]): Option[B]
def retrieve[R](request: RequestPipeline[R]): Option[String]

/**
* Embeds payload into the request.
Expand All @@ -109,16 +103,13 @@ trait RequestTransport[A, B] extends Transport[A, B] {
* @tparam R The type of the request.
* @return The manipulated request pipeline.
*/
def embed[R](payload: B, request: RequestPipeline[R]): RequestPipeline[R]
def embed[R](payload: String, request: RequestPipeline[R]): RequestPipeline[R]
}

/**
* The response transport handles payload which can be transported in a response.
*
* @tparam A The internal type every transport implementation handles.
* @tparam B The type of the payload to transport.
*/
trait ResponseTransport[A, B] extends Transport[A, B] {
trait ResponseTransport extends Transport {

/**
* Embeds payload into the response.
Expand All @@ -128,7 +119,7 @@ trait ResponseTransport[A, B] extends Transport[A, B] {
* @tparam P The type of the response.
* @return The manipulated response pipeline.
*/
def embed[P](payload: B, response: ResponsePipeline[P]): ResponsePipeline[P]
def embed[P](payload: String, response: ResponsePipeline[P]): ResponsePipeline[P]

/**
* Manipulates the response so that it removes payload stored on the client.
Expand Down
22 changes: 18 additions & 4 deletions silhouette/src/main/scala/silhouette/http/client/Body.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import io.circe.parser.parse
import io.circe.{ Json, ParsingFailure }
import silhouette.exceptions.{ TransformException, UnsupportedContentTypeException }
import silhouette.http.client.DefaultBodyFormat._
import silhouette.util.Format
import silhouette.util.{ Reads, Writes }

import scala.io.Codec
import scala.util.{ Failure, Success, Try }
Expand Down Expand Up @@ -51,11 +51,25 @@ private[silhouette] object Body {
}

/**
* Represents a transformation from [[Body]] to an instance of T.
* Transforms a [[Body]] into an instance of `T`.
*
* @tparam T The target type.
* @tparam T The target type on the read operation.
*/
private[silhouette] trait BodyFormat[T] extends Format[Body, T]
private[silhouette] trait BodyReads[T] extends Reads[Body, Try[T]]

/**
* Transforms an instance of `T` into a [[Body]].
*
* @tparam T The source type on the write operation
*/
private[silhouette] trait BodyWrites[T] extends Writes[T, Body]

/**
* Body transformer combinator.
*
* @tparam T The target type on the read operation and the source type on the write operation.
*/
private[silhouette] trait BodyFormat[T] extends BodyReads[T] with BodyWrites[T]

/**
* The only aim of this object is to provide a default implicit [[BodyFormat]], that uses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package silhouette.http.transport

import silhouette.http._
import silhouette.util.Format

import scala.concurrent.duration.FiniteDuration

Expand All @@ -45,17 +44,14 @@ final case class CookieTransportSettings(
* The cookie transport.
*
* @param settings The transport settings.
* @param format The format to transform between the transport specific value and the payload.
*/
final case class CookieTransport[B](settings: CookieTransportSettings)(
implicit
protected val format: Format[String, B]
) extends RequestTransport[String, B] with ResponseTransport[String, B] {
final case class CookieTransport(settings: CookieTransportSettings)
extends RequestTransport with ResponseTransport {

/**
* The type of the concrete implementation of this abstract type.
*/
override type Self = CookieTransport[B]
override type Self = CookieTransport

/**
* The settings type.
Expand All @@ -77,7 +73,7 @@ final case class CookieTransport[B](settings: CookieTransportSettings)(
* @tparam R The type of the request.
* @return Some value or None if no payload could be found in request.
*/
override def retrieve[R](request: RequestPipeline[R]): Option[B] =
override def retrieve[R](request: RequestPipeline[R]): Option[String] =
request.cookie(settings.name).map(_.value)

/**
Expand All @@ -88,7 +84,7 @@ final case class CookieTransport[B](settings: CookieTransportSettings)(
* @tparam R The type of the request.
* @return The manipulated request pipeline.
*/
override def embed[R](payload: B, request: RequestPipeline[R]): RequestPipeline[R] =
override def embed[R](payload: String, request: RequestPipeline[R]): RequestPipeline[R] =
request.withCookies(cookie(payload))

/**
Expand All @@ -99,7 +95,7 @@ final case class CookieTransport[B](settings: CookieTransportSettings)(
* @tparam P The type of the response.
* @return The manipulated response pipeline.
*/
override def embed[P](payload: B, response: ResponsePipeline[P]): ResponsePipeline[P] =
override def embed[P](payload: String, response: ResponsePipeline[P]): ResponsePipeline[P] =
response.withCookies(cookie(payload))

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package silhouette.http.transport

import silhouette.http._
import silhouette.util.Format

/**
* The settings for the header transport.
Expand All @@ -30,19 +29,15 @@ final case class HeaderTransportSettings(name: String) extends TransportSettings
/**
* The header transport.
*
* @tparam B The type of the payload.
* @param settings The transport settings.
* @param format The format to transform between the transport specific value and the payload.
*/
final case class HeaderTransport[B](settings: HeaderTransportSettings)(
implicit
protected val format: Format[String, B]
) extends RequestTransport[String, B] with ResponseTransport[String, B] {
final case class HeaderTransport(settings: HeaderTransportSettings)
extends RequestTransport with ResponseTransport {

/**
* The type of the concrete implementation of this abstract type.
*/
override type Self = HeaderTransport[B]
override type Self = HeaderTransport

/**
* The settings type.
Expand All @@ -64,8 +59,8 @@ final case class HeaderTransport[B](settings: HeaderTransportSettings)(
* @tparam R The type of the request.
* @return Some value or None if no payload could be found in request.
*/
override def retrieve[R](request: RequestPipeline[R]): Option[B] =
request.header(settings.name).headOption.map(read)
override def retrieve[R](request: RequestPipeline[R]): Option[String] =
request.header(settings.name).headOption

/**
* Adds a header with the given payload to the request.
Expand All @@ -75,7 +70,7 @@ final case class HeaderTransport[B](settings: HeaderTransportSettings)(
* @tparam R The type of the request.
* @return The manipulated request pipeline.
*/
override def embed[R](payload: B, request: RequestPipeline[R]): RequestPipeline[R] =
override def embed[R](payload: String, request: RequestPipeline[R]): RequestPipeline[R] =
request.withHeaders(settings.name -> payload)

/**
Expand All @@ -86,7 +81,7 @@ final case class HeaderTransport[B](settings: HeaderTransportSettings)(
* @tparam P The type of the response.
* @return The manipulated response pipeline.
*/
override def embed[P](payload: B, response: ResponsePipeline[P]): ResponsePipeline[P] =
override def embed[P](payload: String, response: ResponsePipeline[P]): ResponsePipeline[P] =
response.withHeaders(settings.name -> payload)

/**
Expand Down
Loading

0 comments on commit 9218c73

Please sign in to comment.