All Downloads are FREE. Search and download functionalities are using the official Maven repository.

blobstore.url.Authority.scala Maven / Gradle / Ivy

There is a newer version: 0.9.15
Show newest version
package blobstore.url

import blobstore.url.exception.{AuthorityParseError, MultipleUrlValidationException}
import cats.{ApplicativeThrow, Order, Show}
import cats.data.{NonEmptyChain, ValidatedNec}
import cats.data.Validated.{Invalid, Valid}
import cats.kernel.Eq
import cats.syntax.all.*

/** An authority as defined by RFC3986. Can point to any valid host on a computer network. Characterized by supporting
  * userinfo, port as well as IP addresses in addition to normal hostnames.
  *
  * @param host
  *   A valid host. This is either a domain name, or an IPv4 or IPv6 address
  * @param userInfo
  *   Optional userinfo component holding username and optionally password
  * @param port
  *   Optional port component
  * @see
  *   https://www.ietf.org/rfc/rfc3986.txt chapter 3.2 Authority
  */
case class Authority(host: Host, userInfo: Option[UserInfo] = None, port: Option[Port] = None) {

  override def toString: String = super.toString

  def toStringWithPassword: String = {
    val p = port.fold("")(p => show":$p")
    val u = userInfo.fold("")(u => show"${u.toStringWithPassword}@")
    show"$u$host$p"
  }
}

object Authority {

  /** input string bob:[email protected]:8080
    *
    * results in the following subexpression matches
    *
    * $1 = bob $2 = vacuum2000 $3 = example.com $4 = 8080
    *
    * Group 3 is mandatory, all other are optional
    */
  private val regex = "^(?:([^:@]+)(?::([^@]+))?@)?([^:/@]+)(?::([0-9]+))?$".r

  def unsafe(candidate: String): Authority = parse(candidate) match {
    case Valid(a)   => a
    case Invalid(e) => throw MultipleUrlValidationException(e) // scalafix:ok
  }

  def localhost: Authority = unsafe("localhost")

  def parseF[F[_]: ApplicativeThrow](host: String): F[Authority] =
    parse(host).toEither.leftMap(MultipleUrlValidationException.apply).liftTo[F]

  def parse(candidate: String): ValidatedNec[AuthorityParseError, Authority] =
    regex.findFirstMatchIn(candidate).toRight(
      NonEmptyChain(AuthorityParseError.InvalidFormat(candidate, regex.pattern.pattern()))
    ).flatMap { m =>
      val password = Option(m.group(2))
      val user     = Option(m.group(1)).map(UserInfo(_, password))

      val host = Option(m.group(3)).map { h =>
        Hostname.parse(h)
      }.getOrElse(AuthorityParseError.MissingHostname(candidate).invalidNec)

      val port = Option(m.group(4)).traverse(p => Port.parse(p).toValidatedNec)

      (host, user.validNec, port).mapN(Authority.apply).toEither
    }.toValidated

  implicit val show: Show[Authority]         = s => s.host.show + s.port.map(_.show).map(":" + _).getOrElse("")
  implicit val order: Order[Authority]       = Order.by(_.show)
  implicit val ordering: Ordering[Authority] = order.toOrdering
  implicit val eq: Eq[Authority] = (x, y) => {
    x.host === y.host && x.port === y.port
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy