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

skinny.controller.SkinnyResource.scala Maven / Gradle / Ivy

The newest version!
package skinny.controller

import skinny._
import skinny.ParamType
import skinny.validator.{ NewValidation, MapValidator }
import skinny.exception.StrongParametersException
import java.util.Locale
import skinny.controller.feature.RequestScopeFeature

/**
 * Skinny resource is a DRY module to implement ROA(Resource-oriented architecture) apps.
 *
 * SkinnyResource is surely inspired by Rails ActiveSupport.
 */
trait SkinnyResource extends SkinnyResourceActions with SkinnyResourceRoutes

/**
 * Actions for Skinny resource.
 */
trait SkinnyResourceActions extends SkinnyController {

  /**
   * SkinnyModel for this resource.
   */
  protected def model: SkinnyModel[_]

  /**
   * Resource name in the plural. This name will be used for path and directory name to locale template files.
   */
  protected def resourcesName: String

  /**
   * Resource name in the singular. This name will be used for path and validator's prefix.
   */
  protected def resourceName: String

  override protected def xmlRootName = resourcesName

  override protected def xmlItemName = resourceName

  /**
   * Creates validator with prefix(resourceName).
   *
   * @param params params
   * @param validations validations
   * @param locale current locale
   * @return validator
   */
  override def validation(params: Params, validations: NewValidation*)(implicit locale: Locale = currentLocale.orNull[Locale]): MapValidator = {
    validationWithPrefix(params, resourceName, validations: _*)
  }

  /**
   * Use relative path if true. This is set as false by default.
   *
   * If you set this as true, routing will become simpler but /{resources}.xml or /{resources}.json don't work.
   */
  protected def useRelativePath: Boolean = false

  /**
   * Base path prefix. (e.g. /admin/{resourcesName} )
   */
  protected def basePathPrefix: String = ""

  /**
   * Base path.
   */
  protected def basePath: String = basePathPrefix + (if (useRelativePath) "" else s"/${resourcesName}")

  /**
   * Outputs debug logging for passed parameters.
   *
   * @param form input form
   * @param id id if exists
   */
  protected def debugLoggingParameters(form: MapValidator, id: Option[Long] = None) = {
    val forId = id.map { id => s" for [id -> ${id}]" }.getOrElse("")
    val params = form.paramMap.map { case (name, value) => s"${name} -> '${value}'" }.mkString("[", ", ", "]")
    logger.debug(s"Parameters${forId}: ${params}")
  }

  /**
   * Outputs debug logging for permitted parameters.
   *
   * @param parameters permitted strong parameters
   * @param id id if exists
   */
  protected def debugLoggingPermittedParameters(parameters: PermittedStrongParameters, id: Option[Long] = None) = {
    val forId = id.map { id => s" for [id -> ${id}]" }.getOrElse("")
    val params = parameters.params.map { case (name, (v, t)) => s"${name} -> '${v}' as ${t}" }.mkString("[", ", ", "]")
    logger.debug(s"Permitted parameters${forId}: ${params}")
  }

  // ----------------------------
  //  Actions for this resource
  // ----------------------------

  /**
   * Shows a list of resource.
   *
   * GET /{resources}/
   * GET /{resources}
   * GET /{resources}.xml
   * GET /{resources}.json
   *
   * @param format format
   * @return list of resource
   */
  def showResources()(implicit format: Format = Format.HTML): Any = withFormat(format) {
    set(resourcesName, model.findAllModels())
    render(s"/${resourcesName}/index")
  }

  /**
   * Show single resource.
   *
   * GET /{resources}/{id}
   * GET /{resources}/{id}.xml
   * GET /{resources}/{id}.json
   *
   * @param id id
   * @param format format
   * @return single resource
   */
  def showResource(id: Long)(implicit format: Format = Format.HTML): Any = withFormat(format) {
    set(resourceName, model.findModel(id).getOrElse(haltWithBody(404)))
    render(s"/${resourcesName}/show")
  }

  /**
   * Shows input form for creation.
   *
   * GET /{resources}/new
   *
   * @param format format
   * @return input form
   */
  def newResource()(implicit format: Format = Format.HTML): Any = withFormat(format) {
    render(s"/${resourcesName}/new")
  }

  /**
   * Input form for creation
   */
  protected def createForm: MapValidator

  /**
   * Params for creation.
   */
  protected def createParams: Params = Params(params)

  /**
   * Strong parameter definitions for creation form
   */
  protected def createFormStrongParameters: Seq[(String, ParamType)]

  /**
   * Executes resource creation.
   *
   * @param parameters permitted parameters
   * @return generated resource id
   */
  protected def doCreateAndReturnId(parameters: PermittedStrongParameters): Long = {
    debugLoggingPermittedParameters(parameters)
    model.createNewModel(parameters)
  }

  /**
   * Set notice flash message for successful completion of creation.
   */
  protected def setCreateCompletionFlash(): Unit = {
    flash += ("notice" -> createI18n().get(s"${resourceName}.flash.created").getOrElse(s"The ${resourceName} was created."))
  }

  /**
   * Creates new resource.
   *
   * POST /{resources}
   *
   * @param format format
   * @return created response if success
   */
  def createResource()(implicit format: Format = Format.HTML): Any = withFormat(format) {
    debugLoggingParameters(createForm)
    if (createForm.validate()) {
      val id: Long = if (!createFormStrongParameters.isEmpty) {
        val parameters = createParams.permit(createFormStrongParameters: _*)
        doCreateAndReturnId(parameters)
      } else {
        throw new StrongParametersException(
          "'createFormStrongParameters' or 'createFormTypedStrongParameters' must be defined.")
      }
      format match {
        case Format.HTML =>
          setCreateCompletionFlash()
          redirect(s"/${resourcesName}/${id}")
        case _ =>
          status = 201
          response.setHeader("Location", s"${contextPath}/${resourcesName}/${id}")
      }
    } else {
      status = 400
      render(s"/${resourcesName}/new")
    }
  }

  /**
   * Shows input form for modification.
   *
   * GET /{resources}/{id}/edit
   *
   * @param id id
   * @param format format
   * @return input form
   */
  def editResource(id: Long)(implicit format: Format = Format.HTML): Any = withFormat(format) {
    model.findModel(id).map {
      m =>
        status = 200
        format match {
          case Format.HTML =>
            setAsParams(m)
            render(s"/${resourcesName}/edit")
          case _ =>
        }
    } getOrElse haltWithBody(404)
  }

  /**
   * Input form for modification
   */
  protected def updateForm: MapValidator

  /**
   * Params for modification.
   */
  protected def updateParams: Params = Params(params)

  /**
   * Strong parameter definitions for mofidication form
   */
  protected def updateFormStrongParameters: Seq[(String, ParamType)]

  /**
   * Executes modification for the specified resource.
   *
   * @param id  id
   * @param parameters permitted parameters
   * @return count
   */
  protected def doUpdate(id: Long, parameters: PermittedStrongParameters): Int = {
    debugLoggingPermittedParameters(parameters, Some(id))
    model.updateModelById(id, parameters)
  }

  /**
   * Set notice flash message for successful completion of modification.
   */
  protected def setUpdateCompletionFlash() = {
    flash += ("notice" -> createI18n().get(s"${resourceName}.flash.updated").getOrElse(s"The ${resourceName} was updated."))
  }

  /**
   * Updates the specified single resource.
   *
   * PUT /{resources}/{id}
   *
   * @param id id
   * @param format format
   * @return result
   */
  def updateResource(id: Long)(implicit format: Format = Format.HTML): Any = withFormat(format) {
    debugLoggingParameters(updateForm, Some(id))

    model.findModel(id).map { m =>
      if (updateForm.validate()) {
        if (!updateFormStrongParameters.isEmpty) {
          val parameters = updateParams.permit(updateFormStrongParameters: _*)
          doUpdate(id, parameters)
        } else {
          throw new StrongParametersException(
            "'updateFormStrongParameters' or 'updateFormTypedStrongParameters' must be defined.")
        }
        status = 200
        format match {
          case Format.HTML =>
            setUpdateCompletionFlash()
            set(resourceName, model.findModel(id).getOrElse(haltWithBody(404)))
            render(s"/${resourcesName}/show")
          case _ =>
        }
      } else {
        status = 400
        render(s"/${resourcesName}/edit")
      }
    } getOrElse haltWithBody(404)
  }

  /**
   * Executes deletion of the specified single resource.
   *
   * @param id id
   * @return count
   */
  protected def doDestroy(id: Long): Int = {
    model.deleteModelById(id)
  }

  /**
   * Set notice flash message for successful completion of deletion.
   */
  protected def setDestroyCompletionFlash() = {
    flash += ("notice" -> createI18n().get(s"${resourceName}.flash.deleted").getOrElse(s"The ${resourceName} was deleted."))
  }

  /**
   * Destroys the specified resource.
   *
   * DELETE /{resources}/{id}
   *
   * @param id id
   * @param format format
   * @return result
   */
  def destroyResource(id: Long)(implicit format: Format = Format.HTML): Any = withFormat(format) {
    model.findModel(id).map { m =>
      doDestroy(id)
      setDestroyCompletionFlash()
      status = 200
    } getOrElse haltWithBody(404)
  }

}

/**
 * Routings for Skinny resource.
 */
trait SkinnyResourceRoutes extends Routes { self: SkinnyResourceActions =>

  // ------------------
  // Routing
  // ------------------

  /**
   * Pass this controller instance implicitly
   * because [[skinny.routing.implicits.RoutesAsImplicits]] expects [[skinny.controller.SkinnyControllerBase]].
   */
  private[this] implicit val skinnyController: SkinnyController = this

  // set resourceName/resourcesName to the request scope
  before() {
    set(RequestScopeFeature.ATTR_RESOURCE_NAME -> resourceName)
    set(RequestScopeFeature.ATTR_RESOURCES_NAME -> resourcesName)
  }

  // --------------
  // create

  // should be defined in front of 'show
  get(s"${basePath}/new")(newResource).as('new)

  post(s"${basePath}/?")(createResource).as('create)

  // --------------
  // show

  get(s"${basePath}/?")(showResources()).as('index)

  get(s"${basePath}.:ext") {
    (for {
      ext <- params.get("ext")
    } yield {
      ext match {
        case "json" => showResources()(Format.JSON)
        case "xml" => showResources()(Format.XML)
        case _ => haltWithBody(404)
      }
    }) getOrElse haltWithBody(404)
  }.as('index)

  get(s"${basePath}/:id") {
    if (params.getAs[String]("id").exists(_ == "new")) {
      newResource()
    } else {
      params.getAs[Long]("id").map { id => showResource(id) } getOrElse haltWithBody(404)
    }
  }.as('show)

  get(s"${basePath}/:id.:ext") {
    (for {
      id <- params.getAs[Long]("id")
      ext <- params.get("ext")
    } yield {
      ext match {
        case "json" => showResource(id)(Format.JSON)
        case "xml" => showResource(id)(Format.XML)
        case _ => haltWithBody(404)
      }
    }) getOrElse haltWithBody(404)
  }.as('show)

  // --------------
  // update

  get(s"${basePath}/:id/edit") {
    params.getAs[Long]("id").map(id => editResource(id)) getOrElse haltWithBody(404)
  }.as('edit)

  post(s"${basePath}/:id") {
    params.getAs[Long]("id").map(id => updateResource(id)) getOrElse haltWithBody(404)
  }.as('update)

  put(s"${basePath}/:id") {
    params.getAs[Long]("id").map(id => updateResource(id)) getOrElse haltWithBody(404)
  }.as('update)

  patch(s"${basePath}/:id") {
    params.getAs[Long]("id").map(id => updateResource(id)) getOrElse haltWithBody(404)
  }.as('update)

  // --------------
  // delete

  delete(s"${basePath}/:id") {
    params.getAs[Long]("id").map(id => destroyResource(id)) getOrElse haltWithBody(404)
  }.as('destroy)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy