Permalink
Browse files

More intuitive type parameters (#18)

  • Loading branch information...
amrhassan committed Jan 10, 2018
1 parent 3afa2dd commit f3110a7f20349bc2814495621e5c8fccf4dd9da5
View
@@ -1,3 +1,3 @@
align = most
align.tokens = ["->", "=>", ":", "="]
align.tokens = ["<-", "->", "=>", ":", "="]
maxColumn = 160
View
@@ -1,4 +1,3 @@
name := "aws4s"
organization := "org.aws4s"
scalaVersion := "2.12.4"
@@ -17,8 +16,8 @@ scalacOptions in (Compile, doc) ++= Seq(
"-no-link-warnings" // Suppresses problems with Scaladoc
)
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.4")
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.4")
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
enablePlugins(TutPlugin)
val http4sVersion = "0.18.0-M8"
@@ -28,12 +27,12 @@ val scalatestVersion = "3.0.4"
val circeVersion = "0.9.0"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-client" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-scala-xml" % http4sVersion,
"io.circe" %% "circe-core" % circeVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.http4s" %% "http4s-blaze-client" % http4sVersion % Test,
"org.http4s" %% "http4s-client" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-scala-xml" % http4sVersion,
"io.circe" %% "circe-core" % circeVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.http4s" %% "http4s-blaze-client" % http4sVersion % Test,
)
publishTo := Some(
@@ -54,5 +53,5 @@ scmInfo := Some(
)
)
developers := List(
Developer(id="amrhassan", name="Amr Hassan", email="amr.hassan@gmail.com", url=url("http://amrhassan.info"))
Developer(id = "amrhassan", name = "Amr Hassan", email = "amr.hassan@gmail.com", url = url("http://amrhassan.info"))
)
@@ -6,11 +6,11 @@ import org.http4s.{EntityDecoder, Request}
import cats.implicits._
import org.http4s.client.Client
/** An AWS command that has parameter values rendered as [[B]] and when ran results in [[A]] */
private[aws4s] abstract class Command[F[_]: Effect, A: EntityDecoder[F, ?], B] {
/** An AWS command that has parameter values rendered as [[A]] and when ran results in [[R]] */
private[aws4s] abstract class Command[F[_]: Effect, A, R: EntityDecoder[F, ?]] {
/** The request that will be sent to AWS given the rendered input params */
def generateRequest(validRenderedParams: List[Param.Rendered[B]]): F[Request[F]]
def generateRequest(validRenderedParams: List[Param.Rendered[A]]): F[Request[F]]
/** Request body signing strategy for the outgoing request */
def payloadSigning: PayloadSigning
@@ -22,26 +22,26 @@ private[aws4s] abstract class Command[F[_]: Effect, A: EntityDecoder[F, ?], B] {
def region: Region
/** The input parameters of the command */
def params: List[Param.RenderedOptional[B]]
def params: List[Param.RenderedOptional[A]]
/** Runs the command given an HTTP client and a set of credentials */
final def run(fclient: F[Client[F]], credentials: () => Credentials): F[A] =
final def run(fclient: F[Client[F]], credentials: () => Credentials): F[R] =
(fclient, finalRequest(credentials)).tupled >>= {
case (client, r) =>
client.fetch(r) { resp =>
if (resp.status.isSuccess) {
resp.as[A]
resp.as[R]
} else {
resp.as[ResponseContent] >>= { content =>
(Failure.badResponse(resp.status, resp.headers, content): Throwable)
.raiseError[F, A]
.raiseError[F, R]
}
}
}
}
@inline private final def finalRequest(credentials: () => Credentials): F[Request[F]] = {
val renderedParams: Either[Failure, List[Param.Rendered[B]]] =
val renderedParams: Either[Failure, List[Param.Rendered[A]]] =
params.collect({ case Some(p) => p }).sequence
val request: F[Request[F]] = renderedParams match {
case Right(validParams) => generateRequest(validParams)
@@ -7,6 +7,6 @@ import org.http4s.client.Client
private[aws4s] abstract class Service[F[_], A] {
def client: F[Client[F]]
def credentials: () => Credentials
def run[B: EntityDecoder[F, ?]](command: Command[F, B, A]) =
def run[R: EntityDecoder[F, ?]](command: Command[F, A, R]) =
command.run(client, credentials)
}
@@ -1,9 +1,10 @@
package org.aws4s
case class ServiceName(name: String) extends AnyVal
abstract class ServiceName(val name: String)
object ServiceName {
val sqs = ServiceName("sqs")
val s3 = ServiceName("s3")
val kms = ServiceName("kms")
object Sqs extends ServiceName("sqs")
object S3 extends ServiceName("s3")
object Kms extends ServiceName("kms")
object DynamoDb extends ServiceName("dynamodb")
}
@@ -10,9 +10,9 @@ import org.http4s.headers.{Host, `Content-Type`}
import cats.implicits._
import ExtraEntityDecoderInstances._
private[kms] abstract class KmsCommand[F[_]: Effect, A: Decoder] extends Command[F, A, Json] {
private[kms] abstract class KmsCommand[F[_]: Effect, R: Decoder] extends Command[F, Json, R] {
override def serviceName: ServiceName = ServiceName.kms
override def serviceName: ServiceName = ServiceName.Kms
override def payloadSigning: PayloadSigning = PayloadSigning.Signed
def action: String
@@ -6,14 +6,14 @@ import org.http4s.{Method, Request, Uri}
import cats.implicits._
import org.aws4s.Param.RenderedOptional
private[aws4s] case class DeleteObject[F[_]: Effect](region: Region, bucket: Bucket, name: Uri.Path) extends Command[F, Unit, Nothing] {
private[aws4s] case class DeleteObject[F[_]: Effect](region: Region, bucket: Bucket, name: Uri.Path) extends Command[F, Nothing, Unit] {
override def generateRequest(validRenderedParams: List[Param.Rendered[Nothing]]): F[Request[F]] =
ObjectRequests.request[F](region, Method.DELETE, bucket, name).pure[F]
override def payloadSigning: PayloadSigning = PayloadSigning.Signed
override def serviceName: ServiceName = ServiceName.s3
override def serviceName: ServiceName = ServiceName.S3
override def params: List[RenderedOptional[Nothing]] = List.empty
}
@@ -12,14 +12,14 @@ private[aws4s] case class GetObject[F[_]: Effect](
region: Region,
bucket: Bucket,
name: Uri.Path
) extends Command[F, Stream[F, Byte], Nothing] {
) extends Command[F, Nothing, Stream[F, Byte]] {
override def generateRequest(validRenderedParams: List[Param.Rendered[Nothing]]): F[Request[F]] =
ObjectRequests.request[F](region, Method.GET, bucket, name).pure[F]
override def payloadSigning: PayloadSigning = PayloadSigning.Signed
override def serviceName: ServiceName = ServiceName.s3
override def serviceName: ServiceName = ServiceName.S3
override def params: List[RenderedOptional[Nothing]] = List.empty
}
@@ -7,7 +7,7 @@ import org.http4s.{EntityDecoder, Headers, Method, Request, Uri}
import cats.implicits._
import org.aws4s.Param.RenderedOptional
private[s3] case class ListBucketsCommand[F[_]: Effect]() extends Command[F, ListBucketsSuccess, Unit] {
private[s3] case class ListBucketsCommand[F[_]: Effect]() extends Command[F, Unit, ListBucketsSuccess] {
override val region = Region.`us-east-1`
private val host = s"s3.amazonaws.com"
@@ -17,7 +17,7 @@ private[s3] case class ListBucketsCommand[F[_]: Effect]() extends Command[F, Lis
override def payloadSigning: PayloadSigning = PayloadSigning.Signed
override def serviceName: ServiceName = ServiceName.s3
override def serviceName: ServiceName = ServiceName.S3
override def params: List[RenderedOptional[Unit]] = List.empty
}
@@ -13,12 +13,12 @@ private[aws4s] case class PutObject[F[_]: Effect](
name: Uri.Path,
obj: Stream[F, Byte],
payloadSigning: PayloadSigning
) extends Command[F, Unit, Nothing] {
) extends Command[F, Nothing, Unit] {
override def generateRequest(validRenderedParams: List[Param.Rendered[Nothing]]): F[Request[F]] =
ObjectRequests.request[F](region, Method.PUT, bucket, name, obj).pure[F]
override def serviceName: ServiceName = ServiceName.s3
override def serviceName: ServiceName = ServiceName.S3
override def params: List[RenderedOptional[Nothing]] = List.empty
}
@@ -6,7 +6,7 @@ import org.http4s.{EntityDecoder, Headers, Method, Request, UrlForm}
import org.aws4s.s3.PayloadSigning
import org.aws4s._
private[sqs] abstract class SqsCommand[F[_]: Effect, A: EntityDecoder[F, ?]] extends Command[F, A, String] {
private[sqs] abstract class SqsCommand[F[_]: Effect, R: EntityDecoder[F, ?]] extends Command[F, String, R] {
def action: String
def q: Queue
@@ -17,7 +17,7 @@ private[sqs] abstract class SqsCommand[F[_]: Effect, A: EntityDecoder[F, ?]] ext
}
override def payloadSigning: PayloadSigning = PayloadSigning.Signed
override def serviceName: ServiceName = ServiceName.sqs
override def serviceName: ServiceName = ServiceName.Sqs
override def region: Region = q.region
def params: List[Param.RenderedOptional[String]]
@@ -13,16 +13,16 @@ import org.scalatest.{FlatSpec, Matchers}
class RequestSigningSpec extends FlatSpec with Matchers {
val awsAccessKey = "AKIDEXAMPLE"
val awsSecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
val awsAccessKey = "AKIDEXAMPLE"
val awsSecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
val credentialsNow = Credentials(awsAccessKey, awsSecretKey)
val credentials = () => credentialsNow
val region = Region("us-east-1")
val service = ServiceName("service")
val credentials = () => credentialsNow
val region = Region("us-east-1")
object DummyServiceName extends ServiceName("service")
val sessionToken: String = "AKIDEXAMPLESESSION"
val credentialsNowWithSessionToken = Credentials(awsAccessKey, awsSecretKey, Some(sessionToken))
val credentialsWithSessionToken = () => credentialsNowWithSessionToken
val credentialsWithSessionToken = () => credentialsNowWithSessionToken
val clock: () => LocalDateTime = () => LocalDateTime.of(2011, 9, 9, 23, 36, 0)
@@ -37,14 +37,17 @@ class RequestSigningSpec extends FlatSpec with Matchers {
// Header for HTTP Request.
val headers = Headers(Header("Date", date), fooHost)
val signer = RequestSigning(credentials, region, service, PayloadSigning.Signed, clock)
val signer = RequestSigning(credentials, region, DummyServiceName, PayloadSigning.Signed, clock)
val signedHeaders = signer.signedHeaders[IO]("/", Method.GET, Map.empty[String, String], headers, Stream.empty).unsafeRunSync()
// The signature must match the expected signature
val expectedSignature = "b0a671385ef1f9513c15c34d206c7d83e3a4d848c43603569eca2760ee75c3b3"
val expectedAuthorizationHeader = String.format(
"AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=date;host, Signature=%s",
awsAccessKey, region.name, service.name, expectedSignature
awsAccessKey,
region.name,
DummyServiceName.name,
expectedSignature
)
assert(signedHeaders.iterator.contains(Header(Authorization.name.value, expectedAuthorizationHeader)))
@@ -66,14 +69,17 @@ class RequestSigningSpec extends FlatSpec with Matchers {
val params: Map[String, String] = Map.empty[String, String] ++ Map("Param2" -> "value2", "Param1" -> "value1")
val signer = RequestSigning(credentials, region, service, PayloadSigning.Signed, () => LocalDateTime.of(2015, 8, 30, 12, 36, 0))
val signer = RequestSigning(credentials, region, DummyServiceName, PayloadSigning.Signed, () => LocalDateTime.of(2015, 8, 30, 12, 36, 0))
val signedHeaders = signer.signedHeaders[IO]("/", Method.GET, params, headers, Stream.empty).unsafeRunSync()
// The signature must match the expected signature
val expectedSignature = "b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500"
val expectedAuthorizationHeader = String.format(
"AWS4-HMAC-SHA256 Credential=%s/20150830/%s/%s/aws4_request, SignedHeaders=host;x-amz-date, Signature=%s",
awsAccessKey, region.name, service.name, expectedSignature
awsAccessKey,
region.name,
DummyServiceName.name,
expectedSignature
)
assert(signedHeaders.iterator.contains(Header(Authorization.name.value, expectedAuthorizationHeader)))
@@ -90,14 +96,19 @@ class RequestSigningSpec extends FlatSpec with Matchers {
// WHEN
// The request is signed
val signer = RequestSigning(credentials, region, service, PayloadSigning.Signed, clock)
val signer = RequestSigning(credentials, region, DummyServiceName, PayloadSigning.Signed, clock)
val signedHeaders = signer.signedHeaders[IO]("/", Method.POST, queryParams, headers, Stream.empty).unsafeRunSync()
// THEN
// The signature must match the expected signature
val expectedSignature: String = "ffa9577fe836168407d8a9afce6d75e903de636017cb60bb37f4b094ecfb1c27"
val expectedAuthorizationHeader: String = format("AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=date;host, Signature=%s",
awsAccessKey, region.name, service.name, expectedSignature)
val expectedAuthorizationHeader: String = format(
"AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=date;host, Signature=%s",
awsAccessKey,
region.name,
DummyServiceName.name,
expectedSignature
)
assert(signedHeaders.iterator.contains(Header(Authorization.name.value, expectedAuthorizationHeader)))
assert(signedHeaders.iterator.contains(fooHost))
@@ -113,14 +124,19 @@ class RequestSigningSpec extends FlatSpec with Matchers {
// WHEN
// The request is signed
val signer = RequestSigning(credentials, region, service, PayloadSigning.Signed, clock)
val signer = RequestSigning(credentials, region, DummyServiceName, PayloadSigning.Signed, clock)
val signedHeaders = signer.signedHeaders[IO]("/", Method.GET, Map.empty[String, String], headers, Stream.empty).unsafeRunSync()
// THEN
// The signature must match the expected signature
val expectedSignature: String = "922abe18f0e78e55d69b34458c61e73134ab710adcb9a3257b638d70e2363ce1"
val expectedAuthorizationHeader: String = String.format("AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=host;x-amz-date, Signature=%s",
awsAccessKey, region.name, service.name, expectedSignature)
val expectedAuthorizationHeader: String = String.format(
"AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=host;x-amz-date, Signature=%s",
awsAccessKey,
region.name,
DummyServiceName.name,
expectedSignature
)
assert(signedHeaders.iterator.contains(Header(Authorization.name.value, expectedAuthorizationHeader)))
assert(signedHeaders.iterator.contains(fooHost))
@@ -136,14 +152,19 @@ class RequestSigningSpec extends FlatSpec with Matchers {
// WHEN
// The request is signed
val signer = RequestSigning(credentialsWithSessionToken, region, service, PayloadSigning.Signed, clock)
val signer = RequestSigning(credentialsWithSessionToken, region, DummyServiceName, PayloadSigning.Signed, clock)
val signedHeaders = signer.signedHeaders[IO]("/", Method.GET, Map.empty[String, String], headers, Stream.empty).unsafeRunSync()
// THEN
// The signature must match the expected signature
val expectedSignature: String = "78448a6ffad33b798ea2bb717fe5c3ef849a1b726ed1e692f4b5635b95070fb3"
val expectedAuthorizationHeader: String = format("AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=date;host;x-amz-security-token, Signature=%s",
awsAccessKey, region.name, service.name, expectedSignature)
val expectedAuthorizationHeader: String = format(
"AWS4-HMAC-SHA256 Credential=%s/20110909/%s/%s/aws4_request, SignedHeaders=date;host;x-amz-security-token, Signature=%s",
awsAccessKey,
region.name,
DummyServiceName.name,
expectedSignature
)
assert(signedHeaders.iterator.contains(Header(Authorization.name.value, expectedAuthorizationHeader)))
assert(signedHeaders.iterator.contains(fooHost))
@@ -6,13 +6,13 @@ import org.scalatest.{AsyncFlatSpec, Matchers}
abstract class SmokeTest extends AsyncFlatSpec with Matchers {
final val httpClient = Http1Client[IO]()
final val region = Region.`eu-central-1`
final val httpClient = Http1Client[IO]()
final val region = Region.`eu-central-1`
final val credentials = () => Credentials(getEnvOrDie("AWS_ACCESS_KEY"), getEnvOrDie("AWS_SECRET_KEY"))
private final def getEnvOrDie(name: String): String =
Option(System.getenv(name)) match {
case Some(v) => v
case None => throw new RuntimeException(s"ENV variable $name is missing")
case None => throw new RuntimeException(s"ENV variable $name is missing")
}
}
@@ -7,14 +7,14 @@ class KmsSmokeTest extends SmokeTest {
"Essential functionality" should "be alright" in {
val kms = Kms(httpClient, region, credentials)
val kms = Kms(httpClient, region, credentials)
val data = "secretdata"
val all = for {
keyId <- kms.createKey(Some(s"KMS smoke-test key ${ZonedDateTime.now}")) map (_.keyMetadata.keyId)
ciphertext <- kms.encrypt(keyId, data.getBytes) map (_.cipherText)
plaintext <- kms.decrypt(ciphertext) map (_.plainText)
_ <- kms.scheduleKeyDeletion(keyId, Some(7))
keyId <- kms.createKey(Some(s"KMS smoke-test key ${ZonedDateTime.now}")) map (_.keyMetadata.keyId)
ciphertext <- kms.encrypt(keyId, data.getBytes) map (_.cipherText)
plaintext <- kms.decrypt(ciphertext) map (_.plainText)
_ <- kms.scheduleKeyDeletion(keyId, Some(7))
} yield new String(plaintext)
all.unsafeToFuture() map (_ shouldBe data)
Oops, something went wrong.

0 comments on commit f3110a7

Please sign in to comment.