diff --git a/README.md b/README.md index 6875b97..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): @@ -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..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 { @@ -14,8 +15,12 @@ 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,12 @@ 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 { + @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 043b2a6..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,14 +3,19 @@ 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: 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 +42,24 @@ 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))) + ) + ) + + p1.copy( + address = p1.address.copy( + country = p1.address.country.map(_.toUpperCase) + ) + ) + + // should not typecheck + "lens(p1)(_.address.zip)(_.toUpperCase)" should compile "lens(p1)(_.address.zip.length)(_ + 1)" shouldNot compile