The Good, The Bad and The Ugly
or
Musings of a frustrated Rails dev
Model View Controller
Invented by Microsoft in 2009
Neater dialect of HAML, a DRY whitespace significant markup language
Path | action | used for | |
---|---|---|---|
GET | /photos | index | list of all photos |
GET | /photos/new | new | return a form for creating a new photo |
POST | /photos | create | create a new photo |
GET | /photos/:id | show | display a specific photo |
GET | /photos/:id/edit | edit | return a form for editing a photo |
PUT | /photos/:id | update | update a specific photo |
DELETE | /photos/:id | destroy | delete a specific photo |
play new my_blog
play
Request => Request => Result
GET / controllers.Posts.index
GET /posts controllers.Posts.index
GET /posts/new controllers.Posts.newPost
GET /posts/:id controllers.Posts.show(id: ObjectId)
POST /posts controllers.Posts.create
GET /posts/:id/edit controllers.Posts.edit(id: ObjectId)
POST /posts/:id/update controllers.Posts.update(id: ObjectId)
DELETE /posts/:id controllers.Posts.destroy(id: ObjectId)
... and we're not even up to comments or users yet
routes.Posts.index()
BlogApplication::Application.routes.draw do
resources :users
resources :posts do
resources :comments
end
root to: 'posts#index'
end
Convention based, DRY, unsafe less safe and driven by voodoo "idiomatic" Ruby
Out of the box is crucial, it sets the tone for the developer community.
Request => Result
object Posts extends ApplicationController {
def index = Action { implicit request =>
val posts = Post.findAll().map { p =>
p.copy(author = User.dao.findOneById(p.userId))
}
Ok(views.html.posts.index(posts))
}
}
Hello n+1 Queries
def apply(block: Request[AnyContent] => Result)
Authenticated
def Authenticated(f:AuthenticatedRequest => Result) : Action[AnyContent] = {
Action { request =>
request.session.get("username").flatMap { username =>
User.findOneByUsername(username).map { user =>
f(AuthenticatedRequest(user, request))
}
}.getOrElse {
Redirect(routes.Session.newSession()).flashing("success" -> "You need to login.")
}
}
}
@(posts: Iterator[Post])(implicit request: AuthenticatedRequest)
@application("Posts index", Some(request.user)) {
<h1>Posts</h1>
<div class="posts">
@for(p <- posts) {
<article class="post">
<h4><a href="@routes.Posts.show(p.id)">@p.title</a> (by @p.author.get.username)</h4>
</article>
}
</div>
}
Ok(views.html.posts.index(posts))
def render(path: String,
attributeMap: Map[String, Any] = Map()): Unit
Jade + statically typed view interface
or
The bane of statically typed web frameworks
The controller side: parsing query string/POST in to something useful
case class UserSession(username: String, password:String)
val loginForm = Form(
mapping(
"username" -> text(minLength = 4),
"password" -> text(minLength = 4)
) (UserSession.apply) (UserSession.unapply).verifying(
"Invalid username or password",
u => User.findWithUsernameAndPassword(u.username, u.password).isDefined
)
)
case class Form[T](mapping: Mapping[T],
data: Map[String, String],
errors: Seq[FormError],
value: Option[T])
All the action happens in the mapping. I'd show you the type signatures but ...
def mapping[R, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18](a1: (String, Mapping[A1]), a2: (String, Mapping[A2]), a3: (String, Mapping[A3]), a4: (String, Mapping[A4]), a5: (String, Mapping[A5]), a6: (String, Mapping[A6]), a7: (String, Mapping[A7]), a8: (String, Mapping[A8]), a9: (String, Mapping[A9]), a10: (String, Mapping[A10]), a11: (String, Mapping[A11]), a12: (String, Mapping[A12]), a13: (String, Mapping[A13]), a14: (String, Mapping[A14]), a15: (String, Mapping[A15]), a16: (String, Mapping[A16]), a17: (String, Mapping[A17]), a18: (String, Mapping[A18]))(apply: Function18[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, R])(unapply: Function1[R, Option[(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18)]]): Mapping[R] = {
ObjectMapping18(apply, unapply, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18)
}
def bind(data: Map[String, String]): Form[T]
def bindFromRequest()(implicit request: play.api.mvc.Request[_]): Form[T]
def fill(value: T)
def fold[R](hasErrors: Form[T] => R, success: T => R): R
???
Is it lenses?
MongoDB: Database with JSON documents instead of rows
Salat: Immutable Case Class only ORM for MongoDB
case class User(id: ObjectId = new ObjectId,
username: String,
password: String,
added: Date = new Date(),
updated: Option[Date] = None,
deleted: Option[Date] = None)
object User extends ModelCompanion[User, ObjectId] {
val collection = mongoCollection("users")
val dao = new SalatDAO[User, ObjectId](collection = collection) {}
def findWithUsernameAndPassword(username:String,
password:String) :Option[User] = {
dao.findOne(MongoDBObject("username" -> username, "password" -> password))
}
}
val steve = User(username = "steve",
password = "please")
User.save(steve)
Post.dao.findOneById(id).map { post =>
val updatedPost = post.copy(title = value.title,
content = value.content)
Post.save(updatedPost)
}
Simple & Immutable
> db.posts.findOne();
{
"_id" : ObjectId("5020efd803646a16c93b2f9a"),
"userId" : ObjectId("5020efd803646a16c93b2f99"),
"title" : "New Apple Products",
"content" : "content for New Apple Product",
"tags" : [ ],
"comments" : [
{
"userId" : ObjectId("5020efd803646a16c93b2f99"),
"content" : "I love this product."
}
]
}
The Good, The Bad and The Ugly The Vanilla?