japgolly.webapputil.general.Url.scala Maven / Gradle / Ivy
package japgolly.webapputil.general
import japgolly.univeq._
object Url {
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* @param relativeUrlNoHeadSlash The URL without a leading slash.
*/
final case class Relative private[Relative](relativeUrlNoHeadSlash: String) extends AnyVal {
override def toString = relativeUrl
def underlying : String = relativeUrlNoHeadSlash
def relativeUrlNoHeadOrTailSlash : String = dropTailSlashes(relativeUrlNoHeadSlash)
def relativeUrlNoTailSlash : String = "/" + relativeUrlNoHeadOrTailSlash
def relativeUrl : String = "/" + relativeUrlNoHeadSlash
def relativeUrlWithHeadAndTailSlashes: String = "/" + relativeUrlNoHeadOrTailSlash + "/"
def isRoot: Boolean =
relativeUrlNoHeadSlash.isEmpty
def thenParam[A](f: A => String): Relative.Param1[A] =
new Relative.Param1(new Relative(relativeUrlNoHeadOrTailSlash), f)
def isParentOf: Relative => Boolean = {
val prefix = relativeUrlNoHeadOrTailSlash + "/"
_.underlying.startsWith(prefix)
}
def isEqualToOrParentOf: Relative => Boolean = {
val prefix = relativeUrlNoHeadOrTailSlash
child => {
val lencmp = child.relativeUrlNoHeadSlash.length - prefix.length
(lencmp >= 0) &&
(child.relativeUrlNoHeadOrTailSlash startsWith prefix) &&
((lencmp == 0) || (child.relativeUrlNoHeadSlash.charAt(prefix.length) == '/'))
}
}
/** Use isEqualToOrParentOf first or else this may crash! */
def removeSelfOrParent: Relative => Relative = {
val l = relativeUrlNoHeadOrTailSlash.length
child => Relative(child.relativeUrlNoHeadSlash.substring(l))
}
def /(s: String): Relative = {
val next = Relative(s)
if (this.isRoot)
next
else if (next.isRoot)
this
else
new Relative(relativeUrlNoHeadOrTailSlash + "/" + next.relativeUrlNoHeadSlash)
}
}
object Relative {
def apply(value: String): Relative =
new Relative(dropHeadSlashes(value))
def root: Relative =
apply("")
implicit def univEq: UnivEq[Relative] = UnivEq.derive
/** Represents `/prefix/`; the param is always last */
final class Param1[-A] private[Relative](val prefix: Relative, val suffix: A => String) {
val prefixNoHeadSlash: String =
if (prefix.isRoot)
""
else
prefix.relativeUrlNoHeadOrTailSlash + "/"
def apply(a: A): Relative =
new Relative(prefixNoHeadSlash + suffix(a))
def thenParam[B](f: B => String): Relative.Param2[A, B] =
thenParam("/", f)
def thenParam[B](sep: String, f: B => String): Relative.Param2[A, B] =
new Relative.Param2(prefix, suffix, sep, f)
}
/** Represents `/prefix/`; the param is always last */
final class Param2[-A, -B] private[Relative](val prefix: Relative, val pa: A => String, val sep: String, val pb: B => String) {
val prefixNoHeadSlash: String =
if (prefix.isRoot)
""
else
prefix.relativeUrlNoHeadOrTailSlash + "/"
def apply(a: A, b: B): Relative =
new Relative(prefixNoHeadSlash + pa(a) + sep + pb(b))
}
final class MutableMap[A] {
private val lock = new AnyRef
private var m = Map.empty[String, A]
def +=(a: (Url.Relative, A)) =
addAll(a :: Nil)
def add(url: Url.Relative, a: A): this.type =
addAll((url, a) :: Nil)
@inline def ++=(as: IterableOnce[(Url.Relative, A)]) =
addAll(as)
def addAll(as: IterableOnce[(Url.Relative, A)]): this.type =
lock.synchronized {
for ((u, a) <- as.iterator) {
val k = u.relativeUrlNoHeadSlash
m.get(k) match {
case None => m = m.updated(k, a)
case Some(v) => throw new IllegalStateException(s"Duplicate values at Url.Relative(${u.relativeUrl}): $v & $a")
}
}
this
}
def toMapNoHeadSlash: Map[String, A] =
lock.synchronized(m)
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
final case class Absolute(absoluteUrl: String) extends AnyVal {
def base: Absolute.Base =
Absolute.Base(absoluteUrl.dropRight(relativeUrl.relativeUrl.length - 1))
def relativeUrl: Relative =
Relative(absoluteUrl.replaceFirst("^.*?//.+?(?:/|$)", ""))
def /(r: Relative): Absolute =
base / r
}
object Absolute {
/** An absolute URL until (and excluding) the path.
*
* @param value Never ends with a slash.
* Eg. "http://qwe.com:123"
*/
final case class Base private[Base](value: String) extends AnyVal {
def /(r: Relative): Absolute =
Absolute(if (r.isRoot) value else value + r.relativeUrl)
def /[A](r: Relative.Param1[A]): Absolute.Param1[A] =
Absolute.Param1(this / r.prefix, r.suffix)
def forWebSocket: Base =
if (value.matches("^https?:.*"))
new Base("ws" + value.drop(4))
else
this
}
object Base {
def apply(value: String): Base =
new Base(dropTailSlashes(value))
implicit def univEq: UnivEq[Base] = UnivEq.derive
}
/** Represents `https://blah.com/prefix/`; the param is always last */
final case class Param1[-A](prefix: Absolute, suffix: A => String) {
private val pre = prefix.absoluteUrl + "/"
def apply(a: A): Absolute =
Absolute(pre + suffix(a))
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/** Super-efficient version of _.dropWhile(_ == '/') */
val dropHeadSlashes: String => String = s => {
var i = 0
while (i < s.length && s(i) == '/') i += 1
if (i == 0) s else s.substring(i)
}
/** Super-efficient version of _.dropRightWhile(_ == '/') */
val dropTailSlashes: String => String = s => {
val j = s.length - 1
var i = j
while (i >= 0 && s(i) == '/') i -= 1
if (i == j) s else s.substring(0, j)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy