Recently I ported a small piece of Ruby(on Rails) code into Scala. Nothing fancy, I just want to share with developers who are interested but not yet start coding in functional programming paradigm.
The code is on the server side and the business logic is simple: from the incoming request, read some session information from the cookie; send it to an external service which validates it and returns a username; find the user with the username from the DB and finally return the user information in Json response.
First, the imperative Ruby code:
Action in the controllerdef validate_user user_name = UserSession.new(cookie).validate_username user = User.find_by_username(username) if user_name if user render json: {user: user.to_json } else render json: {error: "Cannot find user"} end rescue => e render json: { error: e.message } end endLibrary class that does the validation
class UserSession def validate_username return nil if [@user,@user_id,@session_key].include?(nil) make_http_request unless @response.nil? && @response.body.nil? json_resp = MultiJson.load(@response.body) if user_json["response"] && json_resp["response"]["auth_status"] == "Success" json_resp["response"]["user_name"] end end end def initialize(cookies) @user_id = cookies["user_id"] @user = cookies["user"] @session_key = cookies["s"] end def make_http_request url = $API_URL + '?t=user&action=vrf' url += '&api_client_token=' + $USERAPI_TOKEN url += "&user_id=#{@user_id}&user=#{@user}&s=#{CGI.escape(@session_key)}" escaped_url = URI.escape(url) uri = URI.parse(escaped_url) http = Net::HTTP.new(uri.host, uri.port) request = Net::HTTP::Get.new(uri.request_uri) @response = http.request(request) end end
It's typical imperative paradigm code that thanks to ruby is reasonably concise and expressive. The process keeps updating a set of mutable states until it gets the final result.
Now let's look at the scala one. There is a difference in functionality that the scala code is asynchronous so that the process is not blocked while waiting for the external service validating the session info. Also the Scala code provides more specific error message for each exception scenario.
Action in controllerdef validateUser = Action.async { implicit request => UserSession.from(request.cookies).map { si => si.validateUsername.right.map(User.findByUsername(_)).map { case Left(errors) => Unauthorized(Json.toJson("error" -> errors)) case Right(Some(u)) => Ok(Json.toJson(u)) case Right(None) => NotFound } }.getOrElse(Future.successful(Unauthorized(Json.toJson("error" -> "Not Session In Cookie"))) }Library class that does the validation.
case class UserSession(userId: String, user: String, sessionKey: String) { lazy val apiUrl = WS.url(API_URL).withQueryString("api_client_token"->API_TOKEN) def validateUsername: Future[Either[ValidationErrors,String]] = { apiUrl.withQueryString( "t" -> "user", "action" -> "vrf_sess", "user_id" -> userId, "user" -> user, "s" -> sessionKey ).get.map { resp => Json.parse(resp.body).validate(resultReads).fold[Either[ValidationErrors, String]]( valid = result => Right(result._2), invalid = errs => Left(errs) ) } } lazy val resultReads: Reads[(String, String)] = (__ \ "response" ).read ( (__ \ "auth_status" ).read[String](equalReads("Success")) ~ (__ \ "username" ).read[String] tupled ) } object UserSession { def from(cookies: Cookies): Option[UserSession] = { for { userId <- cookies.get("user_id") user <- cookies.get("user") sessionKey <- cookies.get("s") } yield UserSession(userId.value, user.value, sessionKey.value) } }
In the functional programming paradigm, in stead of having a set of intermediate mutable states, computation is more often carried forward through a series of data transformation. In the controller, it first uses the SessionInfo.from method to create a SessionInfo out of cookie, then use the validateUsername to transform (map) it into a Future of Either. A Future represents the result of an asynchronous process, it was due the fact that the call to the external service is asynchronous. Either is scala's way to return either the result when all things go well or an error when something is wrong. So in our code, it's an Either between the validated username and the validation errors. The code then transform the right branch of it from a username into an Option of User by querying the DB. Finally, the code used a pattern match to map different possible value of the Either into different types of Http response to be returned to the client.
One of the contrasts between the Scala and Ruby code is how they handle null differently. In Ruby, if statements of null checks are everywhere while in the Scala it's mostly handled using Option. One example is how SessionInfo is generated from cookie. SessionInfo requires all three cookies present. In Ruby this is implemented using the following check.return nil if [@user,@user_id,@session_key].include?(nil)It returns nil as a result if any of the cookies is missing. In Scala, it uses the for syntax sugar to map 3 cookie options into an option of SessionInfo.
def from(cookies: Cookies): Option[UserSession] = { for { userId <- cookies.get("user_id") user <- cookies.get("user") sessionKey <- cookies.get("s") } yield UserSession(userId.value, user.value, sessionKey.value) }cookies.get(key) returns an Option of cookie. If the key exists, it returns a Some(cookie) otherwise it returns a None, the for structure yields a Some(UserSession) only when all these cookie Options presents, a None when any of these cookies is a None. When the controller uses this Option of SessionInfo, it transform it into an Option of a http response to be returned to the client (actually, a Future of the response), and finally calls getOrElse on the Option to get the inner http response out of it. The getOrElse requires a default value when the Option is None, in our case, we used Future.successful(Unauthorized(Json.toJson("error" -> "Not Session In Cookie"))), which is a json response saying the session info is not in the cookie.
Another interesting piece of difference between the two examples is their implementation of the Json validation. In the Ruby code, the validation is performed through several if statements, such as
if user_json["response"] && json_resp["response"]["auth_status"] == "Success"In scala, we created a structured Reads that both read and validate the json response from the external validation service.
val resultReads: Reads[(String, String)] = (__ \ "response" ).read ( (__ \ "auth_status" ).read[String](equalReads("Success")) ~ (__ \ "username" ).read[String] tupled )This Reads presents an expressive way to specify the Json format we expect from the response. The code then use a fold method to handle the regular read results and when external service returned a unexpected format of Json.
Json.parse(resp.body).validate(resultReads).fold[Either[ValidationErrors, String]]( valid = result => Right(result._2), //when reads succeeds invalid = errs => Left(errs) //when the response json doesn't conform to the structure specified in the reads )This reflects the difference between the two philosophies behind Scala and Ruby. Scala leans towards structured statically checked computation while Ruby is more about being fast, flexible and dynamic.
No comments:
Post a Comment