Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala using JSON and/or XML templates

I am using Scalatra, but this question should be valid for any Scala programming. I come from a Ruby on Rails background. Simply put, using templating systems such as XML Builder or jsonbuilder (https://github.com/rails/jbuilder), I had full control on what my JSON or XML output in an RESTful API would be by creating a template such as the following:

Jbuilder.encode do |json|
  json.content format_content(@message.content)
  json.(@message, :created_at, :updated_at)

  json.author do
    json.name @message.creator.name.familiar
    json.email_address @message.creator.email_address_with_name
    json.url url_for(@message.creator, format: :json)
  end

  if current_user.admin?
    json.visitors calculate_visitors(@message)
  end

  json.comments @message.comments, :content, :created_at

  json.attachments @message.attachments do |attachment|
    json.filename attachment.filename
    json.url url_for(attachment)
  end
end

The ideal here is, I put together a @message object with whatever logic is required in a controller+action. That gets passed to a template that has logic such as if current_user.admin? include some stuff, otherwise don't.

What is the equivalent tool that's available in Scala or Scalatra to do something similar? I know a serializer will let me override the JSON or XML that gets generated from a particular model, but that's the same thing in Ruby (correct me if I'm wrong) as overriding as_json or as_xml. Sometimes however, templates are much more complicated and include multiple models, specific structuring of data, specific ordering of data, etc. This is the flexibility I need. Is there current a tool that allows for such templating in a Scala/Scalatra environment?

like image 338
randombits Avatar asked Oct 03 '22 16:10

randombits


1 Answers

unfortunately I don't know any really good library to do this in XML (although Anti-Xml is worth taking a look). But there are such library for Json and it's the PlayJson With play json you can do basically everything you can do with Ruby's JsBuilder, except some paradigm difference:

  1. Scala tries to be as functional as possible and many many libraries operate with unmutable data structures. Play json is not an exception. This means that you cannot just change some value deep in the json tree, you need to reconstruct the whole json object

  2. Scala is a statically typed language. Which is great, because compiler checks all type-signatures for correctness, except we must provide those signatures.

Here's example code:

import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat

case class Attachment(fileName: String, url: String)
case class Author(name: String, email: String, url: String)
case class Comment(content: String, created_at: DateTime)
case class Post(author: Author, content: String, attachments: List[Attachment], comments: List[Comment], created_at: DateTime, updated_at: DateTime)

object Main {

  import play.api.libs.json._
  import play.api.libs.functional.syntax._

  val isAdmin = true

  def calculateVisits(post: Post) = 35

  implicit val jodaTimeWrites: Writes[DateTime] = new Writes[DateTime] {
    def writes(c: DateTime): JsValue = {
      Json.toJson(c.toString(DateTimeFormat.fullDateTime()))
    }
  }
  implicit val attachmentFormat = Json.format[Attachment]

  implicit val authorWrites: Writes[Author] = (
    (__ \ "name").write[String] and
    (__ \ "email").write[String] and
    (__ \ "url").write[String]) { unlift(Author.unapply) }

  implicit val commentWrites: Writes[Comment] = (
    (__ \ "content").write[String] and
    (__ \ "created_at").write[DateTime]) { unlift(Comment.unapply) }

  implicit val postWrites: Writes[Post] = (
    (__ \ "content").write[String] and
    (__ \ "created_at").write[DateTime] and
    (__ \ "updated_at").write[DateTime] and
    (__ \ "author").write[Author] and
    (__ \ "visitors").write[Option[Int]] and
    (__ \ "comments").write[List[Comment]] and
    (__ \ "attachments").write[List[Attachment]]) { post: Post =>
      (
        post.content,
        post.created_at,
        post.updated_at,
        post.author,
        if (isAdmin) Some(calculateVisits(post)) else None,
        post.comments,
        post.attachments)
    }

  def main(args: Array[String]): Unit = {
    val post = Post(
      Author("David H.", "[email protected]", "http://example.com/users/1-david.json"),
      "<p>This is <i>serious</i> monkey business</p>",
      List(
        Attachment("forecast.xls", "http://example.com/downloads/forecast.xls"),
        Attachment("presentation.pdf", "http://example.com/downloads/presentation.pdf")),
      List(
        Comment("Hello everyone!", new DateTime()),
        Comment("To you my good sir!", new DateTime())),
      new DateTime(),
      new DateTime())

    Console println (Json prettyPrint (Json toJson post))
  }

}

Notice the attachmentFormat - it is generated by scala macros to be matched with case class definition. In my project I've never written a single manual Format override compiler generates all formats for me! But I can If I need to. Good example is the jodaTimeWrites - default jodaTimeFormat will generate long value which is more appropriate for machine processing, but I have overridden it with my own implicit format to match ruby's sample.

The code above produces following output:

{
  "content" : "<p>This is <i>serious</i> monkey business</p>",
  "created_at" : "Friday, July 5, 2013 4:19:42 PM +03:00",
  "updated_at" : "Friday, July 5, 2013 4:19:42 PM +03:00",
  "author" : {
    "name" : "David H.",
    "email" : "[email protected]",
    "url" : "http://example.com/users/1-david.json"
  },
  "visitors" : 35,
  "comments" : [ {
    "content" : "Hello everyone!",
    "created_at" : "Friday, July 5, 2013 4:19:42 PM +03:00"
  }, {
    "content" : "To you my good sir!",
    "created_at" : "Friday, July 5, 2013 4:19:42 PM +03:00"
  } ],
  "attachments" : [ {
    "fileName" : "forecast.xls",
    "url" : "http://example.com/downloads/forecast.xls"
  }, {
    "fileName" : "presentation.pdf",
    "url" : "http://example.com/downloads/presentation.pdf"
  } ]
}

Now instead of 21 lines of ruby code I got 33 lines of scala's somehow more complicated mappings (without case classes). Why type more? Because now I'm dead certain that when I pass Comment instead of Attachment I'll get compiler error, or when my colleague will change by mistake joda.time.DateFormat to java.util.DateFormat he'll receive error instead of some gibberish.

like image 116
vitalii Avatar answered Oct 07 '22 19:10

vitalii