From 4cffccc8c8c9e3dd5f7e61d391d1e42a8f1e42f2 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 27 Jun 2016 15:27:42 +0200 Subject: [PATCH] Caching based on sjsonnew --- build.sbt | 12 +- .../sbt/internal/util/AdditionalFormats.scala | 52 ++++ .../internal/util/BasicCacheImplicits.scala | 59 ++++ .../main/scala/sbt/internal/util/Cache.scala | 291 ++++-------------- .../scala/sbt/internal/util/CacheIO.scala | 45 --- .../sbt/internal/util/CacheImplicits.scala | 17 + .../scala/sbt/internal/util/CacheStore.scala | 93 ++++++ .../scala/sbt/internal/util/FileInfo.scala | 239 ++++++++------ .../main/scala/sbt/internal/util/Input.scala | 45 +++ .../main/scala/sbt/internal/util/Output.scala | 32 ++ .../sbt/internal/util/SeparatedCache.scala | 100 +++--- .../sbt/internal/util/StampedFormat.scala | 44 +++ .../util-cache/src/test/scala/CacheSpec.scala | 76 +++++ .../util-cache/src/test/scala/CacheTest.scala | 32 -- .../src/test/scala/SingletonCacheSpec.scala | 91 ++++++ .../scalajson/unsafe/FixedParser.scala | 29 ++ .../scala/sbt/internal/util/Tracked.scala | 206 +++++-------- .../scala/sbt/internal/util/TrackedSpec.scala | 140 +++++++++ .../scalajson/unsafe/FixedParser.scala | 29 ++ project/Dependencies.scala | 4 +- 20 files changed, 1037 insertions(+), 599 deletions(-) create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/AdditionalFormats.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/BasicCacheImplicits.scala delete mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/CacheIO.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/CacheImplicits.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/CacheStore.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/Input.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/Output.scala create mode 100644 internal/util-cache/src/main/scala/sbt/internal/util/StampedFormat.scala create mode 100644 internal/util-cache/src/test/scala/CacheSpec.scala delete mode 100644 internal/util-cache/src/test/scala/CacheTest.scala create mode 100644 internal/util-cache/src/test/scala/SingletonCacheSpec.scala create mode 100644 internal/util-cache/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala create mode 100644 internal/util-tracking/src/test/scala/sbt/internal/util/TrackedSpec.scala create mode 100644 internal/util-tracking/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala diff --git a/build.sbt b/build.sbt index bc5816052..2a335cd79 100644 --- a/build.sbt +++ b/build.sbt @@ -111,22 +111,24 @@ lazy val utilLogic = (project in internalPath / "util-logic"). name := "Util Logic" ) -// Persisted caching based on SBinary +// Persisted caching based on sjson-new lazy val utilCache = (project in internalPath / "util-cache"). - dependsOn(utilCollection). + dependsOn(utilCollection, utilTesting % Test). settings( commonSettings, name := "Util Cache", - libraryDependencies ++= Seq(sbinary, sbtSerialization, scalaReflect.value, sbtIO) ++ scalaXml.value + libraryDependencies ++= Seq(datatypeCodecs, sbtSerialization, scalaReflect.value, sbtIO) ++ scalaXml.value, + libraryDependencies += sjsonnewScalaJson % Test ) // Builds on cache to provide caching for filesystem-related operations lazy val utilTracking = (project in internalPath / "util-tracking"). - dependsOn(utilCache). + dependsOn(utilCache, utilTesting % Test). settings( commonSettings, name := "Util Tracking", - libraryDependencies += sbtIO + libraryDependencies += sbtIO, + libraryDependencies += sjsonnewScalaJson % Test ) // Internal utility for testing diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/AdditionalFormats.scala b/internal/util-cache/src/main/scala/sbt/internal/util/AdditionalFormats.scala new file mode 100644 index 000000000..6e74c26c7 --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/AdditionalFormats.scala @@ -0,0 +1,52 @@ +package sbt.internal.util + +import sbt.datatype.StringFormat +import sbt.internal.util.Types.:+: + +import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder } +import sjsonnew.BasicJsonProtocol.{ wrap, asSingleton } + +import java.io.File + +import java.net.{ URI, URL } + +trait URIFormat { self: StringFormat => + implicit def URIFormat: JsonFormat[URI] = wrap(_.toString, new URI(_: String)) +} + +trait URLFormat { self: StringFormat => + implicit def URLFormat: JsonFormat[URL] = wrap(_.toString, new URL(_: String)) +} + +trait FileFormat { self: StringFormat => + implicit def FileFormat: JsonFormat[File] = wrap(_.toString, new File(_: String)) +} + +trait HListFormat { + implicit def HConsFormat[H: JsonFormat, T <: HList: JsonFormat]: JsonFormat[H :+: T] = + new JsonFormat[H :+: T] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): H :+: T = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val h = unbuilder.readField[H]("h") + val t = unbuilder.readField[T]("t") + unbuilder.endObject() + + HCons(h, t) + + case None => + deserializationError("Expect JValue but found None") + } + + override def write[J](obj: H :+: T, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("h", obj.head) + builder.addField("t", obj.tail) + builder.endObject() + } + } + + implicit val HNilFormat: JsonFormat[HNil] = asSingleton(HNil) + +} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/BasicCacheImplicits.scala b/internal/util-cache/src/main/scala/sbt/internal/util/BasicCacheImplicits.scala new file mode 100644 index 000000000..7829e8e22 --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/BasicCacheImplicits.scala @@ -0,0 +1,59 @@ +package sbt.internal.util + +import sbt.datatype.{ ArrayFormat, BooleanFormat, ByteFormat, IntFormat } + +import java.net.{ URI, URL } + +import sjsonnew.JsonFormat +import sjsonnew.BasicJsonProtocol.asSingleton + +trait BasicCacheImplicits { self: ArrayFormat with BooleanFormat with ByteFormat with IntFormat => + + implicit def basicCache[I: JsonFormat: Equiv, O: JsonFormat]: Cache[I, O] = + new BasicCache[I, O]() + + def defaultEquiv[T]: Equiv[T] = + new Equiv[T] { def equiv(a: T, b: T) = a == b } + + def wrapEquiv[S, T](f: S => T)(implicit eqT: Equiv[T]): Equiv[S] = + new Equiv[S] { + def equiv(a: S, b: S) = + eqT.equiv(f(a), f(b)) + } + + implicit def optEquiv[T](implicit t: Equiv[T]): Equiv[Option[T]] = + new Equiv[Option[T]] { + def equiv(a: Option[T], b: Option[T]) = + (a, b) match { + case (None, None) => true + case (Some(va), Some(vb)) => t.equiv(va, vb) + case _ => false + } + } + implicit def urlEquiv(implicit uriEq: Equiv[URI]): Equiv[URL] = wrapEquiv[URL, URI](_.toURI)(uriEq) + implicit def uriEquiv: Equiv[URI] = defaultEquiv + implicit def stringSetEquiv: Equiv[Set[String]] = defaultEquiv + implicit def stringMapEquiv: Equiv[Map[String, String]] = defaultEquiv + + implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] = + wrapEquiv((x: Array[T]) => x: Seq[T])(seqEquiv[T](t)) + + implicit def seqEquiv[T](implicit t: Equiv[T]): Equiv[Seq[T]] = + new Equiv[Seq[T]] { + def equiv(a: Seq[T], b: Seq[T]) = + a.length == b.length && + ((a, b).zipped forall t.equiv) + } + + def wrapIn[I, J](implicit f: I => J, g: J => I, jCache: SingletonCache[J]): SingletonCache[I] = + new SingletonCache[I] { + override def read(from: Input): I = g(jCache.read(from)) + override def write(to: Output, value: I) = jCache.write(to, f(value)) + override def equiv: Equiv[I] = wrapEquiv(f)(jCache.equiv) + } + + def singleton[T](t: T): SingletonCache[T] = + SingletonCache.basicSingletonCache(asSingleton(t), trueEquiv) + + def trueEquiv[T] = new Equiv[T] { def equiv(a: T, b: T) = true } +} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/Cache.scala b/internal/util-cache/src/main/scala/sbt/internal/util/Cache.scala index 411771300..0a04dbcdd 100644 --- a/internal/util-cache/src/main/scala/sbt/internal/util/Cache.scala +++ b/internal/util-cache/src/main/scala/sbt/internal/util/Cache.scala @@ -3,247 +3,72 @@ */ package sbt.internal.util -import sbinary.{ CollectionTypes, DefaultProtocol, Format, Input, JavaFormats, Output => Out } -import java.io.{ ByteArrayInputStream, ByteArrayOutputStream, File, InputStream, OutputStream } -import java.net.{ URI, URL } -import Types.:+: -import DefaultProtocol.{ asSingleton, BooleanFormat, ByteFormat, IntFormat, wrap } -import scala.xml.NodeSeq -import scala.language.existentials +/** The result of a cache query */ +sealed trait CacheResult[K] +/** A successful hit on the cache */ +case class Hit[O](value: O) extends CacheResult[O] + +/** + * A cache miss. + * `update` associates the missing key with `O` in the cache. + */ +case class Miss[O](update: O => Unit) extends CacheResult[O] + +/** + * A simple cache with keys of type `I` and values of type `O` + */ trait Cache[I, O] { - def apply(file: File)(i: I): Either[O, O => Unit] + /** + * Queries the cache backed with store `store` for key `key`. + */ + def apply(store: CacheStore)(key: I): CacheResult[O] } -trait SBinaryFormats extends CollectionTypes with JavaFormats { - implicit def urlFormat: Format[URL] = DefaultProtocol.UrlFormat - implicit def uriFormat: Format[URI] = DefaultProtocol.UriFormat -} -object Cache extends CacheImplicits { + +object Cache { + + /** + * Materializes a cache. + */ def cache[I, O](implicit c: Cache[I, O]): Cache[I, O] = c - def cached[I, O](file: File)(f: I => O)(implicit cache: Cache[I, O]): I => O = - in => - cache(file)(in) match { - case Left(value) => value - case Right(store) => - val out = f(in) - store(out) - out + /** + * Returns a function that represents a cache that inserts on miss. + * + * @param store The store that backs this cache. + * @param default A function that computes a default value to insert on + */ + def cached[I, O](store: CacheStore)(default: I => O)(implicit cache: Cache[I, O]): I => O = + key => + cache(store)(key) match { + case Hit(value) => + value + + case Miss(update) => + val result = default(key) + update(result) + result } - def debug[I](label: String, c: InputCache[I]): InputCache[I] = - new InputCache[I] { - type Internal = c.Internal - def convert(i: I) = c.convert(i) - def read(from: Input) = - { - val v = c.read(from) - println(label + ".read: " + v) - v - } - def write(to: Out, v: Internal): Unit = { - println(label + ".write: " + v) - c.write(to, v) + def debug[I](label: String, cache: SingletonCache[I]): SingletonCache[I] = + new SingletonCache[I] { + override def read(from: Input): I = { + val value = cache.read(from) + println(label + ".read: " + value) + value } - def equiv: Equiv[Internal] = new Equiv[Internal] { - def equiv(a: Internal, b: Internal) = - { - val equ = c.equiv.equiv(a, b) - println(label + ".equiv(" + a + ", " + b + "): " + equ) - equ - } + + override def write(to: Output, value: I): Unit = { + println(label + ".write: " + value) + cache.write(to, value) + } + + override def equiv: Equiv[I] = new Equiv[I] { + def equiv(a: I, b: I) = { + val equ = cache.equiv.equiv(a, b) + println(label + ".equiv(" + a + ", " + b + "): " + equ) + equ + } } } } -trait CacheImplicits extends BasicCacheImplicits with SBinaryFormats with HListCacheImplicits with UnionImplicits -trait BasicCacheImplicits { - implicit def basicCache[I, O](implicit in: InputCache[I], outFormat: Format[O]): Cache[I, O] = - new BasicCache()(in, outFormat) - def basicInput[I](implicit eq: Equiv[I], fmt: Format[I]): InputCache[I] = InputCache.basicInputCache(fmt, eq) - - def defaultEquiv[T]: Equiv[T] = new Equiv[T] { def equiv(a: T, b: T) = a == b } - - implicit def optInputCache[T](implicit t: InputCache[T]): InputCache[Option[T]] = - new InputCache[Option[T]] { - type Internal = Option[t.Internal] - def convert(v: Option[T]): Internal = v.map(x => t.convert(x)) - def read(from: Input) = - { - val isDefined = BooleanFormat.reads(from) - if (isDefined) Some(t.read(from)) else None - } - def write(to: Out, j: Internal): Unit = - { - BooleanFormat.writes(to, j.isDefined) - j foreach { x => t.write(to, x) } - } - def equiv = optEquiv(t.equiv) - } - - def wrapEquiv[S, T](f: S => T)(implicit eqT: Equiv[T]): Equiv[S] = - new Equiv[S] { - def equiv(a: S, b: S) = - eqT.equiv(f(a), f(b)) - } - - implicit def optEquiv[T](implicit t: Equiv[T]): Equiv[Option[T]] = - new Equiv[Option[T]] { - def equiv(a: Option[T], b: Option[T]) = - (a, b) match { - case (None, None) => true - case (Some(va), Some(vb)) => t.equiv(va, vb) - case _ => false - } - } - implicit def urlEquiv(implicit uriEq: Equiv[URI]): Equiv[URL] = wrapEquiv[URL, URI](_.toURI)(uriEq) - implicit def uriEquiv: Equiv[URI] = defaultEquiv - implicit def stringSetEquiv: Equiv[Set[String]] = defaultEquiv - implicit def stringMapEquiv: Equiv[Map[String, String]] = defaultEquiv - - def streamFormat[T](write: (T, OutputStream) => Unit, f: InputStream => T): Format[T] = - { - val toBytes = (t: T) => { val bos = new ByteArrayOutputStream; write(t, bos); bos.toByteArray } - val fromBytes = (bs: Array[Byte]) => f(new ByteArrayInputStream(bs)) - wrap(toBytes, fromBytes)(DefaultProtocol.ByteArrayFormat) - } - - implicit def xmlInputCache(implicit strEq: InputCache[String]): InputCache[NodeSeq] = wrapIn[NodeSeq, String](_.toString, strEq) - - implicit def seqCache[T](implicit t: InputCache[T]): InputCache[Seq[T]] = - new InputCache[Seq[T]] { - type Internal = Seq[t.Internal] - def convert(v: Seq[T]) = v.map(x => t.convert(x)) - def read(from: Input) = - { - val size = IntFormat.reads(from) - def next(left: Int, acc: List[t.Internal]): Internal = - if (left <= 0) acc.reverse else next(left - 1, t.read(from) :: acc) - next(size, Nil) - } - def write(to: Out, vs: Internal): Unit = { - val size = vs.length - IntFormat.writes(to, size) - for (v <- vs) t.write(to, v) - } - def equiv: Equiv[Internal] = seqEquiv(t.equiv) - } - - implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] = - wrapEquiv((x: Array[T]) => x: Seq[T])(seqEquiv[T](t)) - - implicit def seqEquiv[T](implicit t: Equiv[T]): Equiv[Seq[T]] = - new Equiv[Seq[T]] { - def equiv(a: Seq[T], b: Seq[T]) = - a.length == b.length && - ((a, b).zipped forall t.equiv) - } - implicit def seqFormat[T](implicit t: Format[T]): Format[Seq[T]] = - wrap[Seq[T], List[T]](_.toList, _.toSeq)(DefaultProtocol.listFormat) - - def wrapIn[I, J](implicit f: I => J, jCache: InputCache[J]): InputCache[I] = - new InputCache[I] { - type Internal = jCache.Internal - def convert(i: I) = jCache.convert(f(i)) - def read(from: Input) = jCache.read(from) - def write(to: Out, j: Internal) = jCache.write(to, j) - def equiv = jCache.equiv - } - - def singleton[T](t: T): InputCache[T] = - basicInput(trueEquiv, asSingleton(t)) - - def trueEquiv[T] = new Equiv[T] { def equiv(a: T, b: T) = true } -} - -trait HListCacheImplicits { - implicit def hConsCache[H, T <: HList](implicit head: InputCache[H], tail: InputCache[T]): InputCache[H :+: T] = - new InputCache[H :+: T] { - type Internal = (head.Internal, tail.Internal) - def convert(in: H :+: T) = (head.convert(in.head), tail.convert(in.tail)) - def read(from: Input) = - { - val h = head.read(from) - val t = tail.read(from) - (h, t) - } - def write(to: Out, j: Internal): Unit = { - head.write(to, j._1) - tail.write(to, j._2) - } - def equiv = new Equiv[Internal] { - def equiv(a: Internal, b: Internal) = - head.equiv.equiv(a._1, b._1) && - tail.equiv.equiv(a._2, b._2) - } - } - - implicit def hNilCache: InputCache[HNil] = Cache.singleton(HNil: HNil) - - implicit def hConsFormat[H, T <: HList](implicit head: Format[H], tail: Format[T]): Format[H :+: T] = new Format[H :+: T] { - def reads(from: Input) = - { - val h = head.reads(from) - val t = tail.reads(from) - HCons(h, t) - } - def writes(to: Out, hc: H :+: T): Unit = { - head.writes(to, hc.head) - tail.writes(to, hc.tail) - } - } - - implicit def hNilFormat: Format[HNil] = asSingleton(HNil) -} -trait UnionImplicits { - def unionInputCache[UB, HL <: HList](implicit uc: UnionCache[HL, UB]): InputCache[UB] = - new InputCache[UB] { - type Internal = Found[_] - def convert(in: UB) = uc.find(in) - def read(in: Input) = - { - val index = ByteFormat.reads(in).toInt - val (cache, clazz) = uc.at(index) - val value = cache.read(in) - new Found[cache.Internal](cache, clazz, value, index) - } - def write(to: Out, i: Internal): Unit = { - def write0[I](f: Found[I]): Unit = { - ByteFormat.writes(to, f.index.toByte) - f.cache.write(to, f.value) - } - write0(i) - } - def equiv: Equiv[Internal] = new Equiv[Internal] { - def equiv(a: Internal, b: Internal): Boolean = - { - if (a.clazz == b.clazz) - force(a.cache.equiv, a.value, b.value) - else - false - } - def force[T <: UB2, UB2](e: Equiv[T], a: UB2, b: UB2): Boolean = e.equiv(a.asInstanceOf[T], b.asInstanceOf[T]) - } - } - - implicit def unionCons[H <: UB, UB, T <: HList](implicit head: InputCache[H], mf: Manifest[H], t: UnionCache[T, UB]): UnionCache[H :+: T, UB] = - new UnionCache[H :+: T, UB] { - val size = 1 + t.size - def c = mf.runtimeClass - def find(value: UB): Found[_] = - if (c.isInstance(value)) new Found[head.Internal](head, c, head.convert(value.asInstanceOf[H]), size - 1) else t.find(value) - def at(i: Int): (InputCache[_ <: UB], Class[_]) = if (size == i + 1) (head, c) else t.at(i) - } - - implicit def unionNil[UB]: UnionCache[HNil, UB] = new UnionCache[HNil, UB] { - def size = 0 - def find(value: UB) = sys.error("No valid sum type for " + value) - def at(i: Int) = sys.error("Invalid union index " + i) - } - - final class Found[I](val cache: InputCache[_] { type Internal = I }, val clazz: Class[_], val value: I, val index: Int) - sealed trait UnionCache[HL <: HList, UB] { - def size: Int - def at(i: Int): (InputCache[_ <: UB], Class[_]) - def find(forValue: UB): Found[_] - } -} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/CacheIO.scala b/internal/util-cache/src/main/scala/sbt/internal/util/CacheIO.scala deleted file mode 100644 index afa5d12a6..000000000 --- a/internal/util-cache/src/main/scala/sbt/internal/util/CacheIO.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2009 Mark Harrah - */ -package sbt.internal.util - -import java.io.File -import sbinary.{ DefaultProtocol, Format, Operations } -import scala.reflect.Manifest -import sbt.io.IO - -object CacheIO { - def toBytes[T](format: Format[T])(value: T)(implicit mf: Manifest[Format[T]]): Array[Byte] = - toBytes[T](value)(format, mf) - def toBytes[T](value: T)(implicit format: Format[T], mf: Manifest[Format[T]]): Array[Byte] = - Operations.toByteArray(value)(stampedFormat(format)) - def fromBytes[T](format: Format[T], default: => T)(bytes: Array[Byte])(implicit mf: Manifest[Format[T]]): T = - fromBytes(default)(bytes)(format, mf) - def fromBytes[T](default: => T)(bytes: Array[Byte])(implicit format: Format[T], mf: Manifest[Format[T]]): T = - if (bytes.isEmpty) default else Operations.fromByteArray(bytes)(stampedFormat(format)) - - def fromFile[T](format: Format[T], default: => T)(file: File)(implicit mf: Manifest[Format[T]]): T = - fromFile(file, default)(format, mf) - def fromFile[T](file: File, default: => T)(implicit format: Format[T], mf: Manifest[Format[T]]): T = - fromFile[T](file) getOrElse default - def fromFile[T](file: File)(implicit format: Format[T], mf: Manifest[Format[T]]): Option[T] = - try { Some(Operations.fromFile(file)(stampedFormat(format))) } - catch { case e: Exception => None } - - def toFile[T](format: Format[T])(value: T)(file: File)(implicit mf: Manifest[Format[T]]): Unit = - toFile(value)(file)(format, mf) - def toFile[T](value: T)(file: File)(implicit format: Format[T], mf: Manifest[Format[T]]): Unit = - { - IO.createDirectory(file.getParentFile) - Operations.toFile(value)(file)(stampedFormat(format)) - } - def stampedFormat[T](format: Format[T])(implicit mf: Manifest[Format[T]]): Format[T] = - { - import DefaultProtocol._ - withStamp(stamp(format))(format) - } - def stamp[T](format: Format[T])(implicit mf: Manifest[Format[T]]): Int = typeHash(mf) - def typeHash[T](implicit mf: Manifest[T]) = mf.toString.hashCode - def manifest[T](implicit mf: Manifest[T]): Manifest[T] = mf - def objManifest[T](t: T)(implicit mf: Manifest[T]): Manifest[T] = mf -} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/CacheImplicits.scala b/internal/util-cache/src/main/scala/sbt/internal/util/CacheImplicits.scala new file mode 100644 index 000000000..190282a6e --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/CacheImplicits.scala @@ -0,0 +1,17 @@ +package sbt.internal.util + +import sbt.datatype.{ ArrayFormat, BooleanFormat, ByteFormat, IntFormat, LongFormat, StringFormat } +import sjsonnew.{ CollectionFormats, TupleFormats } + +object CacheImplicits extends BasicCacheImplicits + with ArrayFormat + with BooleanFormat + with ByteFormat + with FileFormat + with IntFormat + with LongFormat + with StringFormat + with URIFormat + with URLFormat + with TupleFormats + with CollectionFormats \ No newline at end of file diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/CacheStore.scala b/internal/util-cache/src/main/scala/sbt/internal/util/CacheStore.scala new file mode 100644 index 000000000..16ef54e95 --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/CacheStore.scala @@ -0,0 +1,93 @@ +package sbt.internal.util + +import sjsonnew.{ IsoString, JsonReader, JsonWriter, SupportConverter } + +import java.io.{ File, InputStream, OutputStream } + +import sbt.io.{ IO, Using } +import sbt.io.syntax.fileToRichFile + +/** + * A `CacheStore` is used by the caching infrastructure to persist cached information. + */ +trait CacheStore extends Input with Output { + /** Delete the persisted information. */ + def delete(): Unit +} + +/** + * Factory that can derive new stores. + */ +trait CacheStoreFactory { + /** Create a new store. */ + def derive(identifier: String): CacheStore +} + +/** + * A factory that creates new stores persisted in `base`. + */ +class DirectoryStoreFactory[J: IsoString](base: File, converter: SupportConverter[J]) extends CacheStoreFactory { + + IO.createDirectory(base) + + override def derive(identifier: String): CacheStore = + new FileBasedStore(base / identifier, converter) +} + +/** + * A `CacheStore` that persists information in `file`. + */ +class FileBasedStore[J: IsoString](file: File, converter: SupportConverter[J]) extends CacheStore { + + IO.touch(file, setModified = false) + + override def delete(): Unit = + IO.delete(file) + + override def read[T: JsonReader](): T = + Using.fileInputStream(file) { stream => + val input = new PlainInput(stream, converter) + input.read() + } + + override def read[T: JsonReader](default: => T): T = + try read[T]() + catch { case _: Exception => default } + + override def write[T: JsonWriter](value: T): Unit = + Using.fileOutputStream(append = false)(file) { stream => + val output = new PlainOutput(stream, converter) + output.write(value) + } + + override def close(): Unit = () + +} + +/** + * A store that reads from `inputStream` and writes to `outputStream + */ +class StreamBasedStore[J: IsoString](inputStream: InputStream, outputStream: OutputStream, converter: SupportConverter[J]) extends CacheStore { + + override def delete(): Unit = () + + override def read[T: JsonReader](): T = { + val input = new PlainInput(inputStream, converter) + input.read() + } + + override def read[T: JsonReader](default: => T): T = + try read[T]() + catch { case _: Exception => default } + + override def write[T: JsonWriter](value: T): Unit = { + val output = new PlainOutput(outputStream, converter) + output.write(value) + } + + override def close(): Unit = { + inputStream.close() + outputStream.close() + } + +} \ No newline at end of file diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/FileInfo.scala b/internal/util-cache/src/main/scala/sbt/internal/util/FileInfo.scala index 8bd025397..cf83d01dd 100644 --- a/internal/util-cache/src/main/scala/sbt/internal/util/FileInfo.scala +++ b/internal/util-cache/src/main/scala/sbt/internal/util/FileInfo.scala @@ -3,135 +3,172 @@ */ package sbt.internal.util -import java.io.File -import sbinary.{ DefaultProtocol, Format } -import DefaultProtocol._ -import scala.reflect.Manifest import sbt.io.Hash -import sbt.serialization._ + +import java.io.File +import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder } +import CacheImplicits._ sealed trait FileInfo { - val file: File + def file: File } -@directSubclasses(Array(classOf[FileHash], classOf[HashModifiedFileInfo])) + sealed trait HashFileInfo extends FileInfo { - val hash: List[Byte] + def hash: List[Byte] } -object HashFileInfo { - implicit val pickler: Pickler[HashFileInfo] with Unpickler[HashFileInfo] = PicklerUnpickler.generate[HashFileInfo] -} -@directSubclasses(Array(classOf[FileModified], classOf[HashModifiedFileInfo])) + sealed trait ModifiedFileInfo extends FileInfo { - val lastModified: Long + def lastModified: Long } -object ModifiedFileInfo { - implicit val pickler: Pickler[ModifiedFileInfo] with Unpickler[ModifiedFileInfo] = PicklerUnpickler.generate[ModifiedFileInfo] -} -@directSubclasses(Array(classOf[PlainFile])) + sealed trait PlainFileInfo extends FileInfo { def exists: Boolean } -object PlainFileInfo { - implicit val pickler: Pickler[PlainFileInfo] with Unpickler[PlainFileInfo] = PicklerUnpickler.generate[PlainFileInfo] -} -@directSubclasses(Array(classOf[FileHashModified])) -sealed trait HashModifiedFileInfo extends HashFileInfo with ModifiedFileInfo -object HashModifiedFileInfo { - implicit val pickler: Pickler[HashModifiedFileInfo] with Unpickler[HashModifiedFileInfo] = PicklerUnpickler.generate[HashModifiedFileInfo] -} -private[sbt] final case class PlainFile(file: File, exists: Boolean) extends PlainFileInfo -private[sbt] object PlainFile { - implicit val pickler: Pickler[PlainFile] with Unpickler[PlainFile] = PicklerUnpickler.generate[PlainFile] -} -private[sbt] final case class FileHash(file: File, hash: List[Byte]) extends HashFileInfo -private[sbt] object FileHash { - implicit val pickler: Pickler[FileHash] with Unpickler[FileHash] = PicklerUnpickler.generate[FileHash] -} -private[sbt] final case class FileModified(file: File, lastModified: Long) extends ModifiedFileInfo -private[sbt] object FileModified { - implicit val pickler: Pickler[FileModified] with Unpickler[FileModified] = PicklerUnpickler.generate[FileModified] -} -private[sbt] final case class FileHashModified(file: File, hash: List[Byte], lastModified: Long) extends HashModifiedFileInfo -private[sbt] object FileHashModified { - implicit val pickler: Pickler[FileHashModified] with Unpickler[FileHashModified] = PicklerUnpickler.generate[FileHashModified] -} +sealed trait HashModifiedFileInfo extends HashFileInfo with ModifiedFileInfo +private final case class PlainFile(file: File, exists: Boolean) extends PlainFileInfo +private final case class FileModified(file: File, lastModified: Long) extends ModifiedFileInfo +private final case class FileHash(file: File, hash: List[Byte]) extends HashFileInfo +private final case class FileHashModified(file: File, hash: List[Byte], lastModified: Long) extends HashModifiedFileInfo object FileInfo { - implicit def existsInputCache: InputCache[PlainFileInfo] = exists.infoInputCache - implicit def modifiedInputCache: InputCache[ModifiedFileInfo] = lastModified.infoInputCache - implicit def hashInputCache: InputCache[HashFileInfo] = hash.infoInputCache - implicit def fullInputCache: InputCache[HashModifiedFileInfo] = full.infoInputCache - implicit val pickler: Pickler[FileInfo] with Unpickler[FileInfo] = PicklerUnpickler.generate[FileInfo] sealed trait Style { type F <: FileInfo - implicit def apply(file: File): F - implicit def unapply(info: F): File = info.file - implicit val format: Format[F] - import Cache._ - implicit def fileInfoEquiv: Equiv[F] = defaultEquiv - def infoInputCache: InputCache[F] = basicInput - implicit def fileInputCache: InputCache[File] = wrapIn[File, F] + implicit val format: JsonFormat[F] + + def apply(file: File): F + + def apply(files: Set[File]): FilesInfo[F] = FilesInfo(files map apply) + + def unapply(info: F): File = info.file + + def unapply(infos: FilesInfo[F]): Set[File] = infos.files map (_.file) } + object full extends Style { - type F = HashModifiedFileInfo - implicit def apply(file: File): HashModifiedFileInfo = make(file, Hash(file).toList, file.lastModified) - def make(file: File, hash: List[Byte], lastModified: Long): HashModifiedFileInfo = FileHashModified(file.getAbsoluteFile, hash, lastModified) - implicit val format: Format[HashModifiedFileInfo] = wrap(f => (f.file, f.hash, f.lastModified), (make _).tupled) + override type F = HashModifiedFileInfo + + override implicit val format: JsonFormat[HashModifiedFileInfo] = new JsonFormat[HashModifiedFileInfo] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): HashModifiedFileInfo = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val file = unbuilder.readField[File]("file") + val hash = unbuilder.readField[List[Byte]]("hash") + val lastModified = unbuilder.readField[Long]("lastModified") + unbuilder.endObject() + FileHashModified(file, hash, lastModified) + case None => + deserializationError("Expected JsObject but found None") + } + + override def write[J](obj: HashModifiedFileInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("file", obj.file) + builder.addField("hash", obj.hash) + builder.addField("lastModified", obj.lastModified) + builder.endObject() + } + } + + override implicit def apply(file: File): HashModifiedFileInfo = + FileHashModified(file.getAbsoluteFile, Hash(file).toList, file.lastModified) } + object hash extends Style { - type F = HashFileInfo - implicit def apply(file: File): HashFileInfo = make(file, computeHash(file)) - def make(file: File, hash: List[Byte]): HashFileInfo = FileHash(file.getAbsoluteFile, hash) - implicit val format: Format[HashFileInfo] = wrap(f => (f.file, f.hash), (make _).tupled) - private def computeHash(file: File): List[Byte] = try { Hash(file).toList } catch { case e: Exception => Nil } + override type F = HashFileInfo + + override implicit val format: JsonFormat[HashFileInfo] = new JsonFormat[HashFileInfo] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): HashFileInfo = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val file = unbuilder.readField[File]("file") + val hash = unbuilder.readField[List[Byte]]("hash") + unbuilder.endObject() + FileHash(file, hash) + case None => + deserializationError("Expected JsObject but found None") + } + + override def write[J](obj: HashFileInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("file", obj.file) + builder.addField("hash", obj.hash) + builder.endObject() + } + } + + override implicit def apply(file: File): HashFileInfo = + FileHash(file.getAbsoluteFile, computeHash(file)) + + private def computeHash(file: File): List[Byte] = + try Hash(file).toList + catch { case _: Exception => Nil } } + object lastModified extends Style { - type F = ModifiedFileInfo - implicit def apply(file: File): ModifiedFileInfo = make(file, file.lastModified) - def make(file: File, lastModified: Long): ModifiedFileInfo = FileModified(file.getAbsoluteFile, lastModified) - implicit val format: Format[ModifiedFileInfo] = wrap(f => (f.file, f.lastModified), (make _).tupled) + override type F = ModifiedFileInfo + + override implicit val format: JsonFormat[ModifiedFileInfo] = new JsonFormat[ModifiedFileInfo] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): ModifiedFileInfo = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val file = unbuilder.readField[File]("file") + val lastModified = unbuilder.readField[Long]("lastModified") + unbuilder.endObject() + FileModified(file, lastModified) + case None => + deserializationError("Expected JsObject but found None") + } + + override def write[J](obj: ModifiedFileInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("file", obj.file) + builder.addField("lastModified", obj.lastModified) + builder.endObject() + } + } + + override implicit def apply(file: File): ModifiedFileInfo = + FileModified(file.getAbsoluteFile, file.lastModified) } + object exists extends Style { - type F = PlainFileInfo - implicit def apply(file: File): PlainFileInfo = make(file) - def make(file: File): PlainFileInfo = { val abs = file.getAbsoluteFile; PlainFile(abs, abs.exists) } - implicit val format: Format[PlainFileInfo] = asProduct2[PlainFileInfo, File, Boolean](PlainFile.apply)(x => (x.file, x.exists)) + override type F = PlainFileInfo + + override implicit val format: JsonFormat[PlainFileInfo] = new JsonFormat[PlainFileInfo] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): PlainFileInfo = + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val file = unbuilder.readField[File]("file") + val exists = unbuilder.readField[Boolean]("exists") + unbuilder.endObject() + PlainFile(file, exists) + case None => + deserializationError("Expected JsObject but found None") + } + + override def write[J](obj: PlainFileInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("file", obj.file) + builder.addField("exists", obj.exists) + builder.endObject() + } + } + + override implicit def apply(file: File): PlainFileInfo = { + val abs = file.getAbsoluteFile + PlainFile(abs, abs.exists) + } } } final case class FilesInfo[F <: FileInfo] private (files: Set[F]) object FilesInfo { - sealed abstract class Style { - type F <: FileInfo - val fileStyle: FileInfo.Style { type F = Style.this.F } - - //def manifest: Manifest[F] = fileStyle.manifest - implicit def apply(files: Set[File]): FilesInfo[F] - implicit def unapply(info: FilesInfo[F]): Set[File] = info.files.map(_.file) - implicit val formats: Format[FilesInfo[F]] - val manifest: Manifest[Format[FilesInfo[F]]] - def empty: FilesInfo[F] = new FilesInfo[F](Set.empty) - import Cache._ - def infosInputCache: InputCache[FilesInfo[F]] = basicInput - implicit def filesInputCache: InputCache[Set[File]] = wrapIn[Set[File], FilesInfo[F]] - implicit def filesInfoEquiv: Equiv[FilesInfo[F]] = defaultEquiv - } - private final class BasicStyle[FI <: FileInfo](style: FileInfo.Style { type F = FI })(implicit val manifest: Manifest[Format[FilesInfo[FI]]]) extends Style { - type F = FI - val fileStyle: FileInfo.Style { type F = FI } = style - private implicit val infoFormat: Format[FI] = fileStyle.format - implicit def apply(files: Set[File]): FilesInfo[F] = FilesInfo(files.map(_.getAbsoluteFile).map(fileStyle.apply)) - implicit val formats: Format[FilesInfo[F]] = wrap(_.files, (fs: Set[F]) => new FilesInfo(fs)) - } - lazy val full: Style { type F = HashModifiedFileInfo } = new BasicStyle(FileInfo.full) - lazy val hash: Style { type F = HashFileInfo } = new BasicStyle(FileInfo.hash) - lazy val lastModified: Style { type F = ModifiedFileInfo } = new BasicStyle(FileInfo.lastModified) - lazy val exists: Style { type F = PlainFileInfo } = new BasicStyle(FileInfo.exists) - - implicit def existsInputsCache: InputCache[FilesInfo[PlainFileInfo]] = exists.infosInputCache - implicit def hashInputsCache: InputCache[FilesInfo[HashFileInfo]] = hash.infosInputCache - implicit def modifiedInputsCache: InputCache[FilesInfo[ModifiedFileInfo]] = lastModified.infosInputCache - implicit def fullInputsCache: InputCache[FilesInfo[HashModifiedFileInfo]] = full.infosInputCache + implicit def format[F <: FileInfo]: JsonFormat[FilesInfo[F]] = implicitly + def empty[F <: FileInfo]: FilesInfo[F] = FilesInfo(Set.empty[F]) } diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/Input.scala b/internal/util-cache/src/main/scala/sbt/internal/util/Input.scala new file mode 100644 index 000000000..75eb92463 --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/Input.scala @@ -0,0 +1,45 @@ +package sbt.internal.util + +import sbt.io.{ IO, Using } + +import java.io.{ Closeable, InputStream } + +import scala.util.{ Failure, Success } + +import sjsonnew.{ IsoString, JsonReader, SupportConverter } + +trait Input extends Closeable { + def read[T: JsonReader](): T + def read[T: JsonReader](default: => T): T +} + +class PlainInput[J: IsoString](input: InputStream, converter: SupportConverter[J]) extends Input { + val isoFormat: IsoString[J] = implicitly + private def readFully(): String = { + Using.streamReader(input, IO.utf8) { reader => + val builder = new StringBuilder() + val bufferSize = 1024 + val buffer = new Array[Char](bufferSize) + var read = 0 + while ({ read = reader.read(buffer, 0, bufferSize); read != -1 }) { + builder.append(String.valueOf(buffer.take(read))) + } + builder.toString() + } + } + + override def read[T: JsonReader](): T = { + val string = readFully() + val json = isoFormat.from(string) + converter.fromJson(json) match { + case Success(value) => value + case Failure(ex) => throw ex + } + } + + override def read[T: JsonReader](default: => T): T = + try read[T]() + catch { case _: Exception => default } + + override def close(): Unit = input.close() +} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/Output.scala b/internal/util-cache/src/main/scala/sbt/internal/util/Output.scala new file mode 100644 index 000000000..6e99db9ac --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/Output.scala @@ -0,0 +1,32 @@ +package sbt.internal.util + +import sbt.io.Using + +import java.io.{ Closeable, OutputStream } + +import scala.util.{ Failure, Success } + +import sjsonnew.{ IsoString, JsonWriter, SupportConverter } + +trait Output extends Closeable { + def write[T: JsonWriter](value: T): Unit +} + +class PlainOutput[J: IsoString](output: OutputStream, converter: SupportConverter[J]) extends Output { + val isoFormat: IsoString[J] = implicitly + override def write[T: JsonWriter](value: T): Unit = { + converter.toJson(value) match { + case Success(js) => + val asString = isoFormat.to(js) + Using.bufferedOutputStream(output) { writer => + val out = new java.io.PrintWriter(writer) + out.print(asString) + out.flush() + } + case Failure(ex) => + throw ex + } + } + + override def close(): Unit = output.close() +} diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/SeparatedCache.scala b/internal/util-cache/src/main/scala/sbt/internal/util/SeparatedCache.scala index ff735a528..be8f11a38 100644 --- a/internal/util-cache/src/main/scala/sbt/internal/util/SeparatedCache.scala +++ b/internal/util-cache/src/main/scala/sbt/internal/util/SeparatedCache.scala @@ -3,59 +3,61 @@ */ package sbt.internal.util -import sbinary.{ Format, Input, Output => Out } -import java.io.File -import sbt.io.Using +import scala.util.Try -trait InputCache[I] { - type Internal - def convert(i: I): Internal - def read(from: Input): Internal - def write(to: Out, j: Internal): Unit - def equiv: Equiv[Internal] +import sjsonnew.JsonFormat + +import CacheImplicits._ + +/** + * A cache that stores a single value. + */ +trait SingletonCache[T] { + /** Reads the cache from the backing `from`. */ + def read(from: Input): T + + /** Writes `value` to the backing `to`. */ + def write(to: Output, value: T): Unit + + /** Equivalence for elements of type `T`. */ + def equiv: Equiv[T] } -object InputCache { - implicit def basicInputCache[I](implicit fmt: Format[I], eqv: Equiv[I]): InputCache[I] = - new InputCache[I] { - type Internal = I - def convert(i: I) = i - def read(from: Input): I = fmt.reads(from) - def write(to: Out, i: I) = fmt.writes(to, i) - def equiv = eqv + +object SingletonCache { + + implicit def basicSingletonCache[T: JsonFormat: Equiv]: SingletonCache[T] = + new SingletonCache[T] { + override def read(from: Input): T = from.read[T] + override def write(to: Output, value: T) = to.write(value) + override def equiv: Equiv[T] = implicitly } - def lzy[I](mkIn: => InputCache[I]): InputCache[I] = - new InputCache[I] { - lazy val ic = mkIn - type Internal = ic.Internal - def convert(i: I) = ic convert i - def read(from: Input): ic.Internal = ic.read(from) - def write(to: Out, i: ic.Internal) = ic.write(to, i) - def equiv = ic.equiv + + /** A lazy `SingletonCache` */ + def lzy[T: JsonFormat: Equiv](mkCache: => SingletonCache[T]): SingletonCache[T] = + new SingletonCache[T] { + lazy val cache = mkCache + override def read(from: Input): T = cache.read(from) + override def write(to: Output, value: T) = cache.write(to, value) + override def equiv = cache.equiv } } -class BasicCache[I, O](implicit input: InputCache[I], outFormat: Format[O]) extends Cache[I, O] { - def apply(file: File)(in: I) = - { - val j = input.convert(in) - try { applyImpl(file, j) } - catch { case e: Exception => Right(update(file)(j)) } - } - protected def applyImpl(file: File, in: input.Internal) = - { - Using.fileInputStream(file) { stream => - val previousIn = input.read(stream) - if (input.equiv.equiv(in, previousIn)) - Left(outFormat.reads(stream)) - else - Right(update(file)(in)) - } - } - protected def update(file: File)(in: input.Internal) = (out: O) => - { - Using.fileOutputStream(false)(file) { stream => - input.write(stream, in) - outFormat.writes(stream, out) - } - } +/** + * Simple key-value cache. + */ +class BasicCache[I: JsonFormat: Equiv, O: JsonFormat] extends Cache[I, O] { + private val singletonCache: SingletonCache[(I, O)] = implicitly + val equiv: Equiv[I] = implicitly + override def apply(store: CacheStore)(key: I): CacheResult[O] = + Try { + val (previousKey, previousValue) = singletonCache.read(store) + if (equiv.equiv(key, previousKey)) + Hit(previousValue) + else + Miss(update(store)(key)) + } getOrElse Miss(update(store)(key)) + + private def update(store: CacheStore)(key: I) = (value: O) => { + singletonCache.write(store, (key, value)) + } } diff --git a/internal/util-cache/src/main/scala/sbt/internal/util/StampedFormat.scala b/internal/util-cache/src/main/scala/sbt/internal/util/StampedFormat.scala new file mode 100644 index 000000000..1d3a6d9fc --- /dev/null +++ b/internal/util-cache/src/main/scala/sbt/internal/util/StampedFormat.scala @@ -0,0 +1,44 @@ +package sbt.internal.util + +import scala.reflect.Manifest + +import sbt.datatype.IntFormat + +import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder } + +object StampedFormat extends IntFormat { + + def apply[T](format: JsonFormat[T])(implicit mf: Manifest[JsonFormat[T]]): JsonFormat[T] = { + withStamp(stamp(format))(format) + } + + def withStamp[T, S](stamp: S)(format: JsonFormat[T])(implicit formatStamp: JsonFormat[S], equivStamp: Equiv[S]): JsonFormat[T] = + new JsonFormat[T] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): T = + jsOpt match { + case Some(js) => + unbuilder.extractArray(js) match { + case Vector(readStamp, readValue) => + val actualStamp = formatStamp.read(Some(readStamp), unbuilder) + if (equivStamp.equiv(actualStamp, stamp)) format.read(Some(readValue), unbuilder) + else sys.error(s"Incorrect stamp. Expected: $stamp, Found: $readStamp") + + case other => + deserializationError(s"Expected JsArray of size 2, but found JsArray of size ${other.size}") + } + + case None => + deserializationError("Expected JsArray but found None.") + } + + override def write[J](obj: T, builder: Builder[J]): Unit = { + builder.beginArray() + formatStamp.write(stamp, builder) + format.write(obj, builder) + builder.endArray() + } + } + private def stamp[T](format: JsonFormat[T])(implicit mf: Manifest[JsonFormat[T]]): Int = typeHash(mf) + private def typeHash[T](implicit mf: Manifest[T]) = mf.toString.hashCode + +} \ No newline at end of file diff --git a/internal/util-cache/src/test/scala/CacheSpec.scala b/internal/util-cache/src/test/scala/CacheSpec.scala new file mode 100644 index 000000000..8e2ebd12a --- /dev/null +++ b/internal/util-cache/src/test/scala/CacheSpec.scala @@ -0,0 +1,76 @@ +package sbt.internal.util + +import sbt.io.IO +import sbt.io.syntax._ + +import CacheImplicits._ + +import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder } +import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser } + +import scala.json.ast.unsafe.JValue + +class CacheSpec extends UnitSpec { + + implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe) + + "A cache" should "NOT throw an exception if read without being written previously" in { + testCache[String, Int] { + case (cache, store) => + cache(store)("missing") match { + case Hit(_) => fail + case Miss(_) => () + } + } + } + + it should "write a very simple value" in { + testCache[String, Int] { + case (cache, store) => + cache(store)("missing") match { + case Hit(_) => fail + case Miss(update) => update(5) + } + } + } + + it should "be updatable" in { + testCache[String, Int] { + case (cache, store) => + val value = 5 + cache(store)("someKey") match { + case Hit(_) => fail + case Miss(update) => update(value) + } + + cache(store)("someKey") match { + case Hit(read) => assert(read === value) + case Miss(_) => fail + } + } + } + + it should "return the value that has been previously written" in { + testCache[String, Int] { + case (cache, store) => + val key = "someKey" + val value = 5 + cache(store)(key) match { + case Hit(_) => fail + case Miss(update) => update(value) + } + + cache(store)(key) match { + case Hit(read) => assert(read === value) + case Miss(_) => fail + } + } + } + + private def testCache[K, V](f: (Cache[K, V], CacheStore) => Unit)(implicit cache: Cache[K, V]): Unit = + IO.withTemporaryDirectory { tmp => + val store = new FileBasedStore(tmp / "cache-store", Converter) + f(cache, store) + } + +} \ No newline at end of file diff --git a/internal/util-cache/src/test/scala/CacheTest.scala b/internal/util-cache/src/test/scala/CacheTest.scala deleted file mode 100644 index 569b0bf24..000000000 --- a/internal/util-cache/src/test/scala/CacheTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package sbt.internal.util - -import java.io.File -import Types.:+: - -object CacheTest // extends Properties("Cache test") -{ - val lengthCache = new File("/tmp/length-cache") - val cCache = new File("/tmp/c-cache") - - import Cache._ - import FileInfo.hash._ - import Ordering._ - import sbinary.DefaultProtocol.FileFormat - def test(): Unit = { - lazy val create = new File("test") - - val length = cached(lengthCache) { - (f: File) => { println("File length: " + f.length); f.length } - } - - lazy val fileLength = length(create) - - val c = cached(cCache) { (in: (File :+: Long :+: HNil)) => - val file :+: len :+: HNil = in - println("File: " + file + " (" + file.exists + "), length: " + len) - (len + 1) :+: file :+: HNil - } - c(create :+: fileLength :+: HNil) - () - } -} diff --git a/internal/util-cache/src/test/scala/SingletonCacheSpec.scala b/internal/util-cache/src/test/scala/SingletonCacheSpec.scala new file mode 100644 index 000000000..42c774a2f --- /dev/null +++ b/internal/util-cache/src/test/scala/SingletonCacheSpec.scala @@ -0,0 +1,91 @@ +package sbt.internal.util + +import sbt.io.IO +import sbt.io.syntax._ + +import CacheImplicits._ + +import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder } +import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser } + +import scala.json.ast.unsafe.JValue + +class SingletonCacheSpec extends UnitSpec { + + case class ComplexType(val x: Int, y: String, z: List[Int]) + object ComplexType { + implicit val format: JsonFormat[ComplexType] = + new JsonFormat[ComplexType] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): ComplexType = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val x = unbuilder.readField[Int]("x") + val y = unbuilder.readField[String]("y") + val z = unbuilder.readField[List[Int]]("z") + unbuilder.endObject() + ComplexType(x, y, z) + + case None => + deserializationError("Exception JObject but found None") + } + } + + override def write[J](obj: ComplexType, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("x", obj.x) + builder.addField("y", obj.y) + builder.addField("z", obj.z) + builder.endObject() + } + } + } + + implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe) + + "A singleton cache" should "throw an exception if read without being written previously" in { + testCache[Int] { + case (cache, store) => + intercept[Exception] { + cache.read(store) + } + () + } + } + + it should "write a very simple value" in { + testCache[Int] { + case (cache, store) => + cache.write(store, 5) + } + } + + it should "return the simple value that has been previously written" in { + testCache[Int] { + case (cache, store) => + val value = 5 + cache.write(store, value) + val read = cache.read(store) + + assert(read === value) + } + } + + it should "write a complex value" in { + testCache[ComplexType] { + case (cache, store) => + val value = ComplexType(1, "hello, world!", (1 to 10 by 3).toList) + cache.write(store, value) + val read = cache.read(store) + + assert(read === value) + } + } + + private def testCache[T](f: (SingletonCache[T], CacheStore) => Unit)(implicit cache: SingletonCache[T]): Unit = + IO.withTemporaryDirectory { tmp => + val store = new FileBasedStore(tmp / "cache-store", Converter) + f(cache, store) + } + +} \ No newline at end of file diff --git a/internal/util-cache/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala b/internal/util-cache/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala new file mode 100644 index 000000000..7f9f759dc --- /dev/null +++ b/internal/util-cache/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala @@ -0,0 +1,29 @@ +package sjsonnew +package support.scalajson.unsafe + +import scala.json.ast.unsafe._ +import scala.collection.mutable +import jawn.{ SupportParser, MutableFacade } + +object FixedParser extends SupportParser[JValue] { + implicit val facade: MutableFacade[JValue] = + new MutableFacade[JValue] { + def jnull() = JNull + def jfalse() = JTrue + def jtrue() = JFalse + def jnum(s: String) = JNumber(s) + def jint(s: String) = JNumber(s) + def jstring(s: String) = JString(s) + def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs.toArray) + def jobject(vs: mutable.Map[String, JValue]) = { + val array = new Array[JField](vs.size) + var i = 0 + vs.foreach { + case (key, value) => + array(i) = JField(key, value) + i += 1 + } + JObject(array) + } + } +} \ No newline at end of file diff --git a/internal/util-tracking/src/main/scala/sbt/internal/util/Tracked.scala b/internal/util-tracking/src/main/scala/sbt/internal/util/Tracked.scala index 51e70a8a5..5aaf42e10 100644 --- a/internal/util-tracking/src/main/scala/sbt/internal/util/Tracked.scala +++ b/internal/util-tracking/src/main/scala/sbt/internal/util/Tracked.scala @@ -3,133 +3,76 @@ */ package sbt.internal.util -import java.io.{ File, IOException } -import CacheIO.{ fromFile, toFile } -import sbinary.Format -import scala.pickling.PicklingException -import scala.reflect.Manifest -import sbt.io.IO.{ delete, read, write } +import scala.util.{ Failure, Try, Success } + +import java.io.File import sbt.io.IO -import sbt.io.Using import sbt.io.syntax._ -import sbt.serialization._ + +import sjsonnew.JsonFormat object Tracked { + + import CacheImplicits.LongFormat + /** * Creates a tracker that provides the last time it was evaluated. * If 'useStartTime' is true, the recorded time is the start of the evaluated function. * If 'useStartTime' is false, the recorded time is when the evaluated function completes. * In both cases, the timestamp is not updated if the function throws an exception. */ - def tstamp(cacheFile: File, useStartTime: Boolean = true): Timestamp = new Timestamp(cacheFile, useStartTime) - /** Creates a tracker that only evaluates a function when the input has changed.*/ - //def changed[O](cacheFile: File)(implicit format: Format[O], equiv: Equiv[O]): Changed[O] = - // new Changed[O](cacheFile) + def tstamp(store: CacheStore, useStartTime: Boolean = true): Timestamp = new Timestamp(store, useStartTime) /** Creates a tracker that provides the difference between a set of input files for successive invocations.*/ - def diffInputs(cache: File, style: FilesInfo.Style): Difference = - Difference.inputs(cache, style) + def diffInputs(store: CacheStore, style: FileInfo.Style): Difference = + Difference.inputs(store, style) + /** Creates a tracker that provides the difference between a set of output files for successive invocations.*/ - def diffOutputs(cache: File, style: FilesInfo.Style): Difference = - Difference.outputs(cache, style) + def diffOutputs(store: CacheStore, style: FileInfo.Style): Difference = + Difference.outputs(store, style) - def lastOutput[I, O](cacheFile: File)(f: (I, Option[O]) => O)(implicit o: Format[O], mf: Manifest[Format[O]]): I => O = in => - { - val previous: Option[O] = fromFile[O](cacheFile) - val next = f(in, previous) - toFile(next)(cacheFile) - next + /** Creates a tracker that provides the output of the most recent invocation of the function */ + def lastOutput[I, O: JsonFormat](store: CacheStore)(f: (I, Option[O]) => O): I => O = { in => + val previous = Try { store.read[O] }.toOption + val next = f(in, previous) + store.write(next) + next + } + + /** + * Creates a tracker that indicates whether the arguments given to f have changed since the most + * recent invocation. + */ + def inputChanged[I: JsonFormat: SingletonCache, O](store: CacheStore)(f: (Boolean, I) => O): I => O = { in => + val cache: SingletonCache[I] = implicitly + val help = new CacheHelp(cache) + val changed = help.changed(store, in) + val result = f(changed, in) + if (changed) + help.save(store, in) + result + } + + private final class CacheHelp[I: JsonFormat](val sc: SingletonCache[I]) { + def save(store: CacheStore, value: I): Unit = { + store.write(value) } - // Todo: This function needs more testing. - private[sbt] def lastOutputWithJson[I, O: Pickler: Unpickler](cacheFile: File)(f: (I, Option[O]) => O): I => O = in => - { - val previous: Option[O] = try { - fromJsonFile[O](cacheFile).toOption - } catch { - case e: PicklingException => None - case e: IOException => None + + def changed(store: CacheStore, value: I): Boolean = + Try { store.read[I] } match { + case Success(prev) => !sc.equiv.equiv(value, prev) + case Failure(_) => true } - val next = f(in, previous) - IO.createDirectory(cacheFile.getParentFile) - toJsonFile(next, cacheFile) - next - } - def inputChanged[I, O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): I => O = in => - { - val help = new CacheHelp(ic) - val conv = help.convert(in) - val changed = help.changed(cacheFile, conv) - val result = f(changed, in) - - if (changed) - help.save(cacheFile, conv) - - result - } - private[sbt] def inputChangedWithJson[I: Pickler: Unpickler, O](cacheFile: File)(f: (Boolean, I) => O): I => O = in => - { - val help = new JsonCacheHelp[I] - val conv = help.convert(in) - val changed = help.changed(cacheFile, conv) - val result = f(changed, in) - - if (changed) - help.save(cacheFile, conv) - - result - } - def outputChanged[I, O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): (() => I) => O = in => - { - val initial = in() - val help = new CacheHelp(ic) - val changed = help.changed(cacheFile, help.convert(initial)) - val result = f(changed, initial) - - if (changed) - help.save(cacheFile, help.convert(in())) - - result - } - private[sbt] def outputChangedWithJson[I: Pickler, O](cacheFile: File)(f: (Boolean, I) => O): (() => I) => O = in => - { - val initial = in() - val help = new JsonCacheHelp[I] - val changed = help.changed(cacheFile, help.convert(initial)) - val result = f(changed, initial) - - if (changed) - help.save(cacheFile, help.convert(in())) - - result - } - final class CacheHelp[I](val ic: InputCache[I]) { - def convert(i: I): ic.Internal = ic.convert(i) - def save(cacheFile: File, value: ic.Internal): Unit = - Using.fileOutputStream()(cacheFile)(out => ic.write(out, value)) - def changed(cacheFile: File, converted: ic.Internal): Boolean = - try { - val prev = Using.fileInputStream(cacheFile)(x => ic.read(x)) - !ic.equiv.equiv(converted, prev) - } catch { case e: Exception => true } - } - private[sbt] final class JsonCacheHelp[I: Pickler] { - def convert(i: I): String = toJsonString(i) - def save(cacheFile: File, value: String): Unit = - IO.write(cacheFile, value, IO.utf8) - def changed(cacheFile: File, converted: String): Boolean = - try { - val prev = IO.read(cacheFile, IO.utf8) - converted != prev - } catch { case e: Exception => true } } + } trait Tracked { /** Cleans outputs and clears the cache.*/ def clean(): Unit } -class Timestamp(val cacheFile: File, useStartTime: Boolean) extends Tracked { - def clean() = delete(cacheFile) +class Timestamp(val store: CacheStore, useStartTime: Boolean)(implicit format: JsonFormat[Long]) extends Tracked { + def clean() = store.delete() /** * Reads the previous timestamp, evaluates the provided function, * and then updates the timestamp if the function completes normally. @@ -138,17 +81,16 @@ class Timestamp(val cacheFile: File, useStartTime: Boolean) extends Tracked { { val start = now() val result = f(readTimestamp) - write(cacheFile, (if (useStartTime) start else now()).toString) + store.write(if (useStartTime) start else now()) result } private def now() = System.currentTimeMillis def readTimestamp: Long = - try { read(cacheFile).toLong } - catch { case _: NumberFormatException | _: java.io.FileNotFoundException => 0 } + Try { store.read[Long] } getOrElse 0 } -class Changed[O](val cacheFile: File)(implicit equiv: Equiv[O], format: Format[O]) extends Tracked { - def clean() = delete(cacheFile) +class Changed[O: Equiv: JsonFormat](val store: CacheStore) extends Tracked { + def clean() = store.delete() def apply[O2](ifChanged: O => O2, ifUnchanged: O => O2): O => O2 = value => { if (uptodate(value)) @@ -159,19 +101,15 @@ class Changed[O](val cacheFile: File)(implicit equiv: Equiv[O], format: Format[O } } - def update(value: O): Unit = Using.fileOutputStream(false)(cacheFile)(stream => format.writes(stream, value)) - def uptodate(value: O): Boolean = - try { - Using.fileInputStream(cacheFile) { - stream => equiv.equiv(value, format.reads(stream)) - } - } catch { - case _: Exception => false - } + def update(value: O): Unit = store.write(value) //Using.fileOutputStream(false)(cacheFile)(stream => format.writes(stream, value)) + def uptodate(value: O): Boolean = { + val equiv: Equiv[O] = implicitly + equiv.equiv(value, store.read[O]) + } } object Difference { - def constructor(defineClean: Boolean, filesAreOutputs: Boolean): (File, FilesInfo.Style) => Difference = - (cache, style) => new Difference(cache, style, defineClean, filesAreOutputs) + def constructor(defineClean: Boolean, filesAreOutputs: Boolean): (CacheStore, FileInfo.Style) => Difference = + (store, style) => new Difference(store, style, defineClean, filesAreOutputs) /** * Provides a constructor for a Difference that removes the files from the previous run on a call to 'clean' and saves the @@ -185,15 +123,15 @@ object Difference { */ val inputs = constructor(false, false) } -class Difference(val cache: File, val style: FilesInfo.Style, val defineClean: Boolean, val filesAreOutputs: Boolean) extends Tracked { +class Difference(val store: CacheStore, val style: FileInfo.Style, val defineClean: Boolean, val filesAreOutputs: Boolean) extends Tracked { def clean() = { - if (defineClean) delete(raw(cachedFilesInfo)) else () + if (defineClean) IO.delete(raw(cachedFilesInfo)) else () clearCache() } - private def clearCache() = delete(cache) + private def clearCache() = store.delete() - private def cachedFilesInfo = fromFile(style.formats, style.empty)(cache)(style.manifest).files + private def cachedFilesInfo = store.read(default = FilesInfo.empty[style.F]).files //(style.formats).files private def raw(fs: Set[style.F]): Set[File] = fs.map(_.file) def apply[T](files: Set[File])(f: ChangeReport[File] => T): T = @@ -225,7 +163,9 @@ class Difference(val cache: File, val style: FilesInfo.Style, val defineClean: B val result = f(report) val info = if (filesAreOutputs) style(abs(extractFiles(result))) else currentFilesInfo - toFile(style.formats)(info)(cache)(style.manifest) + + store.write(info) + result } } @@ -240,24 +180,24 @@ object FileFunction { * (which does the actual work: compiles, generates resources, etc.), returning * a Set of output files that it generated. * - * The input file and resulting output file state is cached in - * cacheBaseDirectory. On each invocation, the state of the input and output + * The input file and resulting output file state is cached in stores issued by + * `storeFactory`. On each invocation, the state of the input and output * files from the previous run is compared against the cache, as is the set of * input files. If a change in file state / input files set is detected, the * action function is re-executed. * - * @param cacheBaseDirectory The folder in which to store + * @param storeFactory The factory to use to get stores for the input and output files. * @param inStyle The strategy by which to detect state change in the input files from the previous run * @param outStyle The strategy by which to detect state change in the output files from the previous run * @param action The work function, which receives a list of input files and returns a list of output files */ - def cached(cacheBaseDirectory: File, inStyle: FilesInfo.Style = FilesInfo.lastModified, outStyle: FilesInfo.Style = FilesInfo.exists)(action: Set[File] => Set[File]): Set[File] => Set[File] = - cached(cacheBaseDirectory)(inStyle, outStyle)((in, out) => action(in.checked)) + def cached(storeFactory: CacheStoreFactory, inStyle: FileInfo.Style = FileInfo.lastModified, outStyle: FileInfo.Style = FileInfo.exists)(action: Set[File] => Set[File]): Set[File] => Set[File] = + cached(storeFactory)(inStyle, outStyle)((in, out) => action(in.checked)) - def cached(cacheBaseDirectory: File)(inStyle: FilesInfo.Style, outStyle: FilesInfo.Style)(action: UpdateFunction): Set[File] => Set[File] = + def cached(storeFactory: CacheStoreFactory)(inStyle: FileInfo.Style, outStyle: FileInfo.Style)(action: UpdateFunction): Set[File] => Set[File] = { - lazy val inCache = Difference.inputs(cacheBaseDirectory / "in-cache", inStyle) - lazy val outCache = Difference.outputs(cacheBaseDirectory / "out-cache", outStyle) + lazy val inCache = Difference.inputs(storeFactory.derive("in-cache"), inStyle) + lazy val outCache = Difference.outputs(storeFactory.derive("out-cache"), outStyle) inputs => { inCache(inputs) { inReport => diff --git a/internal/util-tracking/src/test/scala/sbt/internal/util/TrackedSpec.scala b/internal/util-tracking/src/test/scala/sbt/internal/util/TrackedSpec.scala new file mode 100644 index 000000000..b23c191dc --- /dev/null +++ b/internal/util-tracking/src/test/scala/sbt/internal/util/TrackedSpec.scala @@ -0,0 +1,140 @@ +package sbt.internal.util + +import sbt.io.IO +import sbt.io.syntax._ + +import CacheImplicits._ + +import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder } +import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser } + +import scala.json.ast.unsafe.JValue + +class TrackedSpec extends UnitSpec { + + implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe) + + "lastOutput" should "store the last output" in { + withStore { store => + + val value = 5 + val otherValue = 10 + + val res0 = + Tracked.lastOutput[Int, Int](store) { + case (in, None) => + assert(in === value) + in + case (in, Some(_)) => + fail() + }(implicitly)(value) + assert(res0 === value) + + val res1 = + Tracked.lastOutput[Int, Int](store) { + case (in, None) => + fail() + case (in, Some(read)) => + assert(in === otherValue) + assert(read === value) + read + }(implicitly)(otherValue) + assert(res1 === value) + + val res2 = + Tracked.lastOutput[Int, Int](store) { + case (in, None) => + fail() + case (in, Some(read)) => + assert(in === otherValue) + assert(read === value) + read + }(implicitly)(otherValue) + assert(res2 === value) + } + } + + "inputChanged" should "detect that the input has not changed" in { + withStore { store => + val input0 = 0 + + val res0 = + Tracked.inputChanged[Int, Int](store) { + case (true, in) => + assert(in === input0) + in + case (false, in) => + fail() + }(implicitly, implicitly)(input0) + assert(res0 === input0) + + val res1 = + Tracked.inputChanged[Int, Int](store) { + case (true, in) => + fail() + case (false, in) => + assert(in === input0) + in + }(implicitly, implicitly)(input0) + assert(res1 === input0) + + } + } + + it should "detect that the input has changed" in { + withStore { store => + val input0 = 0 + val input1 = 1 + + val res0 = + Tracked.inputChanged[Int, Int](store) { + case (true, in) => + assert(in === input0) + in + case (false, in) => + fail() + }(implicitly, implicitly)(input0) + assert(res0 === input0) + + val res1 = + Tracked.inputChanged[Int, Int](store) { + case (true, in) => + assert(in === input1) + in + case (false, in) => + fail() + }(implicitly, implicitly)(input1) + assert(res1 === input1) + + } + } + + "tstamp tracker" should "have a timestamp of 0 on first invocation" in { + withStore { store => + Tracked.tstamp(store) { last => + assert(last === 0) + } + } + } + + it should "provide the last time a function has been evaluated" in { + withStore { store => + + Tracked.tstamp(store) { last => + assert(last === 0) + } + + Tracked.tstamp(store) { last => + val difference = System.currentTimeMillis - last + assert(difference < 1000) + } + } + } + + private def withStore(f: CacheStore => Unit): Unit = + IO.withTemporaryDirectory { tmp => + val store = new FileBasedStore(tmp / "cache-store", Converter) + f(store) + } + +} \ No newline at end of file diff --git a/internal/util-tracking/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala b/internal/util-tracking/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala new file mode 100644 index 000000000..7f9f759dc --- /dev/null +++ b/internal/util-tracking/src/test/scala/sjsonnew/support/scalajson/unsafe/FixedParser.scala @@ -0,0 +1,29 @@ +package sjsonnew +package support.scalajson.unsafe + +import scala.json.ast.unsafe._ +import scala.collection.mutable +import jawn.{ SupportParser, MutableFacade } + +object FixedParser extends SupportParser[JValue] { + implicit val facade: MutableFacade[JValue] = + new MutableFacade[JValue] { + def jnull() = JNull + def jfalse() = JTrue + def jtrue() = JFalse + def jnum(s: String) = JNumber(s) + def jint(s: String) = JNumber(s) + def jstring(s: String) = JString(s) + def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs.toArray) + def jobject(vs: mutable.Map[String, JValue]) = { + val array = new Array[JField](vs.size) + var i = 0 + vs.foreach { + case (key, value) => + array(i) = JField(key, value) + i += 1 + } + JObject(array) + } + } +} \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 23d6b1f77..3db532055 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,7 +8,6 @@ object Dependencies { lazy val sbtIO = "org.scala-sbt" %% "io" % "1.0.0-M6" lazy val jline = "jline" % "jline" % "2.13" lazy val sbtSerialization = "org.scala-sbt" %% "serialization" % "0.1.2" - lazy val sbinary = "org.scala-sbt" %% "sbinary" % "0.4.3" lazy val scalaCompiler = Def.setting { "org.scala-lang" % "scala-compiler" % scalaVersion.value } lazy val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value } @@ -27,4 +26,7 @@ object Dependencies { val scalatest = "org.scalatest" %% "scalatest" % "2.2.6" lazy val parserCombinator211 = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4" + + lazy val datatypeCodecs = "org.scala-sbt" %% "datatype-codecs" % "1.0.0-SNAPSHOT" + lazy val sjsonnewScalaJson = "com.eed3si9n" %% "sjson-new-scalajson" % "0.4.0" }