Search code examples
scalaakkaakka-http

Custom directive in Akka Http


I have some API Rest with CRUD operations. Each entity is identified by using UUID.

For example for Create it is similar to this:

  private val createProduct = post {
    path("product" / Segment) { productUUID =>
        Try(UUID.fromString(productUUID)) match {
          case Failure(_) => // Bad request
          case Success(validUuid) =>
            entity(as[ProductData]) { productData =>
              onComplete(addProduct(validUuid, productData)) {
                case Success(_)  => complete(StatusCodes.OK)
                case Failure(ex) => // some code 
              }
            }
        }
    }
  }

Operations Read(GET), Update(PUT) and Delete(DELETE) are similar to POST:

  • first step is get the uuid
  • for PUT get the payload using entity(as[ProductData]) (not needed for GET and DELETE)
  • invoke some method that returns Future[Something]

What I would like to do is remove boilerplate like:

  • getting the UUID, validating it, returning Bad Request if it's not valid
  • create some directive/function for handling the future: if there's an exception just return 500 Internal Server Error, but in case of Success continue for processing the value (I think using provide directive).

I found this example (https://fmsbeekmans.com/blog/akka-http-2-creating-custom-directives.html):

def simplifiedOnComplete[T](future: Future[T])(timeout: FiniteDuration): Directive1[T] = {
  Try(Await.result(future, Duration.Inf)) match {
    case Success(result) => provide(result)
    case Failure(error) => failWith(error)
  }
}

I said, ok, there's a try in this example! Maybe I can change it for working with UUID instead of Future:

  def getUUID[T]: Directive1[T] = {
    path(Segment) { maybeuuid =>
      Try(UUID.fromString(maybeuuid)) match {
        case Success(result) => provide(result) // compilation error here
        case Failure(error)  => failWith(error)
      }
    }
  }

The code does not compile with the error: Type mismatch. Required: Route, found: Directive1[UUID] I guess the problem is I've added path ...

How can I create a Directive to extract the valid uuid and return Bad Request if it's not valid? And, is it possible to encapsulate in a custom directive the code that handles de future?

For example the routes defined at the top would be something like:

  private val createProduct = post {
     path ("product") {
        extractUUID { validUUID =>
           entity(as[ProductData]) { productData => 
              futureSuccess(addProduct(validUUID, productData)) { successValue =>
                 // code here, for example: complete(OK)
              }
           }
        }
     }
  }


Solution

  • You were almost there - Your code is :

      def getUUID[T]: Directive1[T] = {
        path(Segment) { maybeuuid =>
          Try(UUID.fromString(maybeuuid)) match {
            case Success(result) => provide(result) // compilation error here
            case Failure(error)  => failWith(error)
          }
        }
      }
    
    1. You don't need generic T since you can only have a UUID back from UUID.fromString
    2. path(Segment) gives you a Directive. So you want to useflatMap to get a Directive back (provide returns a Directive)

    So it would be something like

      def getUUID: Directive1[UUID] = {
        path(Segment).flatMap { maybeuuid =>
          Try(UUID.fromString(maybeuuid)) match {
            case Success(result) => provide(result) 
            case Failure(error)  => failWith(error)
          }
        }
      }
    

    And, is it possible to encapsulate in a custom directive the code that handles de future?

    Yes, it is the same as the above. onComplete returns a Directive so you will have to flatMap.

    To return BadRequest, look up the rejection handlers in the akka-http doc.