All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.coursera.naptime.router2.NaptimePlayRouter.scala Maven / Gradle / Ivy

There is a newer version: 0.9.0-alpha5
Show newest version
/*
 * Copyright 2016 Coursera Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.coursera.naptime.router2

import javax.inject.Inject
import javax.inject.Singleton

import com.google.inject.Injector
import com.netflix.governator.annotations.WarmUp
import com.typesafe.scalalogging.StrictLogging
import org.coursera.naptime.schema.HandlerKind
import org.coursera.naptime.schema.ResourceKind
import org.coursera.naptime.schema
import play.api.mvc.Handler
import play.api.mvc.RequestHeader
import play.api.routing
import play.api.routing.Router.Routes

import scala.annotation.tailrec
import scala.collection.immutable

/**
 * Handles routing for Naptime resources in an idiomatic fashion for Play projects.
 *
 * To use this router, include in your routes file something like:
 * {{{
 *   # Include Naptime resources
 *   ->    /api                  org.coursera.naptime.router2.PlayNaptimeRouter
 * }}}
 *
 * Requests matching the prefix for naptime resources will then be routed appropriately.
 *
 * @param injector The Injector from which to instantiate all the naptime resources.
 * @param routerBuilders The set of naptime macro resource router builders corresponding to the
 *                       collection of resources to serve. These are typically bound via Guice
 *                       multi-bindings.
 * @param prefix The prefix path under which the resources should be served (in the example above:
 *               `/api`).
 */
@Singleton
case class NaptimePlayRouter(
    private[naptime] val injector: Injector,
    private[naptime] val routerBuilders: immutable.Set[ResourceRouterBuilder],
    private[naptime] val prefix: String) extends play.api.routing.Router with StrictLogging {

  /**
   * Helper function to filter out those resource router builders that don't provide schemas.
   */
  private[this] def hasDefinedSchema(routerBuilder: ResourceRouterBuilder): Boolean = {
    try {
      routerBuilder.schema
      true
    } catch {
      case e: scala.NotImplementedError =>
        false
    }
  }

  private[this] lazy val schemaMap = routerBuilders.filter(hasDefinedSchema)
    .toList.map { routerBuilder =>
      // Scala nests classes sometimes using a `$` instead of a `.`, but the JVM does not. :-)
      routerBuilder.resourceClass().getName.replace("$", ".") -> routerBuilder.schema
    }.toMap

  // TODO(saeta): Check to ensure there are not 2 resources with the same name / version.
  private[this] lazy val routers = routerBuilders.map { routerBuilder =>
    val resourceClass = routerBuilder.resourceClass()
    routerBuilder.build(injector.getInstance(resourceClass))
  }.toList

  @Inject
  def this(injector: Injector, routerBuilders: immutable.Set[ResourceRouterBuilder]) =
    this(injector, routerBuilders, "")

  /**
   * Defer to [[handlerFor]] instead of the other way around for performance reasons.
   *
   * It is better to have the true implementation in `handlerFor` where we can route a request once
   * than to implement the partial function here, and have handlerFor call [[isDefinedAt]] and then
   * [[apply]], which would result in request routing and URL parsing occuring twice for a single
   * request when it wouldn't need to.
   */
  override lazy val routes: Routes = Function.unlift(handlerFor)

  override def withPrefix(prefix: String): routing.Router = copy(prefix = prefix)

  /**
   * Includes the Naptime resources into Play's dev mode not-found handler that lists all routes.
   */
  override lazy val documentation: Seq[(String, String, String)] = {
    def handlerKindToHttpMethod(kind: HandlerKind): String = {
      kind match {
        case HandlerKind.GET | HandlerKind.MULTI_GET | HandlerKind.GET_ALL | HandlerKind.FINDER =>
          "GET"
        case HandlerKind.CREATE | HandlerKind.ACTION =>
          "POST"
        case HandlerKind.UPSERT =>
          "PUT"
        case HandlerKind.DELETE =>
          "DELETE"
        case HandlerKind.PATCH =>
          "PATCH"
        case HandlerKind.$UNKNOWN =>
          kind.toString
      }
    }

    def shouldUsePathWithKey(kind: HandlerKind): Boolean = {
      kind match {
        case HandlerKind.GET | HandlerKind.UPSERT | HandlerKind.DELETE | HandlerKind.PATCH =>
          true
        case HandlerKind.CREATE | HandlerKind.MULTI_GET | HandlerKind.GET_ALL | HandlerKind.FINDER |
          HandlerKind.ACTION =>
          false
      }
    }

    /**
      * Computes the URL path to access the resource.
     *
     * @param resourceSchema The resource to compute.
      * @return (PathWithoutKeys, PathWithKeySuffix)
      */
    def computeFullPath(resourceSchema: schema.Resource): (String, String) = {
      val previous = if (resourceSchema.parentClass.isDefined) {
        try {
          computeFullPath(schemaMap(resourceSchema.parentClass.get))._2
        } catch {
          case e: NoSuchElementException =>
            logger.error(s"Problem computing schema for resource ${resourceSchema.className}. " +
              s"Parent class ${resourceSchema.parentClass} not found in schema map keys: " +
              s"${schemaMap.keys}", e)
            prefix
        }
      } else {
        prefix
      }
      val resourceName = if (resourceSchema.parentClass.isDefined) {
        s"${resourceSchema.name}"
      } else {
        s"${resourceSchema.name}.v${resourceSchema.version.getOrElse("???")}"
      }
      val path = s"$previous/$resourceName"
      resourceSchema.kind match {
        case ResourceKind.SINGLETON =>
          path -> path
        case ResourceKind.COLLECTION =>
          path -> s"$path/$$id" // TODO: add key type information
      }
    }

    for {
      routerBuilder <- routerBuilders.toList
      if hasDefinedSchema(routerBuilder)
      (path, pathWithKey) = computeFullPath(routerBuilder.schema)
      handler <- routerBuilder.schema.handlers.sortBy(h => handlerOrder(h.kind))
    } yield {
      val method = s"${handlerKindToHttpMethod(handler.kind)} --- ${handler.kind}"
      val documentationPath = if (shouldUsePathWithKey(handler.kind)) {
        pathWithKey
      } else {
        if (handler.kind == HandlerKind.FINDER) {
          s"$path?q=${handler.name}"
        } else if (handler.kind == HandlerKind.ACTION) {
          s"$path?action=${handler.name}"
        } else {
          path
        }
      }
      val queryParamInfo = for {
        param <- handler.parameters
      } yield {
        val default = Option(param.data().get("default"))
        default.map { default =>
          s"${param.name}: ${param.`type`} = $default"
        }.getOrElse {
          s"${param.name}: ${param.`type`}"
        }
      }
      val baseMethod = s"[NAPTIME] ${routerBuilder.schema.className}.${handler.name}"
      val scalaMethod = if (queryParamInfo.isEmpty) {
        baseMethod
      } else {
        s"$baseMethod${queryParamInfo.mkString("(", ", ", ")")}"
      }
      (method, documentationPath, scalaMethod)
    }
  }

  private[this] val handlerOrder = Seq[HandlerKind](
    HandlerKind.GET,
    HandlerKind.MULTI_GET,
    HandlerKind.GET_ALL,
    HandlerKind.CREATE,
    HandlerKind.UPSERT,
    HandlerKind.DELETE,
    HandlerKind.PATCH,
    HandlerKind.FINDER,
    HandlerKind.ACTION).zipWithIndex.toMap

  /**
   * Route the request to one of the naptime resources, invoking the (macro-generated) router.
   *
   * @param request The request to route.
   * @return If this is a naptime request for one of the routers, return the handler, otherwise None
   */
  override def handlerFor(request: RequestHeader): Option[Handler] = {
    if (request.path.startsWith(prefix)) {
      val path = request.path.substring(prefix.length)

      /**
       * Helper method to find the first resource that matches the request and route appropriately.
       *
       * @param resourceRouters The registered resource routers.
       * @param requestHeader The request to route. (It's a parameter to avoid closure allocation.)
       * @return Some(RouteAction) if the request corresponds to a registered resource, else None
       */
      @tailrec
      def routeRequestHelper(
        resourceRouters: immutable.Seq[ResourceRouter],
        requestHeader: RequestHeader,
        path: String): Option[RouteAction] = {
        if (resourceRouters.isEmpty) {
          None
        } else {
          val first = resourceRouters.head
          val firstResult = first.routeRequest(path, requestHeader)
          if (firstResult.isDefined) {
            firstResult
          } else {
            routeRequestHelper(resourceRouters.tail, requestHeader, path)
          }
        }
      }

      routeRequestHelper(routers, request, path)
    } else {
      None
    }
  }

  /**
   * Forces the initialization of the internals of the router.
   *
   * Note: in order to support Naptime's use without governator, everything must work without
   * relying on governator calling this function. If used without governator, everything must be
   * initialized upon object construction or on the first request.
   */
  @WarmUp
  private[this] def warmUp(): Unit = {
    routers
    schemaMap
    documentation
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy