【问题标题】:Scala using JSON and/or XML templatesScala 使用 JSON 和/或 XML 模板
【发布时间】:2013-06-25 09:55:30
【问题描述】:

我正在使用 Scalatra,但这个问题应该适用于任何 Scala 编程。我来自 Ruby on Rails 背景。简而言之,使用 XML Builder 或 jsonbuilder (https://github.com/rails/jbuilder) 等模板系统,我可以通过创建如下模板来完全控制我在 RESTful API 中的 JSON 或 XML 输出:

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

这里的理想是,我将一个@message 对象与控制器+动作中所需的任何逻辑放在一起。这被传递给具有诸如if current_user.admin? 之类的逻辑的模板,包括一些东西,否则不要。

在 Scala 或 Scalatra 中可用于执行类似操作的等效工具是什么?我知道serializer 会让我覆盖从特定模型生成的 JSON 或 XML,但这在 Ruby 中与覆盖 as_jsonas_xml 是一样的(如果我错了,请纠正我)。然而,有时模板要复杂得多,包括多个模型、特定的数据结构、特定的数据排序等。这就是我需要的灵活性。当前是否有允许在 Scala/Scalatra 环境中进行此类模板化的工具?

【问题讨论】:

    标签: xml json scala template-engine scalatra


    【解决方案1】:

    不幸的是,我不知道有什么真正好的库可以在 XML 中执行此操作(尽管 Anti-Xml 值得一看)。但是有这样的 Json 库,它是 PlayJson 有了 play json,你基本上可以做所有你可以用 Ruby 的 JsBuilder 做的事情,除了一些范式差异:

    1. Scala 试图尽可能地发挥功能,许多库都使用不可变的数据结构进行操作。玩json也不例外。这意味着您不能仅更改 json 树深处的某些值,您需要重建整个 json 对象

    2. Scala 是一种静态类型语言。这很好,因为编译器检查所​​有类型签名的正确性,除了我们必须提供这些签名。

    下面是示例代码:

    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.", "david@heinemeierhansson.com", "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))
      }
    
    }
    

    注意attachmentFormat - 它是由 scala 宏生成的,用于匹配案例类定义。在我的项目中,我从未编写过一个手动格式覆盖编译器为我生成所有格式!但如果需要,我可以。很好的例子是 jodaTimeWrites - 默认 jodaTimeFormat 将生成更适合机器处理的 long 值,但我已经用我自己的隐式格式覆盖它以匹配 ruby​​ 的示例。

    上面的代码产生以下输出:

    {
      "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" : "david@heinemeierhansson.com",
        "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"
      } ]
    }
    

    现在我得到了 33 行 scala 更复杂的映射(没有案例类),而不是 21 行 ruby​​ 代码。为什么要多打字?因为现在我很确定当我通过Comment 而不是Attachment 我会得到编译器错误,或者当我的同事错误地将joda.time.DateFormat 更改为java.util.DateFormat 他会收到错误而不是一些乱码。

    【讨论】:

      【解决方案2】:

      这是使用 Scala 的内置 XML 文字和出色的 argonaut.io JSON 序列化库的答案草图:

      // dependencies for use with scala 2.10.2:
      // "io.argonaut" %% "argonaut" % "6.0-RC3"
      // "org.scalaz" %% "scalaz-core" % "7.0.1"
      
      import scalaz._, Scalaz._
      import argonaut._, Argonaut._
      
      object ArgonautJsonExample extends App {
      
        case class File(name: String, url: String = "http://my.dummy.url")
        case class Message(name: String, isAdmin: Boolean, attachmentOpt: Option[File])
        val message =
          new Message(
            "Erik",
            true,
            Some(File("strawberry.png"))
          )
      
        val json: Json =
          ("name" := message.name) ->:
          ("filename" :=? message.attachmentOpt.map(_.name)) ->?:
          ("url" :=? message.attachmentOpt.map(_.url)) ->?:
          jEmptyObject
      
      
        val myXml =
          <myObject>
            <author>
              <name>{message.name}</name>
            </author>
            {
              message.attachmentOpt map { file =>
                <attachment url={file.url}>
                  {file.name}
                </attachment>
              } getOrElse xml.NodeSeq.Empty
            }
          </myObject>
      
      
        println("json = " + json)
        println("")
        println("xml = " + myXml)
      
      }
      

      这会给你:

      json = {"url":"http://my.dummy.url","filename":"strawberry.png","name":"Erik"}
      
      xml = <myObject>
        <author>
          <name>Erik</name>
        </author>
        <attachment url="http://my.dummy.url">
              strawberry.png
            </attachment>
      </myObject>
      

      【讨论】: