// 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: