Skip to content

Commit d362352

Browse files
committed
Merge scalapb/Lenses.git into scalapb/ScalaPB
2 parents 05b25b7 + a351d55 commit d362352

File tree

9 files changed

+473
-10
lines changed

9 files changed

+473
-10
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ project/plugins/project/
1717
.worksheet
1818
.idea
1919

20-
# Auto-generated file.
2120
compiler-plugin/src/main/scala/scalapb/compiler/Version.scala
2221
e2e/project/project/Version.scala
2322
e2e/project/Version.scala
2423
e2e/.bin
25-
atlassian-ide-plugin.xml

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ matrix:
2626
- sudo apt-get update
2727
- curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x
2828
script:
29-
- sbt runtimeNative/test
29+
- sbt runtimeNative/test lensesNative/test
3030

3131
- scala: 2.11.12
3232
env: TEST_SCRIPT=__misc__
@@ -77,4 +77,3 @@ deploy:
7777
tags: true
7878
scala: 2.11.12
7979
condition: "$TRAVIS_PULL_REQUEST = false && $TEST_SCRIPT = __misc__"
80-

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,3 @@ The tests take a few minutes to run. There is a smaller test suite called
9898
ScalaChecks on the outputs. To run it:
9999

100100
$ ./e2e.sh
101-

build.sbt

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
44
val Scala211 = "2.11.12"
55
scalaVersion in ThisBuild := Scala211
66

7-
crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.4")
7+
crossScalaVersions in ThisBuild := Seq("2.10.7", Scala211, "2.12.6")
88

99
scalacOptions in ThisBuild ++= {
1010
CrossVersion.partialVersion(scalaVersion.value) match {
@@ -37,7 +37,7 @@ releaseProcess := Seq[ReleaseStep](
3737
setReleaseVersion,
3838
commitReleaseVersion,
3939
tagRelease,
40-
releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned"),
40+
releaseStepCommandAndRemaining(s";++${Scala211};runtimeNative/publishSigned;lensesNative/publishSigned"),
4141
ReleaseStep(action = "publishSigned" :: _, enableCrossBuild = true),
4242
setNextVersion,
4343
commitNextVersion,
@@ -53,14 +53,16 @@ lazy val root =
5353
publishLocal := {},
5454
siteSubdirName in ScalaUnidoc := "api/scalapb/latest",
5555
addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc),
56-
unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(runtimeJVM, grpcRuntime),
56+
unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(lensesJVM, runtimeJVM, grpcRuntime),
5757
git.remoteRepo := "git@github.com:scalapb/scalapb.github.io.git",
5858
ghpagesBranch := "master",
5959
ghpagesNoJekyll := false,
6060
includeFilter in ghpagesCleanSite := GlobFilter((ghpagesRepository.value / "api/scalapb/latest/*").getCanonicalPath)
6161
)
6262
.enablePlugins(ScalaUnidocPlugin, GhpagesPlugin)
6363
.aggregate(
64+
lensesJS,
65+
lensesJVM,
6466
runtimeJS,
6567
runtimeJVM,
6668
grpcRuntime,
@@ -74,7 +76,6 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform)
7476
.settings(
7577
name := "scalapb-runtime",
7678
libraryDependencies ++= Seq(
77-
"com.thesamet.scalapb" %%% "lenses" % "0.7.0",
7879
"com.lihaoyi" %%% "fastparse" % "1.0.0",
7980
"com.lihaoyi" %%% "utest" % "0.6.4" % "test"
8081
),
@@ -89,6 +90,7 @@ lazy val runtime = crossProject(JSPlatform, JVMPlatform, NativePlatform)
8990
)
9091
}
9192
)
93+
.dependsOn(lenses)
9294
.platformsSettings(JSPlatform, NativePlatform)(
9395
libraryDependencies ++= Seq(
9496
"com.thesamet.scalapb" %%% "protobuf-runtime-scala" % "0.7.1"
@@ -224,7 +226,6 @@ lazy val proptest = project.in(file("proptest"))
224226
"com.google.protobuf" % "protobuf-java" % protobufVersion,
225227
"io.grpc" % "grpc-netty" % grpcVersion % "test",
226228
"io.grpc" % "grpc-protobuf" % grpcVersion % "test",
227-
"com.thesamet.scalapb" %% "lenses" % "0.7.0",
228229
"org.scalacheck" %% "scalacheck" % "1.13.5" % "test",
229230
"org.scalatest" %% "scalatest" % "3.0.5" % "test"
230231
),
@@ -274,3 +275,51 @@ createVersionFile := {
274275
val f2 = genVersionFile(base / "e2e/project/", v)
275276
log.info(s"Created $f2")
276277
}
278+
279+
lazy val lenses = crossProject(JSPlatform, JVMPlatform, NativePlatform).in(file("lenses"))
280+
.settings(
281+
name := "lenses",
282+
sources in Test := {
283+
CrossVersion.partialVersion(scalaVersion.value) match {
284+
case Some((2, 13)) =>
285+
// TODO utest_2.13.0-M3
286+
Nil
287+
case _ =>
288+
(sources in Test).value
289+
},
290+
},
291+
testFrameworks += new TestFramework("utest.runner.Framework"),
292+
libraryDependencies ++= {
293+
CrossVersion.partialVersion(scalaVersion.value) match {
294+
case Some((2, 13)) =>
295+
// TODO utest_2.13.0-M3
296+
Nil
297+
case _ =>
298+
Seq(
299+
"com.lihaoyi" %%% "utest" % "0.6.3" % "test"
300+
)
301+
}
302+
},
303+
mimaPreviousArtifacts := Set("com.thesamet.scalapb" %% "lenses" % "0.7.0"),
304+
mimaBinaryIssueFilters ++= {
305+
import com.typesafe.tools.mima.core._
306+
Seq(
307+
ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=$extension"),
308+
ProblemFilters.exclude[IncompatibleMethTypeProblem]("scalapb.lenses.Lens#MapLens.:++=")
309+
)
310+
}
311+
)
312+
.jsSettings(
313+
scalacOptions += {
314+
val a = (baseDirectory in LocalRootProject).value.toURI.toString
315+
val g = "https://raw.githubusercontent.com/scalapb/ScalaPB/" + sys.process.Process("git rev-parse HEAD").lineStream_!.head
316+
s"-P:scalajs:mapSourceURI:$a->$g/"
317+
}
318+
)
319+
.nativeSettings(
320+
nativeLinkStubs := true // for utest
321+
)
322+
323+
lazy val lensesJVM = lenses.jvm
324+
lazy val lensesJS = lenses.js
325+
lazy val lensesNative = lenses.native
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.trueaccord
2+
3+
package object lenses {
4+
@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
5+
type Updatable[A] = scalapb.lenses.Updatable[A]
6+
7+
@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
8+
type Lens[Container, A] = scalapb.lenses.Lens[Container, A]
9+
10+
@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
11+
type ObjectLens[U, Container] = scalapb.lenses.ObjectLens[U, Container]
12+
13+
@deprecated("Use scalapb.lenses package instead of com.trueaccord.lenses", "0.7.0")
14+
val Lens = scalapb.lenses.Lens
15+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package scalapb.lenses
2+
3+
import scala.language.higherKinds
4+
5+
trait Lens[Container, A] extends Any {
6+
self =>
7+
/** get knows how to extract some field of type `A` from a container */
8+
def get(c: Container): A
9+
10+
/** Represents an assignment operator.
11+
*
12+
* Given a value of type A, sets knows how to transform a container such that `a` is
13+
* assigned to the field.
14+
*
15+
* We must have get(set(a)(c)) == a
16+
*/
17+
def set(a: A): Mutation[Container]
18+
19+
/** alias to set */
20+
def :=(a: A) = set(a)
21+
22+
/** Represent an update operator (like x.y += 1 ) */
23+
def modify(f: A => A): Mutation[Container] = c => set(f(get(c)))(c)
24+
25+
/** Composes two lenses, this enables nesting.
26+
*
27+
* If our field of type A has a sub-field of type B, then given a lens for it
28+
* (other: Lens[A, B]) we can create a single lens from Container to B.
29+
*/
30+
def compose[B](other: Lens[A, B]): Lens[Container, B] = new Lens[Container, B] {
31+
def get(c: Container) = other.get(self.get(c))
32+
33+
def set(b: B) = self.modify(other.set(b))
34+
}
35+
36+
/** Given two lenses with the same origin, returns a new lens that can mutate both values
37+
* represented by both lenses through a tuple.
38+
*/
39+
def zip[B](other: Lens[Container, B]): Lens[Container, (A, B)] = new Lens[Container, (A,B)] {
40+
def get(c: Container): (A,B) = (self.get(c), other.get(c))
41+
def set(t: (A, B)): Mutation[Container] = self.set(t._1).andThen(other.set(t._2))
42+
}
43+
}
44+
45+
object Lens {
46+
/* Create a Lens from getter and setter. */
47+
def apply[Container, A](getter: Container => A)(setter: (Container, A) => Container): Lens[Container, A] = new Lens[Container, A] {
48+
def get(c: Container) = getter(c)
49+
50+
def set(a: A): Mutation[Container] = setter(_, a)
51+
}
52+
53+
/** This is the unit lens, with respect to the compose operation defined above. That is,
54+
* len.compose(unit) == len == unit.compose(len)
55+
*
56+
* More practically, you can view it as a len that mutates the entire object, instead of
57+
* just a field of it: get() gives the original object, and set() returns the assigned value,
58+
* no matter what the original value was.
59+
*/
60+
def unit[U]: Lens[U, U] = Lens(identity[U])((c, v) => v)
61+
62+
/** Implicit that adds some syntactic sugar if our lens watches a Seq-like collection. */
63+
implicit class SeqLikeLens[U, A, Coll[A] <: collection.SeqLike[A, Coll[A]]](val lens: Lens[U, Coll[A]]) extends AnyVal {
64+
type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]]
65+
66+
private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] =
67+
lens.compose[A](Lens[Coll[A], A](getter)(setter))
68+
69+
def apply(i: Int)(implicit cbf: CBF): Lens[U, A] = field(_.apply(i))((c, v) => c.updated(i, v))
70+
71+
def head(implicit cbf: CBF): Lens[U, A] = apply(0)
72+
73+
def last(implicit cbf: CBF): Lens[U, A] = field(_.last)((c, v) => c.updated(c.size - 1, v))
74+
75+
def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ :+ item)
76+
77+
def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item)
78+
79+
def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] =
80+
lens.modify(s => s.map {
81+
(m: A) =>
82+
val field: Lens[A, A] = Lens.unit[A]
83+
val p: Mutation[A] = f(field)
84+
p(m)
85+
})
86+
}
87+
88+
/** Implicit that adds some syntactic sugar if our lens watches a Set-like collection. */
89+
implicit class SetLikeLens[U, A, Coll[A] <: collection.SetLike[A, Coll[A]] with Set[A]](val lens: Lens[U, Coll[A]]) extends AnyVal {
90+
type CBF = collection.generic.CanBuildFrom[Coll[A], A, Coll[A]]
91+
92+
private def field(getter: Coll[A] => A)(setter: (Coll[A], A) => Coll[A]): Lens[U, A] =
93+
lens.compose[A](Lens[Coll[A], A](getter)(setter))
94+
95+
def :+=(item: A)(implicit cbf: CBF) = lens.modify(_ + item)
96+
97+
def :++=(item: scala.collection.GenTraversableOnce[A])(implicit cbf: CBF) = lens.modify(_ ++ item)
98+
99+
def foreach(f: Lens[A, A] => Mutation[A])(implicit cbf: CBF): Mutation[U] =
100+
lens.modify(s => s.map {
101+
(m: A) =>
102+
val field: Lens[A, A] = Lens.unit[A]
103+
val p: Mutation[A] = f(field)
104+
p(m)
105+
})
106+
}
107+
108+
/** Implicit that adds some syntactic sugar if our lens watches an Option[_]. */
109+
implicit class OptLens[U, A](val lens: Lens[U, Option[A]]) extends AnyVal {
110+
def inplaceMap(f: Lens[A, A] => Mutation[A]) =
111+
lens.modify(opt => opt.map {
112+
(m: A) =>
113+
val field: Lens[A, A] = Lens.unit[A]
114+
val p: Mutation[A] = f(field)
115+
p(m)
116+
})
117+
}
118+
119+
/** Implicit that adds some syntactic sugar if our lens watches a Map[_, _]. */
120+
implicit class MapLens[U, A, B](val lens: Lens[U, Map[A, B]]) extends AnyVal {
121+
def apply(key: A): Lens[U, B] = lens.compose(Lens[Map[A, B], B](_.apply(key))((map, value) => map.updated(key, value)))
122+
123+
def :+=(pair: (A, B)) = lens.modify(_ + pair)
124+
125+
def :++=(item: Iterable[(A, B)]) = lens.modify(_ ++ item)
126+
127+
def foreach(f: Lens[(A, B), (A, B)] => Mutation[(A, B)]): Mutation[U] =
128+
lens.modify(s => s.map {
129+
(pair: (A, B)) =>
130+
val field: Lens[(A, B), (A, B)] = Lens.unit[(A, B)]
131+
val p: Mutation[(A, B)] = f(field)
132+
p(pair)
133+
})
134+
135+
def foreachValue(f: Lens[B, B] => Mutation[B]): Mutation[U] =
136+
lens.modify(s => s.mapValues {
137+
(m: B) =>
138+
val field: Lens[B, B] = Lens.unit[B]
139+
val p: Mutation[B] = f(field)
140+
p(m)
141+
})
142+
143+
def mapValues(f: B => B) = foreachValue(_.modify(f))
144+
}
145+
}
146+
147+
/** Represents a lens that has sub-lenses. */
148+
class ObjectLens[U, Container](self: Lens[U, Container]) extends Lens[U, Container] {
149+
/** Creates a sub-lens */
150+
def field[A](lens: Lens[Container, A]): Lens[U, A] = self.compose(lens)
151+
152+
/** Creates a sub-lens */
153+
def field[A](getter: Container => A)(setter: (Container, A) => Container): Lens[U, A] =
154+
field(Lens(getter)(setter))
155+
156+
override def get(u: U) = self.get(u)
157+
158+
override def set(c: Container) = self.set(c)
159+
160+
def update(ms: (Lens[Container, Container] => Mutation[Container])*): Mutation[U] =
161+
u => set(ms.foldLeft[Container](get(u))((p, m) => m(Lens.unit[Container])(p)))(u)
162+
}
163+
164+
trait Updatable[A] extends Any {
165+
self: A =>
166+
def update(ms: (Lens[A, A] => Mutation[A])*): A = ms.foldLeft[A](self)((p, m) => m(Lens.unit[A])(p))
167+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package scalapb
2+
3+
package object lenses {
4+
type Mutation[C] = C => C
5+
}

0 commit comments

Comments
 (0)