// build.sbt
name := "my-project"
scalaVersion := "2.11.8"
Examples of usage:
// build.sbt
lazy val root = project.in(file("."))
.aggregate(app, model, common)
lazy val app = project.in(file("modules/app"))
.dependsOn(model)
lazy val model = project.in(file("modules/model"))
.dependsOn(common)
lazy val common = project.in(file("modules/common"))
// project/Dependencies.scala
import sbt._
import sbt.Keys._
trait Dependencies {
val commonResolvers = Seq(
Resolver sonatypeRepo "public",
Resolver typesafeRepo "releases"
)
val commonLibraries = Seq(
"org.scalaz" %% "scalaz-core" % "7.1.3")
val testLibraries = Seq(
"org.mockito" % "mockito-core" % "1.10.8")
}
// project/Settings.scala
import sbt._
import sbt.Keys._
trait Settings { self: Dependencies =>
val settings = Seq(
organization := "com.example",
version := "0.1.0-SNAPSHOT",
scalaVersion := "2.11.7",
scalaOptions ++= Seq(...),
resolvers ++= commonResolvers,
libraryDependencies ++= commonLibraries,
libraryDependencies ++= testLibraries map (_ % "test"))
}
// project/Common.scala
object Common with Settings with Dependencies
// build.sbt
lazy val root = project.in(file("."))
.aggregate(app, model, common)
lazy val app = project.in(file("modules/app"))
.settings(name := "my-project")
.settings(Common.settings: _*)
.dependsOn(model)
...
For starters: unit testing.
class UtilsSpec extends Specification {
"Utils.convertToX" should {
"be a positive number" in {
// given
val y = ...
// when
val x = UtilsSpec.convertToX(y)
// then
x must beGreaterThan(0)
}
}
}
> sbt test # test all modules
> sbt common:test # test only common
Now let's check coverage.
// project/plugins.sbt
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3")
// build.sbt
import scoverage.ScoverageSbtPlugin
...
lazy val app = project.in(file("modules/app"))
.settings(name := "my-project")
.settings(Common.settings: _*)
.dependsOn(model)
.enablePlugins(ScoverageSbtPlugin)
> sbt clean coverage test # single module project
> sbt clean coverage test coverageAggregate # multi module project
Finally, let's make sure that code follows some style guidelines.
// project/plugins.sbt
addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.5.1")
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.7.0")
// project/Settings.scala
import com.typesafe.sbt.SbtScalariform._
import scalariform.formatter.preferences._
import org.scalastyle.sbt.ScalastylePlugin._
trait Settings { self: Dependencies =>
val settings = Seq(
...,
ScalariformKeys.preferences := ScalariformKeys.preferences.value
.setPreference(DoubleIndentClassDeclaration, true)
.setPreference(IndentLocalDefs, false)
scalastyleFailOnError := true
)
}
> sbt compile # scalariform run on each compilation
> sbt scalastyleGenerateConfig # generate scalastyle-config.xml file
> sbt scalastyle # can be configured to fail on error
// usually you don't want returns like this one...
def isThereADigit1(source: String): Boolean = {
for (c <- source) {
if (c.isDigit)
return true
}
return false
}
// ...you probably wanted some predicate instead
def isThereADigit2(source: String) = source.exists(_.isDigit)
// you probably didn't want a return here as well...
def findANumber1(source: String): Long = {
source split "," foreach { s =>
if (s.matches("-?[0-9]+"))
return s.toLong
}
return 0
}
// ...you most likely needed to "find" first occurrence
def findANumber2(source: String): Option[Long] =
source split "," find (_.matches("-?[0-9]+")) map (_.toLong)
// what about aggregation?
var sum = 0
for (i <- 0 until 50) {
sum += i
}
sum
// well, numbers are quite easy to add up
(0 until 50).fold(0)(_ + _)
(0 until 50).reduce(_ + _)
(0 until 50).sum
// numbers are also easy to multiply...
var product = 1
for (i <- 1 until 7) {
product *= i
}
product
// ...without iterating manually
(1 until 7).fold(1)(_ * _)
(1 until 7).reduce(_ * _)
(1 until 7).product
// let's think about exceptional cases
def extractNumbers1a(source: String) = try {
source split "," map (_.toLong) toSeq
} catch {
case _: NumberFormatException => Seq()
}
def extractNumbers2a(source: String) = source split "," flatMap { s =>
try {
Some(s.toLong)
} catch {
case _: NumberFormatException => None
}
} toSeq
// try/catch -> Try is a small improvement...
def extractNumbers1b(source: String) =
Try(source split "," map (_.toLong) toSeq) getOrElse Seq.empty
def extractNumbers2b(source: String) = source split "," flatMap { s =>
Try(s.toLong).toOption
} toSeq
// ...but expressing posibility of a failure directly is even better
def extractNumbers1c(source: String) =
Try(source split "," map (_.toLong) toSeq)
def extractNumbers2c(source: String) = {
val (right, left) = source split "," partition { s =>
Try(s.toLong).isSuccess
}
if (left.isEmpty) Right(right map (_.toLong) toSeq)
else Left(left.toSeq)
}
trait MyService { ... }
trait MyServiceComponent {
def myService: MyService
}
trait MyServiceComponentImpl { self: MyOtherServiceComponent =>
object myService extends MyService { ... }
}
trait MyOtherServiceComponentMock extends Mockito {
val myService = mock[MyService]
}
val testComponent = new MyServiceComponentImpl
with MyOtherServiceComponentMock
{}
class ComponentRegistry
extends MyServiceComponentImpl
with MyOtherServiceComponentImpl
trait CommonComponentRegistryImpl extends CommonComponentRegistry
with MyServiceComponentImpl
with MyOtherServiceComponentImpl
...
trait ModelComponentRegistryImpl extends ModelComponentRegistry
with UserRepositoryComponentImpl
with AddressRepositoryComponentImpl
...
{ self: CommonComponentRegistry
with DBDriverComponent
...
}
trait CommonComponentRegistryImpl extends CommonComponentRegistry
with MyServiceComponentImpl
with MyOtherServiceComponentImpl
...
trait ModelComponentRegistryImpl extends ModelComponentRegistry
with UserRepositoryComponentImpl
with AddressRepositoryComponentImpl
...
{ self: CommonComponentRegistry
with DBDriverComponent
...
}
class UserRecordsRepository(...) {
def fetch(id: Long): Option[(Long, String, Long, Date)]
def save(record: (String, Long, Date)): (Long, String, Long, Date)
def update(record: (String, Long, Date)): Boolean
}
case class User(name: String, addressId: Long, lastUpdate: Date)
case class UserRecord(
id: Option[Long], name: String, addressId: Long, lastUpdate: Date)
class UserRecordsRepository(...) {
def fetch(id: Long): Option[UserRecord]
def save(record: User): UserRecord
def update(record: User): Boolean
}
// we love debugging those
def prepareMailSubscriptionsForFriends(userCollection: Seq[User]) =
userCollection
.flatMap(getFriendsAddresses)
.filter(onlyContactAddresses)
.map(enrichAddressesWithDetails)
.filter(onlySupportedLocations)
... //
// we love debugging those even more
def createFriendsActivity: JSonValue = for {
currentUserId <- context.currentUserId.toSeq
user <- userRepository.find(currentUserId)
address <- addressRepository.find(user.addressId)
friends = userRepository.findFriendsOf(user.id)
friend <- friends if newerThanYesterday(friend.lastVisited)
... // 30 more lines of for comprehension
} yield JSon.obj(
"user" -> JSon.obj(...),
"address" -> JSon.obj(...),
...
)
// if only there was a way to handle it at compile time...
def handleValue(value: Any) = value match {
case l: Long => ...
case s: String => ...
case _ => throw UnsupportedOperationException("type not supported")
}
// ...with type safety and everything
def handleValue(value: Long) = ...
def handleValue(value: String) = ...
Try to avoid: