2016/10/24

HTTP programming in Scala Play


如何在 scala play 中取得 broswer 的 request,並將結果回傳給 browser,scala play 是以 routes 設定為進入點,確認要使用哪一個 controller 處理哪一個 url request,最重要的是,簡化了非同步處理的處理方法,我們可以很輕鬆地就隔絕後端 DB 的處理,避免因 DB 異常影響到前端頁面卡頓的狀況。


Actions, Controllers and Results


在 Play 收到的 browser request 就是由 Action 處理,並產生 Result


plya.api.mvc.Action 就等同於 (play.api.mvc.Request => plya.api.mv.Result) 函數


controller 就是 Action generator,在 controller 中可以定義 method 為


package controllers

import play.api.mvc._

class Application extends Controller {
  def echo = Action { request =>
    Ok("Got request [" + request + "]")
  }
  
  def hello(name: String) = Action {
    Ok("Hello " + name)
  }
}

產生 Action 的範例


Action {
  Ok("Hello world")
}

Action { request =>
  Ok("Got request [" + request + "]")
}

Action { implicit request =>
  Ok("Got request [" + request + "]")
}

// 指定 BodyParser
Action(parse.json) { implicit request =>
  Ok("Got request [" + request + "]")
}



index 及 index2 會產生相同的頁面 Result


import play.api.http.HttpEntity

def index = Action {
  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Strict(ByteString("Hello world!"), Some("text/plain"))
  )
}

def index2 = Action {
  Ok("Hello world!")
}

以下這些為各種不同類型的 play.api.mvc.Results 的範例


val ok = Ok("Hello world!")
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")



處理 Redirect


// 303 SEE_OTHER
def index = Action {
  Redirect("/user/home")
}

// 301 MOVED_PERMANENTLY
def index = Action {
  Redirect("/user/home", MOVED_PERMANENTLY)
}



如果只是定義介面,還不實作,可以用 TODO


def index(name:String) = TODO


HTTP Routing


定義 /conf/routes


Play 支援兩種 router: 預設建議使用第一種


  1. dependency injected router
  2. static router

如果要更換 router 為 static router,可以在 build.sbt 中調整設定。如果在 routes 符合兩個以上的路由,那就依照路由宣告的順序使用第一個。


routesGenerator := StaticRoutesGenerator



route 的第一個欄位是填寫 HTTP method,有以下這六種


GET, PATCH, POST, PUT, DELETE, HEAD




route 範例


# 固定路徑
GET   /clients/all          controllers.Clients.list()

# 動態取得路徑中的一部份,可以用 regular expression: [^/]+
GET   /clients/:id          controllers.Clients.show(id: Long)

# *id 就等同於 .+
GET   /files/*name          controllers.Application.download(name)

# 使用自訂的 regular expression,如果要自動轉型,才填寫 id:Long,否則可以省略不寫
GET   /items/$id<[0-9]+>    controllers.Items.show(id: Long)

# 可以給予參數的預設值
GET   /items/               controllers.Items.show(id: Long=10)

Manipulating results


Result 的 Content-Type


// Content-Type: text/plain
val textResult = Ok("Hello World!")

// Content-Type: application/xml
val xmlResult = Ok(<message>Hello World!</message>)

// text/html
val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")

// text/html; charset=utf-8
val htmlResult2 = Ok(<h1>Hello World!</h1>).as(HTML)



可以增加或修改 HTTP Response Header


val result = Ok("Hello World!").withHeaders(
  CACHE_CONTROL -> "max-age=3600",
  ETAG -> "xx")



設定 Response Cookie


// 增加 cookie: theme
val result = Ok("Hello world").withCookies(
  Cookie("theme", "blue"))

// 清除 cookie: theme
val result2 = result.discardingCookies(DiscardingCookie("theme"))

// 增加 cookie: theme, 同時刪除 cookie: skin
val result3 = result.withCookies(Cookie("theme", "blue")).discardingCookies(DiscardingCookie("skin"))



預設的 charset 為 utf-8,可以修改


利用 scala 的 implicit instance,可以改變 HTML 的 charset


class Application extends Controller {

  implicit val myCustomCharset = Codec.javaSupported("iso-8859-1")

  def index = Action {
    // text/html; charset=iso-8859-1 
    Ok(<h1>Hello World!</h1>).as(HTML)
  }
}

因為 HTML 是用以下的方式定義的


def HTML(implicit codec: Codec) = {
  "text/html; charset=" + codec.charset
}

Session and Flash scopes


如果需要在不同 HTTP requests 之間共用資料,可以儲存在 Session 或是 Flash scopes,Session 會在整個 user Session 都可以使用,Flash scope 只能在下一個 request 中使用。


session 名稱預設為 PLAYSESSION,可以在 application.conf 用 session.cookieName 修改,(為了簡化相容性的問題,其實可以直接改成 JSESSIONID),PLAYSESSION 有使用 private key 加密以避免 client 竄改。


application.conf 的 play.http.session.maxAge (ms) 可設定 session 存活的時間


如果要暫存一些資料,可以在 SESSION 中存入 unique ID ,然後在 play 的 cache 機制中儲存資料




session 的新增與刪除


// 存入 session
Ok("Welcome!").withSession(
  "connected" -> "user@gmail.com")

Ok("Hello World!").withSession(
  request.session + ("saidHello" -> "yes"))

// 移除 session: theme
Ok("Theme reset!").withSession(
  request.session - "theme")

// 讀取 session value,判斷 user 是否已經登入系統
def index = Action { request =>
  request.session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

// 直接用新的 session 將舊的 session 刪除
Ok("Bye").withNewSession



// 取得 flash scope value: success
def index = Action { implicit request =>
  Ok {
    request.flash.get("success").getOrElse("Welcome!")
  }
}

// 新增 flash scope: success
def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created")
}

如果要在畫面上使用 flash scope value: success


@()(implicit flash: Flash)

@flash.get("success").getOrElse("Welcome!")

controller 必須用 implicit instance request 傳送到 views


def index = Action { implicit request =>
  Ok(views.html.index())
}

Body parsers


一般的 http request 資料會暫存記憶體中,可以用 BodyParser 來處理。如果 http request body 內容太長,就需要 以 stream 的方式來處理,以免資料需要整個暫存在記憶體中。


Play 是使用 Akka Strems 處理 stream 資料。




內建的 body parsers 可以處理 JSON, XML, forms


Action 是以 Request[A] => Result 定義的,只要有可以處理 A 的 BodyParser,就可以處理對應的 Request Content


trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

trait Request[+A] extends RequestHeader {
  def body: A
}

預設會以 request header: Content-Type 作為選用 BodyParser 的條件。例如 Content-Type 為 application/json,就會使用 JsValue 的 BodyParser,Content-Type 為 application/x-form-wwww-urlencoded,就 paring 為 Map[String, Seq[String]]




預設 body parser 會產生 AnyContent 資料型別,然後再以 asJson 轉換成 JsValue


def save = Action { request =>
  val body: AnyContent = request.body
  val jsonBody: Option[JsValue] = body.asJson

  // Expecting json body
  jsonBody.map { json =>
    Ok("Got: " + (json \ "name").as[String])
  }.getOrElse {
    BadRequest("Expecting application/json request body")
  }
}

以下是 default body parser 支援的資料對應


  • text/plain: String, asText
  • application/json: JsValue, asJson
  • application/xml, text/xml 或是 application/XXX+xml: scala.xml.NodeSeq, asXml
  • application/form-url-encoded: Map[String, Seq[String]], asFormUrlEncoded
  • multipart/form-data: MultipartFormData, asMultipartFormData
  • 其他的content: RawBuffer, asRaw



選用適當的 body parser


def save = Action(parse.json) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

可以用 curl 測試,會得到 Got: user name


curl 'http://localhost:9000/test' -H 'Content-Type:application/json' --data-binary '{"name": "user name","interval": "month"}'

如果使用 parse.tolerantJson,就會忽略 Content-Type,直接嘗試將 content body 以 json parsing


def save = Action(parse.tolerantJson) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

以下例子可以將 request body 儲存到一個檔案中


def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.body)
}

storeInUserFile 是一個自訂的 body parser,可以從 session 取得 username,並將 body 以 user 名稱儲存為檔案


val storeInUserFile = parse.using { request =>
  request.session.get("username").map { user =>
    file(to = new File("/tmp/" + user + ".upload"))
  }.getOrElse {
    sys.error("You don't have the right to upload here")
  }
}

def save = Action(storeInUserFile) { request =>
  Ok("Saved the request content to " + request.body)
}



application.conf 中是以這個設定值,決定最大的 content length


play.http.parser.maxMemoryBuffer=128K

可以直接用 maxLength 覆寫這個設定值,parse.maxLength 可以封裝任何一個 body parser


// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)
}

// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
  Ok("Saved the request content to " + request.body)
}



Custom body parser


note: 這部分我決定先跳過不看,等比較熟悉之後,再去了解


Actions composition


如果需要做一個 Action logging decorator,記錄所有呼叫這個 action 的 request。


首先要實作 invokeBlock method,ActionBuilder 產生的每一個 action 都會呼叫。


import play.api.mvc._

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    Logger.info("Calling action")
    block(request)
  }
}

使用剛剛的 LoggingAction


def index = LoggingAction {
  Ok("Hello World")
}

def submit = LoggingAction(parse.text) { request =>
  Ok("Got a body " + request.body.length + " bytes long")
}



用上面的寫法,會需要寫很多個 action buidler,如果要改成將既有的 Action 增加 Logging 的功能,可以用 wrapping actions 的方法實作。


import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

  def apply(request: Request[A]): Future[Result] = {
    Logger.info("Calling action")
    action(request)
  }

  lazy val parser = action.parser
}

也可以不產生 action class,直接定義 logging method


import play.api.mvc._

def logging[A](action: Action[A])= Action.async(action.parser) { request =>
  Logger.info("Calling action")
  action(request)
}



Actions 可以利用 composeAction 方法 mixed in to action builder


object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    block(request)
  }
  override def composeAction[A](action: Action[A]) = new Logging(action)
}

現在就可以這樣使用 LogginAction,或是用 mixin wrapping actions without action builder 的方法


def index = LoggingAction {
  Ok("Hello World")
}

def index = Logging {
  Action {
    Ok("Hello World")
  }
}



這個例子是修改 request,增加一個 remoteAddress 欄位


import play.api.mvc._

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
  val newRequest = request.headers.get("X-Forwarded-For").map { xff =>
    new WrappedRequest[A](request) {
      override def remoteAddress = xff
    }
  } getOrElse request
  action(newRequest)
}

這個例子是拒絕 http 連線,只允許 https 通過


import play.api.mvc._

def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
  request.headers.get("X-Forwarded-Proto").collect {
    case "https" => action(request)
  } getOrElse {
    Future.successful(Forbidden("Only HTTPS requests allowed"))
  }
}

修改 response,增加一個 response header: X-UA-Compatible


import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
  action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}



四個 pre-defined traits,實作了 ActionFunction,用來對不同資料進行處理。我們也可以實作 invokeFunction,實作自訂的 ActionFunction。


  1. ActionTransformer
    可修改 request, 增加資料
  2. ActionFilter
    選擇性攔截 request,可用來產生 error
  3. ActionRefiner
    以上兩種的 general case
  4. ActionBuilder
    以 Request 為參數,可以產生新的 actions

使用範例:Authentication


import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

object UserAction extends
    ActionBuilder[UserRequest] with ActionTransformer[Request, UserRequest] {
  def transform[A](request: Request[A]) = Future.successful {
    new UserRequest(request.session.get("username"), request)
  }
}

使用範例:增加資料到 request


// add Item to UserRequest
import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  def username = request.username
}

// looks up that item and returns Either an error (Left) or a new ItemRequest (Right)
def ItemAction(itemId: String) = new ActionRefiner[UserRequest, ItemRequest] {
  def refine[A](input: UserRequest[A]) = Future.successful {
    ItemDao.findById(itemId)
      .map(new ItemRequest(_, input))
      .toRight(NotFound)
  }
}

使用範例: Validating requests


object PermissionCheckAction extends ActionFilter[ItemRequest] {
  def filter[A](input: ItemRequest[A]) = Future.successful {
    if (!input.item.accessibleByUser(input.username))
      Some(Forbidden)
    else
      None
  }
}

利用 ActionBuilder,以及 andThen,把以上的 action function 合併在一起


def tagItem(itemId: String, tag: String) =
  (UserAction andThen ItemAction(itemId) andThen PermissionCheckAction) { request =>
    request.item.addTag(tag)
    Ok("User " + request.username + " tagged " + request.item.id)
  }

Content negotiation


這個機制可以用相同的 URI 處理不同的 content boday,例如同一個 URI 網址可以同時接受 XML, JSON。也可以利用 Accept-Language 欄位,決定回傳的 response 要用什麼語系的內容。


play.api.mvc.RequestHeader#acceptLanguages 可取得 Accept-Language 的資料


play.api.mvc.RequestHeader#acceptedTypes 可取得可接受的 request MIME types 列表,也就是 Accept 欄位。實際上 Accept 欄位並沒有確切的 MIME type,而是 media range 例如 text/*, /,controller 提供 render method,處理 media range。


val list = Action { implicit request =>
  val items = Item.findAll
  render {
    case Accepts.Html() => Ok(views.html.list(items))
    case Accepts.Json() => Ok(Json.toJson(items))
  }
}

可以使用 play.api.mvc.Accepting case class 產生特定 MIME type 的自訂 extractor。


  val AcceptsMp3 = Accepting("audio/mp3")
  render {
    case AcceptsMp3() => ???
  }

Handling errors


HTTP application 的 error 主要有兩種:client errors, server errors。


Play 會自動偵測 client errors,包含 malformed header value, unsupported content types, requests for unknown resource。也會自動處理一些 server errors,例如 code throws an exception,Play 會自動產生 error page。


以下為 custom error handler,實作了兩個 methods: onClientError 及 onServerError。


import play.api.http.HttpErrorHandler
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent._
import javax.inject.Singleton;

@Singleton
class ErrorHandler extends HttpErrorHandler {

  def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    Future.successful(
      Status(statusCode)("A client error occurred: " + message)
    )
  }

  def onServerError(request: RequestHeader, exception: Throwable) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }
}

可在 application.conf 設定 error handler


play.http.errorHandler = "com.example.ErrorHandler"



也可以利用 default error handler 延伸自己的功能。以下就是不改變 development error message,並修改特定的 onForbidden error page。


import javax.inject._

import play.api.http.DefaultHttpErrorHandler
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.routing.Router
import scala.concurrent._

@Singleton
class ErrorHandler @Inject() (
    env: Environment,
    config: Configuration,
    sourceMapper: OptionalSourceMapper,
    router: Provider[Router]
  ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) {

  override def onProdServerError(request: RequestHeader, exception: UsefulException) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }

  override def onForbidden(request: RequestHeader, message: String) = {
    Future.successful(
      Forbidden("You're not allowed to access this resource.")
    )
  }
}

References


HTTP programming

2016/10/17

Scala Play 2.5


新版本的 Scala Play 又有些不同,軟體的更新帶來了新的功能,新的想法跟做法,但就得花一些時間去了解,一個沒有變化的專案除了表示穩定,也可能是維護的人太少,沒辦法推出新功能,一個常常更新的專案,一般都是希望能夠相容舊版,無痛升級,雖然對開發者來說是福音,但也可能因為相容而自我設限,造成整個專案越來越龐大,該怎麼做沒有定論,Play Framework 就曾因為升級到 2 之後,不跟 1 相容,造成專案開發者的流失,不過都已經是過去的歷史了。


書本的內容也沒辦法隨著軟體更新就馬上更新,所幸 Play Framework 官方文件很完整,跟著文件的說明測試一次,應該就能大致掌握新版的功能。


安裝 Play


  1. 安裝 Java SDK
  2. 下載 Activator 1.3.10 minimal
  3. 將 typesafe-activator-1.3.10-minimal 資料夾的 bin 目錄放到環境變數的 PATH 中
  4. 確認可以在 console 執行 activator

產生 project


在 console 產生 project 有兩種方式,command line 或是 web ui。


  • command line

使用 activator 就可以產生 project


activator new playtest1 play-scala

切換到 playtest project folder,就可以用 activator 執行project


cd playtest1
activator

  • activator ui

執行 activator ui,就可以使用 activator 內建的網頁管理介面


activator ui

連結到 http://localhost:8888 就可以看到管理介面



選擇 Lightbend 的 Play Scala Seed 專案 template 及路徑,就可以產生 project




Play 支援了 Eclipse, IDEA, NetBeans, ENSIME 這些 IDE,跟著 Setting up your preferred IDE 的說明,就可以利用 IDE 直接產生一個新的 project。


以下記錄 IDEA 的步驟:
如果要產生一個新的 Play application


  1. New Project
  2. 選擇 Scala -> Activator
  3. 選擇一個適當的 template,最簡單的就是選擇 Lightbend 提供的 Play Scala Seed
  4. Finish

如果要 import 一個已經產生的 Play application


  1. File -> New -> Project from existing sources
  2. 選擇 project folder
  3. 選擇 Import project from external model -> SBT
  4. Finish

activator command line


cd playtest1

# 進入 activator 互動介面
activator

編譯 project


[playtest1] $ compile

執行 project,在這個模式下會啟用 auto-reload 功能,也就是 Play 會自動檢查 project source 有沒有更新,並自動重新編譯


[playtest1] $ run

執行測試程式


[playtest1] $ test

進入 scala console mode,按 Ctrl-D 就可以跳出 console mode


[playtest1] $ console

在 scala console mode,可以直接使用 play 的 classes


scala> views.html.index("Hello")
res0: play.twirl.api.HtmlFormat.Appendable =

<!DOCTYPE html>
<html lang="en">
    <head>

        <title>Welcome to Play</title>

以下這段 code 在文件中提到是可以在 console mode 中啟動 application


import play.api._
val env = Environment(new java.io.File("."), this.getClass.getClassLoader, Mode.Dev)
val context = ApplicationLoader.createContext(env)
val loader = ApplicationLoader(context)
val app = loader.load(context)
Play.start(app)

如果 debug 就要在啟動 activator 加上參數,JDPA 就會運作在 Port 9999 上


activator -jvm-debug 9999

Play console 其實就是一個 sbt console,因此我們可以用以下的方式,透過 sbt 進行 triggered execution


[playtest1] ~ compile
[playtest1] ~ run
[playtest1] ~ test

Play application file layout


app                      → app 原始程式
 └ assets                → 程式資源資料夾
    └ stylesheets        → LESS CSS 原始程式
    └ javascripts        → CoffeeScript 原始程式
 └ controllers           → app controllers
 └ models                → app business layer
 └ views                 → 網頁 templates
build.sbt                → sbt build script
conf                     → 設定檔及其他不需要編譯的資源
 └ application.conf      → app 主設定檔
 └ routes                → 路由跟程式的對應設定
dist                     → 專案發布的資料夾
public                   → 對外開放的資源資料夾
 └ stylesheets           → CSS files
 └ javascripts           → Javascript files
 └ images                → Image files
project                  → sbt 設定檔
 └ build.properties      → sbt 專案的設定檔
 └ plugins.sbt           → sbt plugins
lib                      → 不在 lib 倉庫中受管理的 libraries
logs                     → logs 資料夾
 └ application.log       → 預設的 log file
target                   → 編譯後的檔案資料夾
 └ resolution-cache      → 相關 libraries 的資料
 └ scala-2.11
    └ api                → API 文件
    └ classes            → 編譯後的 class files
    └ routes             → routes 編譯後的結果
    └ twirl              → templates 編譯後的結果
 └ universal             → app 封裝
 └ web                   → 編譯後的網頁資源
test                     → 單元測試的資料夾

Task sample project


以剛剛的 test1 為基礎,修改成一個簡單的 Task 網頁


  • 修改 conf/routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Map static resources from the /public folder to the /assets URL path
GET           /assets/*file        controllers.Assets.at(path="/public", file)

# Home page
GET           /                    controllers.TaskController.index

# Tasks
GET           /tasks               controllers.TaskController.tasks
POST          /tasks               controllers.TaskController.newTask
DELETE        /tasks/:id           controllers.TaskController.deleteTask(id: Int)

  • 新增 models/Task.scala

package models

case class Task(id: Int, name: String)

object Task {

  private var taskList: List[Task] = List()

  def all: List[Task] = {
    // 回傳整個 taskList
    taskList
  }

  def add(taskName: String) = {
    // task id 由 0 開始,如果 list 為 nonEmpty,就使用 list 中最後一個元素的 id
    val lastId: Int = if (taskList.nonEmpty) taskList.last.id else 0
    taskList = taskList ++ List(Task(lastId + 1, taskName))
  }

  def delete(taskId: Int) = {
    // filter 可取得滿足條件的所有元素
    // filterNot 可取得 不滿足 條件的所有元素
    taskList = taskList.filterNot(task => task.id == taskId)
  }
}

  • 新增 app/controllers/TaskController

package controllers

import models.Task
import play.api.mvc._


class TaskController extends Controller {

  def index = Action {
    // 將 request 由 http://localhost:9000/ redirect 到 http://localhost:9000/tasks
    Redirect(routes.TaskController.tasks)
  }

  def tasks = Action {
    // Task.all 取得整個 taskList,送入 views.html.index 產生網頁
    Ok(views.html.index(Task.all))
  }

  def newTask = Action(parse.urlFormEncoded) {
    implicit request =>
      // 取得 request 裡面的 taskName, 新增到 Task 中
      Task.add(request.body.get("taskName").get.head)
      // redirect 到 http://localhost:9000/tasks
      Redirect(routes.TaskController.index)
  }

  def deleteTask(id: Int) = Action {
    // 刪除 task id
    Task.delete(id)
    Ok
  }

}

  • views/index.scala.html

@(tasks: List[Task])

@main("Task Tracker") {

    <h2>Task</h2>
    <div>
        <form action="@routes.TaskController.newTask()" method="post">
            <input type="text" name="taskName" placeholder="Add a new Task" required>
            <input type="submit" value="Add">
        </form>
    </div>
    <div>
        <ul>
        @tasks.map { task =>
            <li>
                @task.name <button onclick="deleteTask ( @task.id) ;">Remove</button>
            </li>
        }
        </ul>
    </div>
    <script>
    function deleteTask ( id ) {
        var req = new XMLHttpRequest ( ) ;
        req.open ( "delete", "/tasks/" + id ) ;
        req.onload = function ( e ) {
            if ( req.status = 200 ) {
                document.location.reload ( true ) ;
            }
        } ;
        req.send ( ) ;

    }
    </script>
}

  • views/main.scala.html

@(title: String)(content: play.twirl.api.Html)
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
    </head>
    <body>
    @content
    </body>
</html>


Projects


Streaming examples using Play 2.5 and Akka Streams http://loicdescotte.github.io/posts/play25-akka-streams/


play-scala-akka example


Example Play Scala application showing WebSocket use with Akka actors https://www.playframework.com/documentation/2.5.x/ScalaWebSockets


Mastering Play Framework for Scala

2016/10/3

json4s: scala json library


scala 世界中,關於處理 json 的 library 很多, 這裡列出了 13 個: Scala standard libray, jawn, sphere-json, argonaut, circe, json4s, spray-json-shapeless, spray-json, lift-json, play-json, rapture,用 google trends 查了一下,json4s查詢的次數還是多一些,json4s 的目標就是提供一個單一的 AST 供其他Scala類庫來使用,以下測試如何使用 json4s。



準備使用 json4s


在 build.sbt 加上 org.json4s 的 library


val json4sVersion = "3.3.0"

libraryDependencies ++= Seq(
  // json4s
  "org.json4s" %% "json4s-native" % json4sVersion,
  "org.json4s" %% "json4s-jackson" % json4sVersion,
  "org.json4s" %% "json4s-ext" % json4sVersion
)

json4s 需要 import 以下的 packages


import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._

主要提供 parse, render 進行 json 轉換,render 需要輸入 JValue 物件,必須利用 JsonDSL 提供 implicit 轉換,parse 很明確,就是 parsing JSON 字串。


測試 json4s


import java.text.SimpleDateFormat

import org.json4s.JsonDSL._
import org.json4s._
import org.json4s.native.JsonMethods._
import org.scalatest.{FunSpec, Matchers}

class TestJson4s extends FunSpec with Matchers {
  implicit val formats = new DefaultFormats {
    //override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  }

  //implicit val formats = Serialization.formats(ShortTypeHints(List()))

  //  implicit val formats = new DefaultFormats {
  //    override val dateFormat: DateFormat = new DateFormat {
  //      override def parse(s: String): Option[Date] = Some(new Date(s.toLong * 1000))
  //      override def format(d: Date): String = (d.getTime/1000).toString
  //    }
  //  }

  case class Student(name: String, age: Int)

  describe("Testing json4s marshalling, unmarshalling") {
    it("parse json and extract to class") {
      // 將原始的 json 字串,轉換成 json4s JObject 物件
      println()
      println(">將 json 轉換成 JObject")
      val tom = parse( """ { "name" : "tom","age":23,"number":[1,2,3,4] } """)
      println(s"tom:${tom.getClass.getName}")
      println(tom)
      val tstring: String = tom.toString
      //JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))
      tstring should be("JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))")

      // 上面的轉換之後 並不是 Scala 的原生物件
      // 要用 Map[String, _] 轉換
      println()
      println(">把 JObject 轉成 Scala 物件")
      for ((k, v) <- tom.values.asInstanceOf[Map[String, _]]) {
        println(s"${k} -> ${v}:${v.getClass}")
      }
      //      name -> tom:class java.lang.String
      //      age -> 23:class scala.math.BigInt
      //      number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

      // 可以用  \  取得某個欄位的值
      println()
      println(">可以用  \\  取得某個欄位的值")
      println(s"name -> ${(tom \ "name").values}")
      println(s"age -> ${(tom \ "age").values}")
      println(s"number -> ${(tom \ "number").values}")

      //      name -> tom
      //      age -> 23
      //      number -> List(1, 2, 3, 4)

      val name = (tom \ "name").values
      name should be("tom")

      // 可直接將 tom 轉換成 Student

      val student: Student = tom.extract[Student]
      println()
      println(">可直接將 tom 轉換成 Student")
      println(student)
      // Student(tom,23)

      student.name should be("tom")

      ///// 列印 json

      println()
      println("> 列印 json 時要呼叫 compact")
      println(compact(render(Map("name" -> "tom", "age" -> "23"))))
      //结果:{"name":"tom","age":"23"}

      println()
      println("> 只支援 Tuple2[String, A] 這種格式的 tuple")
      val tuple = ("name", "tom")
      println(compact(render(tuple)))
      //结果:{"name":"tom"}


      println()
      println("> 可以用 ~ 將兩個 tuple 連接在一起")
      val tuple2 = ("name", "tom") ~ ("age", 23)
      println(tuple2)
      println(compact(render(tuple2)))
      //结果:
      //JObject(List((name,JString(tom)), (age,JInt(23))))
      //{"name":"tom","age":23}


      println()
      println("> 當 value 為 None 時,就不會序列化")
      val tuple3 = ("name", "tom") ~("age", None: Option[Int])
      println(tuple3)
      //结果:
      //JObject(List((name,JString(tom)), (age,JNothing)))
      //{"name":"tom"}


      println()
      println("> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀")
      case class Winner(id: Long, numbers: List[Int])
      case class Lotto(id: Long, winningNumbers: List[Int], winners: List[Winner], drawDate: Option[java.util.Date])

      val winners = List(Winner(23, List(2, 45, 34, 23, 3, 5)), Winner(54, List(52, 3, 12, 11, 18, 22)))
      val lotto = Lotto(5, List(2, 45, 34, 23, 7, 5, 3), winners, Option(new java.util.Date()))
      val customDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val json =
        ("lotto" ->
          ("lotto-id" -> lotto.id) ~
            ("winning-numbers" -> lotto.winningNumbers) ~
            //("draw-date" -> lotto.drawDate.map(_.toString)) ~
            ("draw-date" -> lotto.drawDate.map( customDateFormatter.format(_) )) ~
            ("winners" ->
              lotto.winners.map { w =>
                (("winner-id" -> w.id) ~
                  ("numbers" -> w.numbers))
              }))

      println(compact(render(json)))
      // {"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 12:03:36","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}

      println(pretty(render(json)))
      //
      //      {
      //        "lotto":{
      //          "lotto-id":5,
      //          "winning-numbers":[2,45,34,23,7,5,3],
      //          "draw-date":"2016-05-19 12:03:36",
      //          "winners":[{
      //          "winner-id":23,
      //          "numbers":[2,45,34,23,3,5]
      //        },{
      //          "winner-id":54,
      //          "numbers":[52,3,12,11,18,22]
      //        }]
      //        }
      //      }


      //
      println()
      println("> 用 merge 合併兩個 json")
      val t1 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaoerban"]
    }
        """)
      val t2 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaosanban"]
    }
        """)
      val t3 = t1 merge t2
      println(pretty(render(t3)))
      //      {
      //        "name":"tom",
      //        "age":23,
      //        "class":["xiaoerban","xiaosanban"]
      //      }

      println()
      println("> 用 Diff 檢查兩個 JObject 的差異")
      val Diff(changed, added, deleted) = t3 diff t1
      println("changed", changed)
      println("added", added)
      println("deleted", deleted)
      //      (changed,JNothing)
      //      (added,JNothing)
      //      (deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))


      println()
      println("> 可以將 xml 轉換成 json")
      import org.json4s.Xml.toJson
      val xml =
        <users>
          <user>
            <id>1</id>
            <name>Harry</name>
          </user>
          <user>
            <id>2</id>
            <name>David</name>
          </user>
        </users>

      val json2 = toJson(xml)
      println(pretty(render(json2)))
    }

  }
}

以下為執行的結果


>將 json 轉換成 JObject
tom:org.json4s.JsonAST$JObject
JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))

>把 JObject 轉成 Scala 物件
name -> tom:class java.lang.String
age -> 23:class scala.math.BigInt
number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

>可以用  \  取得某個欄位的值
name -> tom
age -> 23
number -> List(1, 2, 3, 4)

>可直接將 tom 轉換成 Student
Student(tom,23)

> 列印 json 時要呼叫 compact
{"name":"tom","age":"23"}

> 只支援 Tuple2[String, A] 這種格式的 tuple
{"name":"tom"}

> 可以用 ~ 將兩個 tuple 連接在一起
JObject(List((name,JString(tom)), (age,JInt(23))))
{"name":"tom","age":23}

> 當 value 為 None 時,就不會序列化
JObject(List((name,JString(tom)), (age,JNothing)))

> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀
{"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 13:57:54","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}
{
  "lotto":{
    "lotto-id":5,
    "winning-numbers":[2,45,34,23,7,5,3],
    "draw-date":"2016-05-19 13:57:54",
    "winners":[{
      "winner-id":23,
      "numbers":[2,45,34,23,3,5]
    },{
      "winner-id":54,
      "numbers":[52,3,12,11,18,22]
    }]
  }
}

> 用 merge 合併兩個 json
{
  "name":"tom",
  "age":23,
  "class":["xiaoerban","xiaosanban"]
}

> 用 Diff 檢查兩個 JObject 的差異
(changed,JNothing)
(added,JNothing)
(deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))

> 可以將 xml 轉換成 json
{
  "users":{
    "user":[{
      "id":"1",
      "name":"Harry"
    },{
      "id":"2",
      "name":"David"
    }]
  }
}
[info] TestJson4s:
[info] Testing json4s marshalling, unmarshalling
[info] - parse json and extract to class
[info] Run completed in 371 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Reference


學習和使用Scala的Json解析類庫-JSON4S

2016/9/26

ScalaTest


ScalaTest 可以說是 scala 測試的王者,主要原因是它提供了七種 Test Spec,其中最基本的就是 FunSpec,有兩種不同的 assertion dialects,可以依照你自己的喜好,決定使用哪一種寫法,另外 ScalaTest 也跟 SBT 整合在一起,可以直接由 SBT 自動化進行測試。


如何使用


在專案的 build.sbt 中,增加 ScalaTest 的 libraryDependencies。


build.sbt

libraryDependencies ++= Seq(
  "org.scalactic" %% "scalactic" % "2.2.6",
  "org.scalatest" %% "scalatest" % "2.2.6" % "test"
)

如果要進行 Spark 的測試,也要加上 Spark 的 library,以下是我們的 build.sbt 內容。


name := "sparktest"

version := "1.0"

sbtPlugin := true

scalaVersion := "2.10.6"

val sparkVersion = "1.6.1"

libraryDependencies ++= Seq(
  "ch.qos.logback" % "logback-core" % "0.9.26",
  "ch.qos.logback" % "logback-classic" % "0.9.26",

  "org.slf4j" % "slf4j-api" % "1.6.1",

  "org.scalactic" %% "scalactic" % "2.2.6",
  "org.scalatest" %% "scalatest" % "2.2.6" % "test",

  // kafka
  "org.apache.kafka" %% "kafka" % "0.9.0.1",
  "org.apache.kafka" % "kafka-clients" % "0.9.0.1",

  // spark
  "org.apache.spark" %% "spark-core" % sparkVersion % "provided",
  "org.apache.spark" %% "spark-hive" % sparkVersion % "provided",
  "org.apache.spark" %% "spark-mllib" % sparkVersion % "provided",
  "org.apache.spark" %% "spark-streaming" % sparkVersion % "provided",
  "org.apache.spark" %% "spark-streaming-kafka" % sparkVersion,

  // geoip2-2.6.0
  "com.maxmind.geoip2" % "geoip2" % "2.6.0",
  "com.maxmind.db" % "maxmind-db" % "1.2.1"
)

// 因為 spark 的 libraryDependencies 裡面用到的 library 有衝突,必須加上這一行將衝突解除
dependencyOverrides ++= Set(
  "com.fasterxml.jackson.core" % "jackson-databind" % "2.4.4"
)

ivyScala := ivyScala.value map {
  _.copy(overrideScalaVersion = true)
}

assemblyMergeStrategy in assembly := {
  case m if m.toLowerCase.endsWith("manifest.mf") => MergeStrategy.discard
  case m if m.startsWith("META-INF") => MergeStrategy.discard
  case PathList("javax", "servlet", xs@_*) => MergeStrategy.first
  case PathList("org", "apache", xs@_*) => MergeStrategy.first
  case "about.html" => MergeStrategy.rename
  case "reference.conf" => MergeStrategy.concat
  case _ => MergeStrategy.first
}

Matchers


ScalaTest 包含了兩種 Matchers: MustMatchers 及 ShouldMatchers,以下用 FunSpec 為例,說明 ShouldMatchers,而MustMatchers 寫法跟 ShouldMatchers 類似。


  1. Simple Matcher
  2. String Matcher
  3. Relational Operator Matcher
  4. Floating-point Macher
  5. Reference Macher
  6. Iterable Macher
  7. Seq 與 Traversable Matchers
  8. Map Matcher
  9. Compound Matcher
  10. Property Matcher
  11. Java Collection Matcher

import org.scalatest.{FunSpec, Matchers}

class TestFunSpec extends FunSpec with Matchers{
  describe("Using all should matchers") {

    // Simple Matcher
    //  驗證的資料要放在 () 裡面
    //  be 可以換成 equal
    //  be 不能換成 == 或是 !=
    it("has simple matchers") {
      val list = 2 :: 4 :: 5 :: Nil
      list.size should be(3)
      list.size should equal(3)
    }

    // String Matcher
    it("has string matchers") {
      val string = """I fell into a burning ring of fire.
         I went down, down, down and the flames went higher"""
      string should startWith("I fell")
      string should endWith("higher")
      string should not endWith "My favorite friend, the end"
      string should include("down, down, down")
      string should not include ("Great balls of fire")

      // 以 regex 作為判斷依據
      string should startWith regex ("I.fel+")
      string should endWith regex ("h.{4}r")
      string should not endWith regex("\\d{5}")
      string should include regex ("flames?")

      string should fullyMatch regex ("""I(.|\n|\S)*higher""") //regex in triple quotes
    }

    // Relational Operator Matcher
    it("has <, >, <=, >= matchers") {
      val answerToLife = 42
      answerToLife should be < (50)
      answerToLife should be > (3)
      answerToLife should be <= (100)
      answerToLife should be >= (0)

      answerToLife shouldNot be <= (0)
    }

    // Floating-point Macher
    //  scala 的 FP 運算,會有誤差值
    //   在 ScalaTest 提供了 +- 的方式,設定允許的誤差範圍
    it("has checking floating point imperfections") {
      (4.0 + 1.2) should be(5.2)
      (0.9 - 0.8) should be(0.1 +- .01)
      (0.4 + 0.1) shouldNot be (40.00 +- .30)
    }

    // Reference Macher
    it("has object reference checker") {
      val A:String = "Testing String A"
      val B:String = A
      val C:String = "Testing String C"

      A should be theSameInstanceAs(B)
      C shouldNot be theSameInstanceAs(B)
    }

    // Iterable Matcher
    it("has methods for iterable") {
      // 'empty 是 scala 的特殊符號
      List() should be('empty)
      List() shouldBe 'empty
      List() shouldBe 'traversableAgain
      8 :: 6 :: 7 :: 5 :: 3 :: 0 :: 9 :: Nil should contain(7)
    }

    // Seq 與 Traversable Matchers
    it("has methods for seq") {
      // 提供了 length, size method for Seq or Traversable
      (1 to 9).toList should have length (9)
      (1 to 9).toList should have size (9)
    }
    it("has methods for traversable") {
      (20 to 60 by 2).toList should have length (21)
      (20 to 60 by 2).toList should have size (21)
    }

    // Map Matcher
    it("has methods for map") {
      // 檢查 Map 裡面是否有某個 key 或 value
      val map = Map("Jimmy Page" -> "Led Zeppelin", "Sting" -> "The Police", "Aimee Mann" -> "Til\' Tuesday")
      map should contain key ("Sting")
      map should contain value ("Led Zeppelin")
      map shouldNot contain key("Brian May")
    }

    // Compound Matcher
    it("has compound and and or") {
      val redHotChiliPeppers = List("Anthony Kiedis", "Flea", "Chad Smith", "Josh Klinghoffer")

      // 將 assert 條件用 and, or 串接起來
      redHotChiliPeppers should (contain("Anthony Kiedis") and
        (not contain ("John Frusciante")
          or contain("Dave Navarro")))

      redHotChiliPeppers shouldNot (contain ("The Edge") or contain ("Kenny G"))

      //redHotChiliPeppers shouldNot contain "The Edge" or contain "Kenny G"

      var total = 3
      redHotChiliPeppers shouldNot (contain ("The Edge") or contain {total += 6; "Kenny G"})
      total should be (9)

      val gorillaz:List[String] = null
      gorillaz should be (null)
    }

    // Property Matcher
    it("has a way to asset an object's property using getter method") {
      class Album(val name:String, val year:Int)

      val album = new Album("Ghost Singer", 2016)

      album should have (
        'name ("Ghost Singer"),
        'year (2016)
      )
    }

    // Java Collection Matcher
    it("has method for java collections") {
      import java.util.{ArrayList => JArrayList, HashMap => JHashMap, List => JList, Map => JMap}

      val jList: JList[Int] = new JArrayList[Int](20)
      jList.add(3)
      jList.add(6)
      jList.add(9)

      val emptyJList: JList[Int] = new JArrayList[Int]()

      emptyJList should be('empty)
      jList should have length (3)
      jList should have size (3)
      jList should contain(6)
      jList shouldNot contain (10)

      val backupBands: JMap[String, String] = new JHashMap()
      backupBands.put("Joan Jett", "Blackhearts")
      backupBands.put("Tom Petty", "Heartbreakers")

      backupBands should contain key ("Joan Jett")
      backupBands should contain value ("Heartbreakers")

      backupBands shouldNot contain key("John Lydon")
    }

  }
}



MustMatchers


ShouldMatchers 可以用 MustMatchers 取代,但不能同時並存,語法就是把 should 換成 must,大部分的文件都是使用 should。


import org.scalatest.{FunSpec, MustMatchers}

class TestFunSpecMustMatcher extends FunSpec with MustMatchers {
  describe("Using all should matchers") {
    // MustMatcher
    it("replace should to must") {
      val list = 2 :: 4 :: 5 :: Nil
      list.size must be(3)
      val string = """I fell into a burning ring of fire.
I went down, down, down and the flames went higher"""
      string must startWith regex ("I.fel+")
      string must endWith regex ("h.{4}r")

      val answerToLife = 42
      answerToLife must be < (50)
      answerToLife mustNot be >(50)


      class Artist(val firstname:String, val lastname:String)
      val garthBrooks = new Artist("Garth", "Brooks")
      val chrisGaines = garthBrooks
      val debbieHarry = new Artist("Debbie", "Harry")

      garthBrooks must be theSameInstanceAs (chrisGaines)
      (0.9 - 0.8) must be(0.1 +- .01)

      List() must be('empty)
      1 :: 2 :: 3 :: Nil must contain(3)
      (1 to 9).toList must have length (9)
      (20 to 60 by 2).toList must have size (21)
      val map = Map("Jimmy Page" -> "Led Zeppelin", "Sting" -> "The Police",
        "Aimee Mann" -> "Til\' Tuesday")
      map must contain key ("Sting")
      map must contain value ("Led Zeppelin")
      map must not contain key("Brian May")
      val redHotChiliPeppers = List("Anthony Kiedis", "Flea", "Chad Smith",
        "Josh Klinghoffer")
      redHotChiliPeppers must (contain("Anthony Kiedis") and
        (not contain ("John Frusciante")
          or contain("Dave Navarro")))
    }
  }
}

Exception Handling


利用 thrownBy 或是 intercept 語法,在 FunSpec 使用 Should Matcher 測試 Exception 的驗證


    // Excpetion Handling
    it("has exception handler") {
      class Album(val name: String, val year: Int) {
        if (year <= 2000) throw new IllegalArgumentException("Album year is required")
      }

      the[IllegalArgumentException] thrownBy (new Album("The Joy of Listening to Nothing",
        0)) should have message "Album year is required"

      intercept[IllegalArgumentException] {
        new Album("The Joy of Listening to Nothing", 0)
      }
    }

    it("should produce NoSuchElementException when head is invoked") {
      intercept[NoSuchElementException] {
        Set.empty.head
      }
    }

Informers


Informer 就像是 ScalaTest 中的 debug message,可以在測試程式的任何地方,輸出一些跟這個測試相關的訊息,只要呼叫 info(String) 就可以了。


// Informers
  describe("An Album") {
    class Album(val name: String, val year: Int) {
      if (year <= 0) throw new IllegalArgumentException("Album year is required")
    }

    it("can create an album") {
      val album = new Album("Thriller", 1981)
      info("Test the album name should be Thriller")
      album.name should be("Thriller")
    }
  }

GivenWhenThen


任何一個測試過程,都可以被描述為 Given ... When ... Then。


Given相當於所給的前置條件


When相當於產生了某個動作或處於某種條件下


Then表示前面兩個條件產生的結果


import org.scalatest.{GivenWhenThen, ShouldMatchers, FunSpec}

class TestAlbumSpec extends FunSpec with ShouldMatchers with GivenWhenThen {

  class Album(val name: String, val year: Int, val artist: Artist) {
    if (year <= 0) throw new IllegalArgumentException("Album year is required")
  }

  class Artist(val firstName: String, val lastName: String)

  describe("An Album") {
    it("can add an Artist to the album at construction time") {

      Given("The album Thriller by Michael Jackson")
      val album = new Album("Thriller", 1981, new Artist("Michael", "Jackson"))

      When("the album\'s artist is obtained")
      val artist = album.artist

      Then("the artist obtained should be an instance of Artist")

      artist.isInstanceOf[Artist] should be(true)

      And("the artist's first name and last name should be Michael Jackson")

      artist.firstName should be("Michael")
      artist.lastName should be("Jackson")
    }
  }
}

以下為測試的結果


[info] TestAlbumSpec:
[info] An Album
[info] - can add an Artist to the album at construction time
[info]   + Given The album Thriller by Michael Jackson
[info]   + When the album's artist is obtained
[info]   + Then the artist obtained should be an instance of Artist
[info]   + And the artist's first name and last name should be Michael Jackson
[info] Run completed in 209 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Pending Test 待測試, 未完成的測試, TODO


當我們在 test body 裡面加上 pending 註記時,就表示這是一個需要實作,還沒有完成的測試項目。


    it("can be a pending test") {
      info("pending 可以一直放在 test body 的最後面,代表這還沒完成")

      pending
    }

Ignoring Test


正式環境的程式碼,有可能隨著時間持續的修改,而造成測試的程式碼過時,還沒辦法修改好,無法驗證新版的 production code,寫法就是將 it 換成 ignore 就可以了,ScalaTest 會暫時先忽略這個測試項目。


    ignore("can add a Producer to an album at construction time") {
      new Album("Breezin\'", 1976, new Artist("George", "Benson"))
      //TODO: Figure out the implementation of an album producer
    }

Tagging


可以將測試項目加上 Tag,就可以分組進行測試,Tagging 可以在以下這些狀況使用:


  1. 想跳過某些很費時的測試
  2. 某些測試是檢查一些相關的功能,需要一起執行
  3. 你想給測試分成單元測試、綜合測試、驗收測試等分類時

    object TestTag extends Tag("TestTag")
    object DevTag extends Tag("DevTag")
  
    it("can add multiple tags", TestTag, DevTag) {
      info("test TestTag, DevTag")
    }

在 sbt 裡面,目前只有 testOnly 有支援 Tag,可以啟動某個 tag 的測試,或是忽略某個 tag


# 啟動包含 TestTag 的測試
testOnly com.larzio.lzspark.test.scalatest.TestAlbumSpec -- -n TestTag

# 忽略 TestTag 的測試
testOnly com.larzio.lzspark.test.scalatest.TestAlbumSpec -- -l TestTag

Specifications


ScalaTest 的 Spec 指的是不同的測試 style 寫法,某方面來說,代表著自由度,可以自己選擇習慣的語法,但相對地,代表著混亂,因為寫法太多元,造成團隊開發時,程式風格差異太大。


擁抱開放與自由,就必須接納多元與混亂,開放與封閉環境,都有成功的案例。


  1. FunSpec
  2. WordSpec
  3. FeatureSpec
  4. FreeSpec
  5. FlatSpec
  6. JUnitSuite
  7. TestNGSuite



  1. FunSpec


    使用 describe, it


    import org.scalatest.{FunSpec, Tag, GivenWhenThen, ShouldMatchers}
    
    class AlbumFunSpec extends FunSpec with ShouldMatchers with GivenWhenThen {
    
      class Album(val title: String, val year: Int, val acts: Artist*) {
        if (year <= 0) throw new IllegalArgumentException("Album year is required")
      }
    
      class Artist(val firstName: String, val lastName: String)
    
      describe("An Album") {
        it("can add an Artist to the album at construction time", Tag("construction")) {
          Given("The album Thriller by Michael Jackson")
          val album = new Album("Thriller", 1981, new Artist("Michael", "Jackson"))
    
          When("the first act of the album is obtained")
          val act = album.acts.head
    
          Then("the act should be an instance of Artist")
          act.isInstanceOf[Artist] should be(true)
    
          And("the artist's first name and last name should be Michael Jackson")
          val artist = act.asInstanceOf[Artist]
          artist.firstName === "Michael"
          artist.lastName === "Jackson"
          info("This is still pending, since there may be more to accomplish in this test")
          //      pending
        }
    
        ignore("can add a Producer to an album at construction time") {
          //TODO: Add some logic to add a producer.
        }
      }
    }
  2. WordSpec


    使用了 when, should


    import org.scalatest.{WordSpec, ShouldMatchers}
    
    class AlbumWordSpec extends WordSpec with ShouldMatchers {
    
      class Album(val name: String, val year: Int) {
        if (year <= 0) throw new IllegalArgumentException("Album year is required")
      }
    
      "An Album" when {
        "created" should {
          "accept the title, the year, and a Band as a parameter, and be able to read those parameters back" in {
            new Album("Hotel California", 1977)
          }
        }
      }
    
      "An album" should {
        "throw an IllegalArgumentException if there are no acts when created" in {
          intercept[IllegalArgumentException] {
            new Album("The Joy of Listening to Nothing", 0)
          }
          info("The test at this point should still continue")
          info("since we successfully trapped the  exception")
        }
    
        "test with a thousand acts" ignore {
          //Working on a thousand acts
          new Album("One thousand singers", 2010)
        }
      }
    
    }
  3. FeatureSpec


    使用 feature, scenario


    import org.scalatest.{ShouldMatchers, GivenWhenThen, FeatureSpec}
    
    class EAlbum {
      private var on: Boolean = false
    
      def isOn: Boolean = on
    
      def pressPowerButton() {
        on = !on
      }
    }
    
    class AlbumFeatureSpec extends FeatureSpec with ShouldMatchers with GivenWhenThen {
      info("As a EAlbum set owner")
      info("I want to be able to turn the EAlbum on and off")
      info("So I can watch EAlbum when I want")
      info("And save energy when I'm not watching EAlbum")
    
      feature("EAlbum power button") {
        scenario("User presses power button when EAlbum is off") {
    
          Given("a EAlbum set that is switched off")
          val tv = new EAlbum
          assert(!tv.isOn)
    
          When("the power button is pressed")
          tv.pressPowerButton()
    
          Then("the EAlbum should switch on")
          assert(tv.isOn)
        }
    
        scenario("User presses power button when TV is on") {
    
          Given("a EAlbum set that is switched on")
          val tv = new EAlbum
          tv.pressPowerButton()
          assert(tv.isOn)
    
          When("the power button is pressed")
          tv.pressPowerButton()
    
          Then("the EAlbum should switch off")
          assert(!tv.isOn)
        }
      }
    }
  4. FreeSpec


    將 story line 後面加上 - {


    import org.scalatest.FreeSpec
    
    class TestFreeSpec extends FreeSpec {
    
      "A Set" - {
        "when empty" - {
          "should have size 0" in {
            assert(Set.empty.size == 0)
          }
    
          "should produce NoSuchElementException when head is invoked" in {
            intercept[NoSuchElementException] {
              Set.empty.head
            }
          }
        }
      }
    
    }
  5. FlatSpec


    "X should Y", "A must B"


    import org.scalatest.FlatSpec
    
    class TestFlatSpec extends FlatSpec {
      "An empty Set" should " have size 0 " in {
        assert(Set.empty.size == 0)
      }
    
      it should "produce NoSuchElementException when head is invoked" in {
        intercept[NoSuchElementException] {
          Set.empty.head
        }
      }
    }
    
  6. JUnitSuite


    首先要加上 junit library


    libraryDependencies += "junit" % "junit" % "4.12"

    跟 JUnit 的習慣一樣,@Before 是測試準備,@After 是結束測試,@Test 則是每一個測試的 case


    import org.scalatest.junit.JUnitSuite
    import org.junit.{After, Test, Before}
    import org.junit.Assert._
    
    class TestJUnitSuite extends JUnitSuite {
    
      class Album(val title: String, val year: Int, val acts: Artist*) {
        if (year <= 0) throw new IllegalArgumentException("Album year is required")
      }
    
      class Artist(val firstName: String, val lastName: String)
    
      var artist: Artist = _
    
      @Before
      def startUp() {
        artist = new Artist("Kenny", "Rogers")
      }
    
      @Test
      def newAlbum() {
        val album = new Album("Love will turn you around", 1982, artist)
        assertEquals(album.acts.size, 1)
      }
    
      @After
      def shutDown() {
        this.artist = null
      }
    
    }
    
  7. TestNGSuite


    TestNG 風格的測試,首先要加上 library


    libraryDependencies += "org.testng" % "testng" % "6.8.21"

    @DataProvider(name = "provider") 提供測試資料,@Test(dataProvider = "provider") 則是針對 data provider 進行測試


    import org.scalatest.testng.TestNGSuite
    import collection.mutable.ArrayBuilder
    import org.testng.annotations.{Test, DataProvider}
    import org.testng.Assert._
    
    class TestTestNGSuite extends TestNGSuite {
    
      @DataProvider(name = "provider")
      def provideData = {
        val g = new ArrayBuilder.ofRef[Array[Object]]()
        g += (Array[Object]("Heart", 5.asInstanceOf[java.lang.Integer]))
        g += (Array[Object]("Jimmy Buffet", 12.asInstanceOf[java.lang.Integer]))
        g.result()
      }
    
      @Test(dataProvider = "provider")
      def testTheStringLength(n1: String, n2: java.lang.Integer) {
        assertEquals(n1.length, n2)
      }
    
    }

Fixtures


不同的測試可能會有相同的 object dependencies 或是 測試資料,fixture 就是一次建立 subjects 並能重複使用的機制。


  • Anonymous Objects

import org.scalatest.{ShouldMatchers, FunSpec}

class AlbumFixtureSpec extends FunSpec with ShouldMatchers {

  class Album(val name: String, val year: Int) {
    if (year <= 0) throw new IllegalArgumentException("Album year is required")
  }

  // 定義測試資料
  def fixture = new {
    val letterFromHome = new Album("Letter from Home", 1989)
  }

  describe("The Letter From Home Album by Pat Metheny") {
    it("should get the year 1989 from the album") {

      // 取出測試資料
      val album = fixture.letterFromHome
      album.year should be(1989)
    }
  }
}

  • Fixture Traits


    利用 trait 產生匿名物件


import org.scalatest.{ShouldMatchers, FunSpec}

class AlbumFixtureTraitSpec extends FunSpec with ShouldMatchers {

  class Album(val name: String, val year: Int) {
    if (year <= 0) throw new IllegalArgumentException("Album year is required")
  }

  trait AlbumFixture {
    val letterFromHome = new Album("Letter from Home", 1989)
  }

  describe("The Letter From Home Album by Pat Metheny") {
    it("should get the year 1989 from the album") {
      new AlbumFixture {
        letterFromHome.year should be(1989)
      }
    }
  }
}

  • OneInstancePerTest


    以 OneInstancePerTest 讓每個測試都有自己的資料


import org.scalatest.{OneInstancePerTest, ShouldMatchers, FreeSpec}

import scala.collection.mutable.ListBuffer

class AlbumListOneInstancePerTestFreeSpec extends FreeSpec with ShouldMatchers with OneInstancePerTest {

  class Album(val title: String, val year: Int, val acts: Artist*) {
    if (year <= 0) throw new IllegalArgumentException("Album year is required")
  }

  class Artist(val firstName: String, val lastName: String)

  val graceJonesDiscography = new ListBuffer[Album]()

  graceJonesDiscography += (new Album("Portfolio", 1977, new Artist("Grace", "Jones")))
  "Given an initial Grace Jones Discography" - {
    "when an additional two albums are added, then the discography size should be 3" in {
      graceJonesDiscography += (new Album("Fame", 1978, new Artist("Grace", "Jones")))
      graceJonesDiscography += (new Album("Muse", 1979, new Artist("Grace", "Jones")))
      graceJonesDiscography.size should be(3)
    }

    "when one additional album is added, then the discography size should be 2" in {
      // graceJonesDiscography 會先重建,然後再加入一個新的 Album
      graceJonesDiscography += (new Album("Warm Leatherette", 1980, new Artist("Grace", "Jones")))
      graceJonesDiscography.size should be(2)
    }
  }

  "Given an initial Grace Jones Discography " - {
    "when one additional album from 1980 is added, then the discography size should be 2" in {
      graceJonesDiscography += (new Album("Nightclubbing", 1981, new Artist("Grace", "Jones")))
      graceJonesDiscography.size should be(2)
    }
  }
}

  • Before and After


    控制測試前後的行為


import collection.mutable.ListBuffer
import org.scalatest.{BeforeAndAfter, WordSpec, ShouldMatchers}

class AlbumBeforeAndAfterFixtureSpec extends WordSpec with ShouldMatchers with BeforeAndAfter {

  class Album(val title: String, val year: Int, val acts: Artist*) {
    if (year <= 0) throw new IllegalArgumentException("Album year is required")
  }

  class Artist(val firstName: String, val lastName: String)

  val humanLeagueDiscography = new ListBuffer[Album]()
  
  before {
    info("Starting to populate the discography")
    humanLeagueDiscography += (new Album("Dare", 1981, new Artist("Grace", "Jones")))
  }
  
  "A mutable ListBuffer of albums" should {
    "have a size of 3 when two more albums are added to the Human League Discography" in {
      humanLeagueDiscography += (new Album("Hysteria", 1984, new Artist("Grace", "Jones")))
      humanLeagueDiscography += (new Album("Crash", 1986, new Artist("Grace", "Jones")))
      humanLeagueDiscography should have size (3)
    }
    "have a size of 2 when one more album is added to the Human League Discography" in {
      humanLeagueDiscography += (new Album("Romantic", 1990, new Artist("Grace", "Jones")))
      humanLeagueDiscography should have size (2)
    }
  }
  
  after {
    info("Clearing the discography")
    humanLeagueDiscography.clear()
  }
}

References


Testing in Scala


scalatest selecting a style


ScalaTest測試框架


專題:如何寫測試——Spark


ScalaTest學習筆記


Spark com.fasterxml.jackson.module error

2016/9/19

SBT

SBT


SBT 是 scala 上一個通用的 build tool,除了沿用 ivy 進行 library dependency 管理之外,還增加了更多 console 互動的指令,而且可以直接啟動一個新的 interpreter,進行 code testing。


Installation


在 mac 安裝 sbt,可以用 port 或是 brew


port install sbt

brew install sbt

在 windows,就下載 msi 安裝包,直接安裝


如果要自己手動安裝,必須先下載 sbt-launch.jar,參考 Installing sbt manually 的說明,建立 script


> vi ~/bin/sbt
#!/bin/bash
SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M -Dfile.encoding=UTF8"
java $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"

first simple project test


建立一個 sbttest 目錄,在該目錄中,執行 sbt,會進入 sbt console


> help
    列印 sbt 基本的指令
> tasks
    列出可以使用的 build task
> settings
    列出我們可以修改的 settings
> inspect
    查詢 setting/task 的資訊

列印 scala source folder


> scalaSource
[info] /project/idea/sbttest/src/main/scala

sbt project 的基本結構如下


<build directory>/
    project/            sbt plugins and build help code
    src/
        main/
            scala/      scala source code
            java/       java source code
            resources/  要放在 classpath 但又不需要編譯的資源檔案
        test/
            scala/
            java/
            resources/
    target/
    
    build.sbt           build file

buildscript.sh


#!/bin/bash

mkdir -p project
mkdir -p src/{main,test}/{scala,java,resources}
mkdir -p target

建立兩個檔案


vi build.sbt


name := "sbttest"

version := "1.0"

vi project/build.properties


sbt.version=0.13.7

建立一個 HelloWorld scala source


vi src/main/scala/HelloWorld.scala


object HelloWorld extends App {
    println("Hello, sbt world!")
}

回到 sbt console,執行 compile task,然後就能直接 run


> compile
[info] Updating {file:/project/idea/sbttest/}sbttest...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to /project/idea/sbttest/target/scala-2.10/classes...
[success] Total time: 2 s, completed 2016/5/12 上午 10:54:37

> run
[info] Running HelloWorld
Hello, sbt world!
[success] Total time: 0 s, completed 2016/5/12 上午 10:55:48



增修以下的檔案


vi src/main/scala/models.scala


case class Product(id: Long, 
                  attributes: Seq[String])
case class BuyerPreferences(attributes: Seq[String])

vi src/main/scala/logic.scala


object Logic {
  // 判斷是否有吻合 buyer 的喜好特徵
  def matchLikelihood(product: Product, buyer: BuyerPreferences): Double = {
    val matches = buyer.attributes map { attribute =>
      product.attributes contains attribute
    }
    val nums = matches map { b => if(b) 1.0 else 0.0 }
    if (nums.length > 0) nums.sum / nums.length else 0.0
  }
}

在 sbt 中,可以直接以 console 指令進入 scala interpreter


> console
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_92).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

可以直接使用剛剛編譯的 scala 物件


// 產生 Product p1
scala> val p1 = Product(id=100, attributes = Seq("female", "kid-friendly"))
p1: Product = Product(100,List(female, kid-friendly))

// 產生 BuyerPreferences
scala> val prefs = BuyerPreferences(List("male", "kid-friendly"))
prefs: BuyerPreferences = BuyerPreferences(List(male, kid-friendly))

// 產生 Product p2
scala> val p2 = Product(id=110, attributes = Seq("male", "kid-friendly"))
p2: Product = Product(110,List(male, kid-friendly))

// 檢查 p1 的 attributes 是否有吻合 prefs 的 atrributes
scala> prefs.attributes.map(attribute => p1.attributes.contains(attribute))
res1: Seq[Boolean] = List(false, true)

// 將吻合的特性標記為 分數 1.0
scala> res1 map (matched => if(matched) 1.0 else 0)
res2: Seq[Double] = List(0.0, 1.0)

// 計算總得分
scala> res2.sum / res2.length
res3: Double = 0.5

利用 specs 進行 unit test


首先在 build.sbt 增加一行


libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"

增加一個測試程式


import org.specs2.mutable.Specification

object LogicSpec extends Specification {
  "The 'matchLikelihood' method" should {
    "be 100% when all attributes match" in {
      val tabby = Product(1, List("male", "tabby"))
      val prefs = BuyerPreferences(List("male", "tabby"))
      Logic.matchLikelihood(tabby, prefs) must beGreaterThan(0.999)
    }
    "be 0% when no attributes match" in {
      val tabby = Product(1, List("male", "tabby"))
      val prefs = BuyerPreferences(List("female", "calico"))
      val result = Logic.matchLikelihood(tabby, prefs)
      result must beLessThan(0.001)
    }
    "correctly handle an empty BuyerPreferences" in {
      val tabby = Product(1, List("male", "tabby"))
      val prefs = BuyerPreferences(List())
      val result = Logic.matchLikelihood(tabby, prefs)
      result.isNaN mustEqual false
    }
  }
}

回到 sbt console(記得要跳出 scala interpreter),以 reload 重新載入 project 設定,以 test 進行 unit test


> reload
[info] Loading project definition from /project/idea/book/sbt-in-action-examples-master/chapter2/project
[info] Set current project to preowned-kittens (in build file:/project/idea/book/sbt-in-action-examples-master/chapter2/)
> test
[info] Compiling 1 Scala source to /project/idea/book/sbt-in-action-examples-master/chapter2/target/scala-2.10/test-classes...
[info] LogicSpec
[info]
[info] The 'matchLikelihood' method should
[info] + be 100% when all attributes match
[info] + be 0% when no attributes match
[info] + correctly handle an empty BuyerPreferences
[info]
[info]
[info] Total for specification LogicSpec
[info] Finished in 68 ms
[info] 3 examples, 0 failure, 0 error
[info]
[info] Passed: Total 3, Failed 0, Errors 0, Passed 3
[success] Total time: 4 s, completed 2016/5/12 上午 11:23:04

sbt 的測試還有另一個模式 ~test,可以持續等待 source code 的更新,並在更新後,自動執行 unit test,當 source code 有任何異動時,他會自動編譯並執行測試,在這個模式下的測試非常地有效率。


> ~test
[info] LogicSpec
[info]
[info] The 'matchLikelihood' method should
[info] + be 100% when all attributes match
[info] + be 0% when no attributes match
[info] + correctly handle an empty BuyerPreferences
[info]
[info]
[info] Total for specification LogicSpec
[info] Finished in 15 ms
[info] 3 examples, 0 failure, 0 error
[info]
[info] Passed: Total 3, Failed 0, Errors 0, Passed 3
[success] Total time: 1 s, completed 2016/5/12 上午 11:36:08
1. Waiting for source changes... (press enter to interrupt)

如果只要進行某一個單元測試,就要用 testOnly


> testOnly LogicSpec

如何定義 build.sbt


settings 裡面有三個 operators 用來建立 settings


  1. :=
    將新的 value 複寫原本的 key
  2. +=
    將新的 value 附加到原本 key 所儲存的 sequence 裡面
  3. ++=
    將新的 sequence of values 附加到原本 key 所儲存的 sequence 裡面

定義 ModuleID 的格式為


"groupId" % "artifactId" % "version" 

例如 libraryDependencies 裡面就存放著 sequence of library dependencies,如果要定義兩個以上的 libraries,就要用以下的寫法


libraryDependencies ++= Seq(
    "junit" % "junit" % "4.11" % "test",
    "org.specs2" % "specs2_2.10" % "1.10" % "test"
)



自訂 key 的方式如下:gitHeadCommitSha 是 key 的名稱,型別為 String,而真實的值是由 scala.sys.process 裡面的 Process("git rev-parse HEAD") 運算得來的


val gitHeadCommitSha = taskKey[String]("Determines the current git commit SHA")

gitHeadCommitSha := Process("git rev-parse HEAD").lines.head

如果在一個 git project 裡面直接執行此指令,可以取得 hash value,所以上面的 gitHeadCommitSha 其實就是取得這個 hash value


> git rev-parse HEAD
8a0c542b032c78262f9e3a9a60cef318290c7d99

parallel execution


當 taskA depends on taskB, taskC,sbt 會嘗試同時執行 taskB與taskC,以下是驗證的方式:taskB, taskC 都會暫停 5 秒鐘,但 taskA 執行的時候,也是暫停 5s,這表示 taskB, taskC 確實是平行執行的


val taskA = taskKey[String]("taskA")
val taskB = taskKey[String]("taskB")
val taskC = taskKey[String]("taskC")
taskA := { val b = taskB.value; val c = taskC.value; "taskA" }
taskB := { Thread.sleep(5000); "taskB" }
taskC := { Thread.sleep(5000); "taskC" }

執行結果


> taskA
[success] Total time: 5 s, completed 2016/5/12 下午 03:19:12
> taskB
[success] Total time: 5 s, completed 2016/5/12 下午 03:19:30

subproject


sbt 支援 subproject,可以在子資料夾中,定義另一個 subproject,並在編譯時決定 project dependency。


在 build.sbt 中定義 common project,並以子資料夾 common 為 subproject 目錄


lazy val common = (
    Project("common", file("common")).
    settings()
)

在 sbt console可以用 projects 指令查閱


> projects
[info] In file:/project/idea/book/sbt-in-action-examples-master/chapter3/
[info]   * chapter3
[info]     temp
[info]     website



如果先定義一個共用的 project function: PreownedKittenProject,就可以在後面其他的 project 定義中,直接呼叫該 project function。


// Common settings/definitions for the build

def PreownedKittenProject(name: String): Project = (
  Project(name, file(name))
  settings(
    libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
  )
)


lazy val common = (
  PreownedKittenProject("common")
  settings()
)

lazy val analytics = (
  PreownedKittenProject("analytics")
  dependsOn(common)
  settings()
)

lazy val website = (
  PreownedKittenProject("website")
  dependsOn(common)
  settings()
)

compile, run, test, package, publish


利用 inspect tree compile:compile 指令,可查詢編譯時,所需要的資源 tree,而 inspect tree sources 可查閱原始程式的 tree。


> inspect tree compile:compile
[info] chapter4/compile:compile = Task[sbt.inc.Analysis]
[info]   +-chapter4/compile:compile::compileInputs = Task[sbt.Compiler$Inputs]
[info]   | +-chapter4/compile:classDirectory = target/scala-2.10/classes
[info]   | +-*/*:compileOrder = Mixed
[info]   | +-chapter4/*:compilers = Task[sbt.Compiler$Compilers]
[info]   | +-chapter4/compile:dependencyClasspath = Task[scala.collection.Seq[sbt.Attributed[jav..
[info]   | +-chapter4/compile:incCompileSetup = Task[sbt.Compiler$IncSetup]
[info]   | +-*/*:javacOptions = Task[scala.collection.Seq[java.lang.String]]
[info]   | +-*/*:maxErrors = 100
[info]   | +-chapter4/compile:scalacOptions = Task[scala.collection.Seq[java.lang.String]]
[info]   | +-*/*:sourcePositionMappers = Task[scala.collection.Seq[scala.Function1[xsbti.Positio..
[info]   | +-chapter4/compile:sources = Task[scala.collection.Seq[java.io.File]]
[info]   | +-chapter4/compile:compile::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <..
[info]   |   +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info]   |
[info]   +-chapter4/compile:compile::compilerReporter = Task[scala.Option[xsbti.Reporter]]
[info]   +-chapter4/compile:compile::streams = Task[sbt.std.TaskStreams[sbt.Init$ScopedKey[_ <: ..
[info]     +-*/*:streamsManager = Task[sbt.std.Streams[sbt.Init$ScopedKey[_ <: Any]]]
[info]

> inspect tree sources
[info] chapter4/compile:sources = Task[scala.collection.Seq[java.io.File]]
[info]   +-chapter4/compile:managedSources = Task[scala.collection.Seq[java.io.File]]
[info]   | +-chapter4/compile:sourceGenerators = List()
[info]   |
[info]   +-chapter4/compile:unmanagedSources = Task[scala.collection.Seq[java.io.File]]
[info]     +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-examples-..
[info]     +-*/*:sourcesInBase = true
[info]     +-chapter4/compile:unmanagedSourceDirectories = List(/Users/charley/project/idea/book..
[info]     | +-chapter4/compile:javaSource = src/main/java
[info]     | | +-chapter4/compile:sourceDirectory = src/main
[info]     | |   +-chapter4/*:sourceDirectory = src
[info]     | |   | +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-e..
[info]     | |   |   +-chapter4/*:thisProject = Project(id chapter4, base: /Users/charley/projec..
[info]     | |   |
[info]     | |   +-chapter4/compile:configuration = compile
[info]     | |
[info]     | +-chapter4/compile:scalaSource = src/main/scala
[info]     |   +-chapter4/compile:sourceDirectory = src/main
[info]     |     +-chapter4/*:sourceDirectory = src
[info]     |     | +-chapter4/*:baseDirectory = /Users/charley/project/idea/book/sbt-in-action-e..
[info]     |     |   +-chapter4/*:thisProject = Project(id chapter4, base: /Users/charley/projec..
[info]     |     |
[info]     |     +-chapter4/compile:configuration = compile
[info]     |
[info]     +-*/*:excludeFilter = sbt.HiddenFileFilter$@731a5a39
[info]     +-*/*:unmanagedSources::includeFilter = sbt.SimpleFilter@1acd952e
[info]


> inspect tree test:sources
> 
> inspect tree compile:dependencyClasspath

  1. unmanagedSources
    既有的 project convernsions 取得的 a list of source files
  2. managedSources
    手動加入的 sources list

# 查閱 java source folders
> show javaSource

# 查閱 scala source folders
> show scalaSource

# 查閱資源目錄
> show resourceDirectory

sbt 預設會使用以下的 libray repositories


  • Bintray's JCenter
  • Maven Central
  • Typesafe releases
  • sbt community releases

Reference


sbt in action