Last active
April 18, 2021 02:29
-
-
Save gakuzzzz/4977d57e33bf24edf7e1 to your computer and use it in GitHub Desktop.
Play2 Controller Utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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