Using Lenses with java.net.URI.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.net.URI | |
/** | |
* State co-monad. | |
* | |
* `Store[F, _]` is the co-monad. | |
*/ | |
final case class Store[F, R](get: F, set: F => R) { | |
def map[S](f: R => S): Store[F, S] = Store(get, f compose set) | |
/** | |
* Aka "coFlatMap" aka "extend". | |
* | |
* @param f | |
* @tparam S | |
* @return | |
*/ | |
def =>>[S](f: Store[F, R] => S): Store[F, S] = Store(get, x => f(Store(x, set))) | |
/** | |
* Aka "coFlatten". | |
* | |
* @return | |
*/ | |
def duplicate: Store[F, Store[F, R]] = this =>> identity | |
} | |
/** | |
* An asymmetric lens. | |
* | |
* @tparam R The record type. | |
* @tparam F The field type. | |
*/ | |
final case class @>[R, F](store: R => Store[F, R]) { | |
/** | |
* Compose two lenses. | |
*/ | |
def <=<[R2](g: R2 @> R): R2 @> F = @> { | |
q => | |
val rr2Store: Store[R, R2] = g.store(q) | |
val frStore: Store[F, R] = this.store(rr2Store.get) | |
Store(get = frStore.get, set = rr2Store.set compose frStore.set) | |
} | |
/** | |
* Compose with arguments reversed, aka "andThen". | |
*/ | |
def >=>[F2](g: F @> F2): R @> F2 = g <=< this | |
def modify(record: R)(mod: F => F): R = { | |
val s = store(record) | |
s.set(mod(s.get)) | |
} | |
def get(record: R): F = store(record).get | |
def set(record: R, value: F): R = store(record).set(value) | |
/** | |
* Converts this lens to a partial lens that is always defined. | |
*/ | |
def partial: R @>? F = @>?(r => Some(store(r))) | |
} | |
/** | |
* A partial lens. | |
*/ | |
final case class @>?[T, C](store: T => Option[Store[C, T]]) { | |
/** | |
* Compose with arguments reversed, aka "andThen". | |
*/ | |
def >=>[C2](other: C @>? C2): T @>? C2 = @>?(t => for { | |
c <- store(t) | |
d <- other store c.get | |
} yield Store(get = d.get, set = x => c set (d set x))) | |
def set(t: T, c: C): T = store(t) map (_.set(c)) getOrElse t | |
def get(t: T): Option[C] = store(t) map (_.get) | |
def modify(t: T)(mod: C => C): T = store(t) map (s => s.set(mod(s.get))) getOrElse t | |
def isDefined(t: T): Boolean = store(t).isDefined | |
} | |
object Lens { | |
def apply[R, F](getter: R => F, setter: F => R => R): R @> F = @>[R, F](r => Store(getter(r), setter(_)(r))) | |
} | |
object LensURI { | |
implicit final class RichURI(val uri: URI) extends AnyVal { | |
def scheme = Option(uri.getScheme) | |
def userInfo = Option(uri.getUserInfo) | |
def host = Option(uri.getHost) | |
def port = if (uri.getPort < 0) None else Some(uri.getPort) | |
def path = Option(uri.getPath) | |
def query = Option(uri.getQuery) | |
def fragment = Option(uri.getFragment) | |
def copy(scheme: Option[String] = scheme, | |
userInfo: Option[String] = userInfo, | |
host: Option[String] = host, | |
port: Option[Int] = port, | |
path: Option[String] = path, | |
query: Option[String] = query, | |
fragment: Option[String] = fragment): URI = new URI( | |
scheme.orNull, | |
userInfo.orNull, | |
host.orNull, | |
port getOrElse -1, | |
path.orNull, | |
query.orNull, | |
fragment.orNull | |
) | |
} | |
object lens { | |
val scheme: URI @> Option[String] = Lens(_.scheme, v => _.copy(scheme = v)) | |
val userInfo: URI @> Option[String] = Lens(_.userInfo, v => _.copy(userInfo = v)) | |
val host: URI @> Option[String] = Lens(_.host, v => _.copy(host = v)) | |
val port: URI @> Option[Int] = Lens(_.port, v => _.copy(port = v)) | |
val pathString: URI @> Option[String] = Lens(_.path, v => _.copy(path = v)) | |
val queryString: URI @> Option[String] = Lens(_.query, v => _.copy(query = v)) | |
val fragment: URI @> Option[String] = Lens(_.fragment, v => _.copy(fragment = v)) | |
val emptyFragment: URI @> String = Lens(r => r.fragment getOrElse "", { v => | |
val x = if (v.isEmpty) None else Some(v) | |
_.copy(fragment = x) | |
}) | |
val query: URI @> Map[String, String] = Lens( | |
r => parse(r.query), | |
v => _.copy(query = toQueryString(v)) | |
) | |
val pathList: URI @> List[String] = Lens(pathToList _, | |
v => r => r.copy(path = v match { | |
case Nil => None | |
case xs => Some(xs.reverse mkString "/") | |
}) | |
) | |
} | |
def pathToList(uri: URI) = uri.path match { | |
case Some("") if uri.isAbsolute => List("") | |
case None | Some("") => Nil | |
case Some(s) if s forall (_ == '/') => List("") | |
case Some(s) => s.split('/').toList.reverse | |
} | |
def appendFooToPathJava(u: URI): URI = { | |
new URI(u.getScheme, u.getAuthority, (if (u.getPath eq null) "" else u.getPath) + "/foo", u.getQuery, u.getFragment) | |
} | |
def appendFooToPathCopy(u: URI): URI = u.copy(path = Some((u.path getOrElse "") + "/foo")) | |
def appendFooToPathLens(u: URI): URI = lens.pathList.modify(u)("foo" :: _) | |
private val regex = """([^=&]+)=([^&]+)&?""".r // demo purposes only!! | |
def parse(queryString: Option[String]): Map[String, String] = queryString map { q => | |
(regex.findAllMatchIn(q) map (m => m.group(1) -> m.group(2))).toMap | |
} getOrElse Map.empty | |
def toQueryString(pairs: Iterable[(String, String)]) = if (pairs.isEmpty) None else Some(pairs map { | |
case (k, v) => s"$k=$v" | |
} mkString "&") | |
def mapLens[A, B](key: A): Map[A, B] @> Option[B] = Lens(_ get key, _ map (v => (_: Map[A, B]) + (key -> v)) getOrElse (_ - key)) | |
def mapLensDefault[A, B](key: A, default: B): Map[A, B] @> B = Lens( | |
_.getOrElse(key, default), | |
v => if (v == default) (_ - key) else _ + (key -> v) | |
) | |
/** | |
* Java version of appending a query argument to an existing URI. | |
* | |
* I can't believe I used to program like this. | |
*/ | |
def addQueryArgJava(u: URI): URI = { | |
val origQuery = if (u.getQuery eq null) "" else u.getQuery + '&' | |
new URI(u.getScheme, u.getAuthority, u.getPath, origQuery + "foo=bar", u.getFragment) | |
} | |
def addQueryArgCopy(u: URI): URI = { | |
u.copy(query = Some((u.query map (_ + '&') getOrElse "") + "foo=bar" )) | |
// or - u.copy(query = toQueryString(parse(u.query) + ("foo" -> "bar"))) | |
} | |
def addQueryArgLensString(u: URI): URI = { | |
lens.queryString.modify(u)(x => Some((x map (_ + '&') getOrElse "") + "foo=bar")) | |
} | |
def addQueryArgLens(u: URI): URI = { | |
lens.query >=> mapLens("foo") set (u, Some("bar")) | |
} | |
/** | |
* Java version of removing query argument "foo", if it is present. | |
*/ | |
def removeFooArgJava(u: URI): URI = { | |
val re = "foo=([^&]+)&?".r.pattern | |
val matcher = re.matcher(if (u.getQuery eq null) "" else u.getQuery) | |
val newQuery = matcher.replaceFirst("") | |
new URI(u.getScheme, u.getAuthority, u.getPath, if (newQuery.isEmpty) null else newQuery, u.getFragment) | |
} | |
def removeFooArgCopy(u: URI): URI = { | |
val re = "foo=([^&]+)&?".r | |
val newQuery = re.replaceFirstIn(u.query getOrElse "", "") | |
u.copy(query = if (newQuery.isEmpty) None else Some(newQuery)) | |
} | |
def removeFooArgLens(u: URI): URI = { | |
lens.query >=> mapLens("foo") set (u, None) | |
} | |
def main(args: Array[String]) { | |
val uri1 = URI.create("http://www.site.fake/path/a/b?a=1&b=2&c=3") | |
assert(addQueryArgJava(uri1) == URI.create("http://www.site.fake/path/a/b?a=1&b=2&c=3&foo=bar")) | |
assert(addQueryArgCopy(uri1) == URI.create("http://www.site.fake/path/a/b?a=1&b=2&c=3&foo=bar")) | |
assert(addQueryArgLensString(uri1) == URI.create("http://www.site.fake/path/a/b?a=1&b=2&c=3&foo=bar")) | |
assert(addQueryArgLens(uri1) == URI.create("http://www.site.fake/path/a/b?a=1&b=2&c=3&foo=bar")) | |
val uri2 = URI.create("/path?a=1&foo=bar&b=2") | |
assert(removeFooArgJava(uri2) == URI.create("/path?a=1&b=2")) | |
assert(removeFooArgCopy(uri2) == URI.create("/path?a=1&b=2")) | |
assert(removeFooArgLens(uri2) == URI.create("/path?a=1&b=2")) | |
val uri3 = URI.create("http://www.google.com") | |
assert(removeFooArgJava(uri3) == uri3) | |
assert(removeFooArgCopy(uri3) == uri3) | |
assert(removeFooArgLens(uri3) == uri3) | |
println("Done!") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment