Skip to content

Commit

Permalink
Merge scalapb/Lenses.git into scalapb/ScalaPB
Browse files Browse the repository at this point in the history
  • Loading branch information
thesamet committed May 29, 2018
2 parents 05b25b7 + a351d55 commit d362352
Show file tree
Hide file tree
Showing 9 changed files with 473 additions and 10 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Expand Up @@ -17,9 +17,7 @@ project/plugins/project/
.worksheet
.idea

# Auto-generated file.
compiler-plugin/src/main/scala/scalapb/compiler/Version.scala
e2e/project/project/Version.scala
e2e/project/Version.scala
e2e/.bin
atlassian-ide-plugin.xml
3 changes: 1 addition & 2 deletions .travis.yml
Expand Up @@ -26,7 +26,7 @@ matrix:
- sudo apt-get update
- curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x
script:
- sbt runtimeNative/test
- sbt runtimeNative/test lensesNative/test

- scala: 2.11.12
env: TEST_SCRIPT=__misc__
Expand Down Expand Up @@ -77,4 +77,3 @@ deploy:
tags: true
scala: 2.11.12
condition: "$TRAVIS_PULL_REQUEST = false && $TEST_SCRIPT = __misc__"

1 change: 0 additions & 1 deletion README.md
Expand Up @@ -98,4 +98,3 @@ The tests take a few minutes to run. There is a smaller test suite called
ScalaChecks on the outputs. To run it:

$ ./e2e.sh

59 changes: 54 additions & 5 deletions build.sbt
Expand Up @@ -4,7 +4,7 @@ import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
val Scala211 = "2.11.12"
scalaVersion in ThisBuild := Scala211

crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.4")
crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.6")

scalacOptions in ThisBuild ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down Expand Up @@ -37,7 +37,7 @@ releaseProcess := Seq[ReleaseStep](
setReleaseVersion,
commitReleaseVersion,
tagRelease,
releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned"),
releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned;lensesNative/publishSigned"),
ReleaseStep(action = "publishSigned" :: _, enableCrossBuild = true),
setNextVersion,
commitNextVersion,
Expand All @@ -53,14 +53,16 @@ lazy val root =
publishLocal := {},
siteSubdirName in ScalaUnidoc := "api/scalapb/latest",
addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc),
unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(runtimeJVM, grpcRuntime),
unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(lensesJVM, runtimeJVM, grpcRuntime),
git.remoteRepo := "git@github.com:scalapb/scalapb.github.io.git",
ghpagesBranch := "master",
ghpagesNoJekyll := false,
includeFilter in ghpagesCleanSite := GlobFilter((ghpagesRepository.value / "api/scalapb/latest/*").getCanonicalPath)
)
.enablePlugins(ScalaUnidocPlugin, GhpagesPlugin)
.aggregate(
lensesJS,
lensesJVM,
runtimeJS,
runtimeJVM,
grpcRuntime,
Expand All @@ -74,7 +76,6 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(
name := "scalapb-runtime",
libraryDependencies ++= Seq(
"com.thesamet.scalapb" %%% "lenses" % "0.7.0",
"com.lihaoyi" %%% "fastparse" % "1.0.0",
"com.lihaoyi" %%% "utest" % "0.6.4" % "test"
),
Expand All @@ -89,6 +90,7 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform)
)
}
)
.dependsOn(lenses)
.platformsSettings(JSPlatform, NativePlatform)(
libraryDependencies ++= Seq(
"com.thesamet.scalapb" %%% "protobuf-runtime-scala" % "0.7.1"
Expand Down Expand Up @@ -224,7 +226,6 @@ lazy val proptest = project.in(file("proptest"))
"com.google.protobuf" % "protobuf-java" % protobufVersion,
"io.grpc" % "grpc-netty" % grpcVersion % "test",
"io.grpc" % "grpc-protobuf" % grpcVersion % "test",
"com.thesamet.scalapb" %% "lenses" % "0.7.0",
"org.scalacheck" %% "scalacheck" % "1.13.5" % "test",
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
),
Expand Down Expand Up @@ -274,3 +275,51 @@ createVersionFile := {
val f2 = genVersionFile(base / "e2e/project/", v)
log.info(s"Created $f2")
}

lazy val lenses = crossProject(JSPlatform, JVMPlatform, NativePlatform).in(file("lenses"))
.settings(
name := "lenses",
sources in Test := {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) =>
// TODO utest_2.13.0-M3
Nil
case _ =>
(sources in Test).value
},
},
testFrameworks += new TestFramework("utest.runner.Framework"),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) =>
// TODO utest_2.13.0-M3
Nil
case _ =>
Seq(
"com.lihaoyi" %%% "utest" % "0.6.3" % "test"
)
}
},
mimaPreviousArtifacts := Set("com.thesamet.scalapb" %% "lenses" % "0.7.0"),
mimaBinaryIssueFilters ++= {
import com.typesafe.tools.mima.core._
Seq(
ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=$extension"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=")
)
}
)
.jsSettings(
scalacOptions += {
val a = (baseDirectory in LocalRootProject).value.toURI.toString
val g = "https://raw.githubusercontent.com/scalapb/ScalaPB/" + sys.process.Process("git rev-parse HEAD").lineStream_!.head
s"-P:scalajs:mapSourceURI:$a->$g/"
}
)
.nativeSettings(
nativeLinkStubs := true // for utest
)

lazy val lensesJVM = lenses.jvm
lazy val lensesJS = lenses.js
lazy val lensesNative = lenses.native
15 changes: 15 additions & 0 deletions lenses/shared/src/main/scala/com/trueaccord/lenses/package.scala
@@ -0,0 +1,15 @@
package com.trueaccord

package object lenses {
@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
type Updatable[A] = scalapb.lenses.Updatable[A]

@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
type Lens[Container, A] = scalapb.lenses.Lens[Container, A]

@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
type ObjectLens[U, Container] = scalapb.lenses.ObjectLens[U, Container]

@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
val Lens = scalapb.lenses.Lens
}
167 changes: 167 additions & 0 deletions lenses/shared/src/main/scala/scalapb/lenses/Lenses.scala
@@ -0,0 +1,167 @@
package scalapb.lenses

import scala.language.higherKinds

trait Lens[Container, A] extends Any {
self =>
/** get knows how to extract some field of type `A` from a container */
def get(c: Container): A

/** Represents an assignment operator.
*
* Given a value of type A, sets knows how to transform a container such that `a` is
* assigned to the field.
*
* We must have get(set(a)(c)) == a
*/
def set(a: A): Mutation[Container]

/** alias to set */
def :=(a: A) = set(a)

/** Represent an update operator (like x.y += 1 ) */
def modify(f: A => A): Mutation[Container] = c => set(f(get(c)))(c)

/** Composes two lenses, this enables nesting.
*
* If our field of type A has a sub-field of type B, then given a lens for it
* (other: Lens[A, B]) we can create a single lens from Container to B.
*/
def compose[B](other: Lens[A, B]): Lens[Container, B] = new Lens[Container, B] {
def get(c: Container) = other.get(self.get(c))

def set(b: B) = self.modify(other.set(b))
}

/** Given two lenses with the same origin, returns a new lens that can mutate both values
* represented by both lenses through a tuple.
*/
def zip[B](other: Lens[Container, B]): Lens[Container, (A, B)] = new Lens[Container, (A,B)] {
def get(c: Container): (A,B) = (self.get(c), other.get(c))
def set(t: (A, B)): Mutation[Container] = self.set(t._1).andThen(other.set(t._2))
}
}

object Lens {
/* Create a Lens from getter and setter. */
def apply[Container, A](getter: Container => A)(setter: (Container, A) => Container): Lens[Container, A] = new Lens[Container, A] {
def get(c: Container) = getter(c)

def set(a: A): Mutation[Container] = setter(_, a)
}

/** This is the unit lens, with respect to the compose operation defined above. That is,
* len.compose(unit) == len == unit.compose(len)
*
* More practically, you can view it as a len that mutates the entire object, instead of
* just a field of it: get() gives the original object, and set() returns the assigned value,
* no matter what the original value was.
*/
def unit[U]: Lens[U, U] = Lens(identity[U])((c, v) => v)

/** Implicit that adds some syntactic sugar if our lens watches a Seq-like collection. */
implicit class SeqLikeLens[U, A, Coll[A] <: collection.SeqLike[A, Coll[A]]](val lens: Lens[U, Coll[A]]) extends AnyVal {
type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]]

private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] =
lens.compose[A](Lens[Coll[A], A](getter)(setter))

def apply(i: Int)(implicit cbf: CBF): Lens[U, A] = field(_.apply(i))((c, v) => c.updated(i, v))

def head(implicit cbf: CBF): Lens[U, A] = apply(0)

def last(implicit cbf: CBF): Lens[U, A] = field(_.last)((c, v) => c.updated(c.size - 1, v))

def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ :+ item)

def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item)

def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] =
lens.modify(s => s.map {
(m: A) =>
val field: Lens[A, A] = Lens.unit[A]
val p: Mutation[A] = f(field)
p(m)
})
}

/** Implicit that adds some syntactic sugar if our lens watches a Set-like collection. */
implicit class SetLikeLens[U, A, Coll[A] <: collection.SetLike[A, Coll[A]] with Set[A]](val lens: Lens[U, Coll[A]]) extends AnyVal {
type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]]

private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] =
lens.compose[A](Lens[Coll[A], A](getter)(setter))

def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ + item)

def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item)

def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] =
lens.modify(s => s.map {
(m: A) =>
val field: Lens[A, A] = Lens.unit[A]
val p: Mutation[A] = f(field)
p(m)
})
}

/** Implicit that adds some syntactic sugar if our lens watches an Option[_]. */
implicit class OptLens[U, A](val lens: Lens[U, Option[A]]) extends AnyVal {
def inplaceMap(f: Lens[A, A] => Mutation[A]) =
lens.modify(opt => opt.map {
(m: A) =>
val field: Lens[A, A] = Lens.unit[A]
val p: Mutation[A] = f(field)
p(m)
})
}

/** Implicit that adds some syntactic sugar if our lens watches a Map[_, _]. */
implicit class MapLens[U, A, B](val lens: Lens[U, Map[A, B]]) extends AnyVal {
def apply(key: A): Lens[U, B] = lens.compose(Lens[Map[A, B], B](_.apply(key))((map, value) => map.updated(key, value)))

def :+=(pair: (A, B)) = lens.modify(_ + pair)

def :++=(item: Iterable[(A, B)]) = lens.modify(_ ++ item)

def foreach(f: Lens[(A, B), (A, B)] => Mutation[(A, B)]): Mutation[U] =
lens.modify(s => s.map {
(pair: (A, B)) =>
val field: Lens[(A, B), (A, B)] = Lens.unit[(A, B)]
val p: Mutation[(A, B)] = f(field)
p(pair)
})

def foreachValue(f: Lens[B, B] => Mutation[B]): Mutation[U] =
lens.modify(s => s.mapValues {
(m: B) =>
val field: Lens[B, B] = Lens.unit[B]
val p: Mutation[B] = f(field)
p(m)
})

def mapValues(f: B => B) = foreachValue(_.modify(f))
}
}

/** Represents a lens that has sub-lenses. */
class ObjectLens[U, Container](self: Lens[U, Container]) extends Lens[U, Container] {
/** Creates a sub-lens */
def field[A](lens: Lens[Container, A]): Lens[U, A] = self.compose(lens)

/** Creates a sub-lens */
def field[A](getter: Container => A)(setter: (Container, A) => Container): Lens[U, A] =
field(Lens(getter)(setter))

override def get(u: U) = self.get(u)

override def set(c: Container) = self.set(c)

def update(ms: (Lens[Container, Container] => Mutation[Container])*): Mutation[U] =
u => set(ms.foldLeft[Container](get(u))((p, m) => m(Lens.unit[Container])(p)))(u)
}

trait Updatable[A] extends Any {
self: A =>
def update(ms: (Lens[A, A] => Mutation[A])*): A = ms.foldLeft[A](self)((p, m) => m(Lens.unit[A])(p))
}
5 changes: 5 additions & 0 deletions lenses/shared/src/main/scala/scalapb/lenses/package.scala
@@ -0,0 +1,5 @@
package scalapb

package object lenses {
type Mutation[C] = C => C
}

0 comments on commit d362352

Please sign in to comment.