aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/com/drivergrp/core/rest.scala
blob: 4edb466b7ec2aaa09abf45d0e5d3053f7250d80c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package com.drivergrp.core

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshalling.{Marshal, Marshaller}
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.Flow
import akka.util.{ByteString, Timeout}
import com.drivergrp.core.crypto.{AuthToken, Crypto}
import com.drivergrp.core.logging.Logger
import com.drivergrp.core.stats.Stats
import com.drivergrp.core.time.TimeRange
import com.drivergrp.core.time.provider.TimeProvider
import com.github.swagger.akka.model._
import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService}
import com.typesafe.config.Config

import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.language.postfixOps
import scala.util.{Failure, Success}
import scalaz.{Failure => _, Success => _}
import scalaz.Scalaz._

object rest {

  final case class ServiceVersion(majorVersion: Int, minorVersion: Int) {
    def isCompatible(otherVersion: ServiceVersion) =
      this.majorVersion === otherVersion.majorVersion
  }

  trait Service {

    def sendRequest[I,O](authToken: AuthToken)(requestInput: I)
                        (implicit marshaller: Marshaller[I, RequestEntity],
                                  unmarshaller: Unmarshaller[ResponseEntity, O]): Future[O]
  }

  trait ServiceDiscovery {

    def discover(serviceName: Name[Service], version: ServiceVersion): Service
  }

  class HttpRestService(method: HttpMethod, uri: Uri, version: ServiceVersion,
                        actorSystem: ActorSystem, executionContext: ExecutionContext,
                        crypto: Crypto, log: Logger, stats: Stats, time: TimeProvider) extends Service {

    protected implicit val materializer = ActorMaterializer()(actorSystem)
    protected implicit val execution = executionContext
    protected implicit val timeout = Timeout(5 seconds)

    def sendRequest[I,O](authToken: AuthToken)(requestInput: I)
                        (implicit marshaller: Marshaller[I, RequestEntity],
                                  unmarshaller: Unmarshaller[ResponseEntity, O]): Future[O] = {

      val requestTime = time.currentTime()
      val encryptionFlow = Flow[ByteString] map { bytes =>
        ByteString(crypto.encrypt(crypto.keyForToken(authToken))(bytes.toArray))
      }
      val decryptionFlow = Flow[ByteString] map { bytes =>
        ByteString(crypto.decrypt(crypto.keyForToken(authToken))(bytes.toArray))
      }

      val response: Future[O] = for {
        requestData: RequestEntity <- Marshal(requestInput).to[RequestEntity](marshaller, executionContext)
        encryptedMessage = requestData.transformDataBytes(encryptionFlow)
        request: HttpRequest = buildRequest(authToken, requestData)
        _ = log.audit(s"Sending to ${request.uri} request $request")
        response <- Http()(actorSystem).singleRequest(request)(materializer)
        decryptedResponse = requestData.transformDataBytes(decryptionFlow)
        responseEntity <- Unmarshal(decryptedResponse).to[O](unmarshaller, executionContext, materializer)
      } yield {
        responseEntity
      }

      response.onComplete {
        case Success(r) =>
          val responseTime = time.currentTime()
          log.audit(s"Response from $uri to request $requestInput is successful")
          stats.recordStats(Seq("request", uri.toString, "success"), TimeRange(requestTime, responseTime), 1)

        case Failure(t: Throwable) =>
          val responseTime = time.currentTime()
          log.audit(s"Failed to receive response from $uri of version $version to request $requestInput")
          log.error(s"Failed to receive response from $uri of version $version to request $requestInput", t)
          stats.recordStats(Seq("request", uri.toString, "fail"), TimeRange(requestTime, responseTime), 1)
      } (executionContext)

      response
    }

    private def buildRequest(authToken: AuthToken, requestData: RequestEntity): HttpRequest = {

      HttpRequest(
        method, uri,
        headers = Vector(
          RawHeader("WWW-Authenticate", s"Macaroon ${authToken.value.value}"),
          RawHeader("Api-Version", version.majorVersion + "." + version.minorVersion)
        ),
        entity = requestData)
    }
  }

  import scala.reflect.runtime.universe._

  class Swagger(override val host: String,
                override val actorSystem: ActorSystem,
                override val apiTypes: Seq[Type],
                val config: Config) extends SwaggerHttpService with HasActorSystem {

    val materializer = ActorMaterializer()(actorSystem)

    override val basePath = config.getString("swagger.basePath")
    override val apiDocsPath = config.getString("swagger.docsPath")

    override val info = Info(
      config.getString("swagger.apiInfo.description"),
      config.getString("swagger.apiVersion"),
      config.getString("swagger.apiInfo.title"),
      config.getString("swagger.apiInfo.termsOfServiceUrl"),
      contact = Some(Contact(
        config.getString("swagger.apiInfo.contact.name"),
        config.getString("swagger.apiInfo.contact.url"),
        config.getString("swagger.apiInfo.contact.email")
      )),
      license = Some(License(
        config.getString("swagger.apiInfo.license"),
        config.getString("swagger.apiInfo.licenseUrl")
      )),
      vendorExtensions = Map.empty[String, AnyRef])

    def swaggerUI = get {
      pathPrefix("") {
        pathEndOrSingleSlash {
          getFromResource("swagger-ui/index.html")
        }
      } ~ getFromResourceDirectory("swagger-ui")
    }
  }
}