Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active April 18, 2021 02:29
Show Gist options
  • Save gakuzzzz/4977d57e33bf24edf7e1 to your computer and use it in GitHub Desktop.
Save gakuzzzz/4977d57e33bf24edf7e1 to your computer and use it in GitHub Desktop.
Play2 Controller Utilities
package controllers.util
import play.api.mvc.{Result, Controller}
import play.api.data.Form
import scala.util.Either.RightProjection
object Implicits {
implicit def formToEither[A](form: Form[A]): Either[Form[A], A] = form.fold(Left.apply, Right.apply)
implicit def eitherToResult(e: Either[Result, Result]): Result = e.merge
implicit class OptionOps[A](val value: Option[A]) extends AnyVal {
def V(left: => Result): RightProjection[Result, A] = value.toRight(left).right
}
implicit class FormOps[A](val value: Form[A]) extends AnyVal {
def V(f: Form[A] => Result): RightProjection[Result, A] = value.left.map(f).right
def V(left: => Result): RightProjection[Result, A] = value.left.map(_ => left).right
}
implicit class BooleanOps(val value: Boolean) extends AnyVal {
def V(left: => Result): RightProjection[Result, Unit] = Either.cond(value, (), left).right
}
implicit class EitherOps[A, B](val value: Either[A, B]) extends AnyVal {
def V(f: A => Result): RightProjection[Result, B] = value.left.map(f).right
def V(left: => Result): RightProjection[Result, B] = value.left.map(_ => left).right
}
}
package controllers
import play.api.mvc.{Action, Controller}
import _root_.controllers.util.Implicits._
import play.api.data.Form
import play.api.data.Forms._
import play.api.templates.Html
trait FooController extends Controller {
// ======================================================================
// 詳細ページ
// ======================================================================
// 独自Utilを使った実装
def show(id: FooId) = Action { implicit req =>
for {
foo <- fooService.findById(id) V NotFound
} yield {
Ok(html.foo.show(foo))
}
}
// 標準のメソッドのみで実装
def show2(id: FooId) = Action { implicit req =>
fooService.findById(id) map { foo =>
Ok(html.foo.show(foo))
} getOrElse {
NotFound
}
}
// ======================================================================
// 編集処理
// ======================================================================
// 独自Utilを使った実装
def update(id: FooId) = Action { implicit req =>
for {
oldFoo <- fooService.findById(id) V NotFound
newFoo <- fooForm.bindFromRequest() V (withError => BadRequest(html.foo.edit(withError)))
_ <- !oldFoo.isClosed V BadRequest(html.foo.edit(fooForm.withGlobalError("既に終了しています。")))
_ <- fooService.update(newFoo) V Conflict("更新が衝突しました。")
} yield Redirect(routes.FooController.show(id)).flashing("success" -> "成功しました")
}
// 標準のメソッドのみで実装
def update2(id: FooId) = Action { implicit req =>
fooService.findById(id) map { oldFoo =>
fooForm.bindFromRequest().fold(
withError => BadRequest(html.foo.edit(withError)),
newFoo =>
if (oldFoo.isClosed) {
BadRequest(html.foo.edit(fooForm.withGlobalError("既に終了しています。")))
} else {
val result = fooService.update(foo) // 本質的な処理がうもれる
if (result) {
Redirect(routes.FooController.show(id)).flashing("success" -> "成功しました")
} else {
Conflict("更新が衝突しました。")
}
}
)
} getOrElse {
NotFound // エラーとそのエラーの原因となる処理が離れすぎている
}
}
// 標準のメソッドのみで実装2
def update3(id: FooId) = Action { implicit req =>
(for {
oldFoo <- fooService.findById(id) .toRight( NotFound ).right
newFoo <- fooForm.bindFromRequest() .fold( withError => Left(BadRequest(html.foo.edit(withError))), Right.apply).right
_ <- Either.cond(!oldFoo.isClosed, (), BadRequest(html.foo.edit(fooForm.withGlobalError("既に終了しています。"))) ).right
_ <- Either.cond(fooService.update(newFoo), (), Conflict("更新が衝突しました。") ).right
} yield Redirect(routes.FooController.show(id)).flashing("success" -> "成功しました")).merge
}
// 例外で実装3
// 都合上、各Serviceのメソッドのシグネチャが例外を投げるように変わっている想定です。
def update4(id: FooId) = Action { implicit req =>
try {
val oldFoo = fooService.findById(id)
fooForm.bindFromRequest().fold(
withError => Left(BadRequest(html.foo.edit(withError))),
newFoo => {fooService.update(foo); Redirect(routes.FooController.show(id)).flashing("success" -> "成功しました")}
)
} catch {
case _: NoSuchElementException => NotFound
case _: IllegalStateException => BadRequest(html.foo.edit(fooForm.withGlobalError("既に終了しています。")))
case _: OptimisticLockException => Conflict("更新が衝突しました。")
}
}
// 標準のメソッドのみで実装3
def update5(id: FooId) = Action { implicit req =>
fooService.findById(id).fold(NotFound: Result) { oldFoo => // 型推論が微妙
fooForm.bindFromRequest().fold(
withError => BadRequest(html.foo.edit(withError)),
newFoo =>
if (oldFoo.isClosed) {
BadRequest(html.foo.edit(fooForm.withGlobalError("既に終了しています。")))
} else {
val result = fooService.update(foo) // 本質的な処理がうもれる
if (result) {
Redirect(routes.FooController.show(id)).flashing("success" -> "成功しました")
} else {
Conflict("更新が衝突しました。")
}
}
)
}
}
// 以下コンパイル通す為のおまけ
val fooForm = Form {
mapping(
"id" -> number,
"name" -> nonEmptyText,
"description" -> text,
"isClosed" -> boolean,
"version" -> number
)(Foo.apply)(Foo.unapply)
}
val fooService: FooService = ???
type FooId = Int
type Name = String
type Description = String
object html {
object foo {
def show(foo: Foo): Html = ???
def edit(form: Form[Foo]): Html = ???
}
}
}
object FooController extends FooController
case class Foo(id: Int, name: String, description: String, isClosed: Boolean, version: Int)
trait FooService {
def findById(id: FooId): Option[Foo]
def update(foo: Foo): Boolean
type FooId = Int
type Name = String
type Description = String
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment