From bf26793e5dd2e3cc8e27ff2f4b2a24ab29e0d64d Mon Sep 17 00:00:00 2001 From: Pathikrit Bhowmick Date: Sun, 22 Mar 2015 12:45:24 -0700 Subject: [PATCH 1/2] first cut at .each --- README.md | 6 +----- .../com/github/pathikrit/sauron/package.scala | 9 +++++++++ .../pathikrit/sauron/suites/SauronSuite.scala | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6875b97..90da473 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ val person = Person(Address(Street("1 Functional Rd."))) import com.github.pathikrit.sauron._ +lens(person)(_.address.street.name).setTo("1 Objective Rd.") lens(person)(_.address.street.name)(_.toUpperCase) ``` @@ -23,11 +24,6 @@ person.copy(address = person.address.copy( ) ``` -**Simple setters**: -```scala -lens(person)(_.address.street.name).setTo("1 Objective Rd.") -``` - **Reusable lenses**: ```scala val f1 = lens(person)(_.address.street.name) diff --git a/src/main/scala/com/github/pathikrit/sauron/package.scala b/src/main/scala/com/github/pathikrit/sauron/package.scala index f8dc369..4830941 100644 --- a/src/main/scala/com/github/pathikrit/sauron/package.scala +++ b/src/main/scala/com/github/pathikrit/sauron/package.scala @@ -14,8 +14,13 @@ package object sauron { def lensImpl[A, B](c: blackbox.Context)(obj: c.Expr[A])(path: c.Expr[A => B]): c.Tree = { import c.universe._ + case class PathElement(term: c.TermName, isEach: Boolean) + def split(accessor: c.Tree): List[c.TermName] = accessor match { // (_.p.q.r) -> List(p, q, r) case q"$pq.$r" => split(pq) :+ r + case q"$tpName[..$_]($r)" if tpName.toString.contains("sauron.`package`.IterableOps") => + + c.abort(c.enclosingPosition, s"Hooray: $accessor, $r") case _: Ident => Nil case _ => c.abort(c.enclosingPosition, s"Unsupported path element: $accessor") } @@ -42,4 +47,8 @@ package object sauron { implicit class UpdaterOps[A, B](val f: Updater[A, B]) extends AnyVal { def setTo(v: B): A = f(_ => v) } + + implicit class IterableOps[A](val l: Iterable[A]) extends AnyVal { + def each: A = throw new UnsupportedOperationException(".each can only be used within sauron's lens macro") + } } diff --git a/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala b/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala index 043b2a6..22d3b40 100644 --- a/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala +++ b/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala @@ -7,10 +7,10 @@ class SauronSuite extends FunSuite { import com.github.pathikrit.sauron._ case class Person(name: String, address: Address) - case class Address(street: Street, street2: Option[Street], city: String, state: String, zip: String, country: String) + case class Address(street: Street, street2: List[Street], city: String, state: String, zip: String, country: Option[String]) case class Street(name: String) - val p1 = Person("Rick", Address(Street("Rock St"), None, "MtV", "CA", "94041", "USA")) + val p1 = Person("Rick", Address(Street("Rock St"), Nil, "MtV", "CA", "94041", Some("USA"))) def addHouseNumber(number: Int)(st: String) = s"$number $st" val p2 = lens(p1)(_.address.street.name)(addHouseNumber(1901)) @@ -37,6 +37,18 @@ class SauronSuite extends FunSuite { val p5: Person = lens(p1)(_.address.street.name).setTo("Rick St") p5.address.street.name shouldEqual "Rick St" + lens(p1)(_.address.street2.each.name)(_.toUpperCase) + + def f(s: String) = s.toUpperCase + + p1.copy( + address = p1.address.copy( + street2 = p1.address.street2.map(el => el.copy(name = f(el.name))) + ) + ) + + // should not typecheck + "lens(p1)(_.address.zip)(_.toUpperCase)" should compile "lens(p1)(_.address.zip.length)(_ + 1)" shouldNot compile From cdb9ede302fc6653e5be47e77b000c822e0b5627 Mon Sep 17 00:00:00 2001 From: Pathikrit Bhowmick Date: Sun, 22 Mar 2015 13:11:24 -0700 Subject: [PATCH 2/2] make .each compile time only --- README.md | 2 +- .../com/github/pathikrit/sauron/package.scala | 8 ++++++-- .../pathikrit/sauron/suites/SauronSuite.scala | 17 ++++++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 90da473..3739778 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Sauron [![Circle CI](https://img.shields.io/circleci/project/pathikrit/sauron.svg)](https://circleci.com/gh/pathikrit/sauron) [![Download](https://api.bintray.com/packages/pathikrit/maven/sauron/images/download.svg)](https://bintray.com/pathikrit/maven/sauron/_latestVersion) +Sauron [![Circle CI](https://img.shields.io/circleci/project/pathikrit/sauron/master.svg)](https://circleci.com/gh/pathikrit/sauron) [![Download](https://api.bintray.com/packages/pathikrit/maven/sauron/images/download.svg)](https://bintray.com/pathikrit/maven/sauron/_latestVersion) -------- Lightweight [lens library](http://stackoverflow.com/questions/3900307/cleaner-way-to-update-nested-structures) in less than [50-lines of Scala](src/main/scala/com/github/pathikrit/sauron/package.scala): diff --git a/src/main/scala/com/github/pathikrit/sauron/package.scala b/src/main/scala/com/github/pathikrit/sauron/package.scala index 4830941..17741dd 100644 --- a/src/main/scala/com/github/pathikrit/sauron/package.scala +++ b/src/main/scala/com/github/pathikrit/sauron/package.scala @@ -1,5 +1,6 @@ package com.github.pathikrit +import scala.annotation.compileTimeOnly import scala.reflect.macros.blackbox package object sauron { @@ -19,7 +20,6 @@ package object sauron { def split(accessor: c.Tree): List[c.TermName] = accessor match { // (_.p.q.r) -> List(p, q, r) case q"$pq.$r" => split(pq) :+ r case q"$tpName[..$_]($r)" if tpName.toString.contains("sauron.`package`.IterableOps") => - c.abort(c.enclosingPosition, s"Hooray: $accessor, $r") case _: Ident => Nil case _ => c.abort(c.enclosingPosition, s"Unsupported path element: $accessor") @@ -49,6 +49,10 @@ package object sauron { } implicit class IterableOps[A](val l: Iterable[A]) extends AnyVal { - def each: A = throw new UnsupportedOperationException(".each can only be used within sauron's lens macro") + @compileTimeOnly(IterableOps.errorMsg) def each: A = throw new UnsupportedOperationException(IterableOps.errorMsg) + } + + object IterableOps { + def errorMsg = ".each can only be used within sauron's lens macro" } } diff --git a/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala b/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala index 22d3b40..3067d79 100644 --- a/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala +++ b/src/test/scala/com/github/pathikrit/sauron/suites/SauronSuite.scala @@ -3,9 +3,14 @@ package com.github.pathikrit.sauron.suites import org.scalatest._, Matchers._ class SauronSuite extends FunSuite { - test("lensing") { - import com.github.pathikrit.sauron._ + import com.github.pathikrit.sauron._ + + case class A(a1: String, a2: B) + case class B(b1: Option[Int], b2: List[C], b3: C) + case class C(c1: Int, c2: List[D], c3: D) + case class D(d1: List[String], d2: Option[Boolean], d3: A, d4: D) + test("lensing") { case class Person(name: String, address: Address) case class Address(street: Street, street2: List[Street], city: String, state: String, zip: String, country: Option[String]) case class Street(name: String) @@ -37,7 +42,7 @@ class SauronSuite extends FunSuite { val p5: Person = lens(p1)(_.address.street.name).setTo("Rick St") p5.address.street.name shouldEqual "Rick St" - lens(p1)(_.address.street2.each.name)(_.toUpperCase) + //lens(p1)(_.address.street2.each.name)(_.toUpperCase) def f(s: String) = s.toUpperCase @@ -47,6 +52,12 @@ class SauronSuite extends FunSuite { ) ) + p1.copy( + address = p1.address.copy( + country = p1.address.country.map(_.toUpperCase) + ) + ) + // should not typecheck