blobstore.url.Path.scala Maven / Gradle / Ivy
package blobstore.url
import blobstore.url.Path.{AbsolutePath, RootlessPath}
import blobstore.url.general.StorageClassLookup
import cats.Show
import cats.data.Chain
import cats.kernel.Order
import cats.syntax.all.*
import java.nio.file.Paths
import java.time.Instant
import scala.annotation.tailrec
/** The path segment of a URI. It is parameterized on the type representing the path. This can be a plain String, or a
* storage provider specific type.
*
* Examples of storage provider types would be `software.amazon.awssdk.services.s3.internal.resource.S3ObjectResource`
* for S3, com.google.storage.Blob for GCS, etc.
*
* @see
* https://www.ietf.org/rfc/rfc3986.txt chapter 3.3, Path
*/
sealed trait Path[+A] {
def representation: A
def segments: Chain[String]
def value: String = Show[Path[A]].show(this)
def plain: Path.Plain = this match {
case AbsolutePath(_, segments) => AbsolutePath("/" + segments.mkString_("/"), segments)
case RootlessPath(_, segments) => RootlessPath(segments.mkString_("/"), segments)
}
def absolute: AbsolutePath[String] = plain match {
case p @ AbsolutePath(_, _) => p
case RootlessPath(a, segments) => AbsolutePath("/" + a, segments)
}
def relative: RootlessPath[String] = plain match {
case AbsolutePath(a, segments) => RootlessPath(a.stripPrefix("/"), segments)
case p @ RootlessPath(_, _) => p
}
def nioPath: java.nio.file.Path = Paths.get(toString)
/** Compose with string to form a new Path
*
* The underlying representation must be String in order for the representation and the path to be kept in sync. Use
* [[addSegment]] to modify paths backed by non-String types
*
* @see
* addSegment
*/
def /(segment: String): Path[String] = {
val nonEmpty = Chain(segment.stripPrefix("/").split("/").toList*)
val emptyElements = Chain(segment.reverse.takeWhile(_ == '/').map(_ => "").toList*)
val stripSuffix = segments.initLast match {
case Some((init, "")) => init
case _ => segments
}
val newChain = stripSuffix ++ nonEmpty ++ emptyElements
this match {
case AbsolutePath(_, _) =>
AbsolutePath("/" + newChain.mkString_("/"), newChain)
case RootlessPath(_, _) =>
RootlessPath(newChain.mkString_("/"), newChain)
}
}
def /(segment: Option[String]): Path[String] = segment match {
case Some(s) => /(s)
case None => this.plain
}
def `//`(segment: Option[String]): Path[String] = segment match {
case Some(s) => `//`(s)
case None => this.plain
}
/** Ensure that path always is suffixed with '/'
*/
def `//`(segment: String): Path[String] =
/(if (segment.endsWith("/")) segment else segment + "/")
def as[B](b: B): Path[B] = this match {
case AbsolutePath(_, segments) => AbsolutePath(b, segments)
case RootlessPath(_, segments) => RootlessPath(b, segments)
}
/** Adds a segment to the path while ensuring that the segments and path representation are kept in sync
*
* If you're just working with String paths, see `/`
*/
def addSegment[B](segment: String, representation: B): Path[B] =
(plain / segment).as(representation)
override def toString: String = Show[Path[A]].show(this)
// scalafix:off
override def equals(obj: Any): Boolean =
obj.isInstanceOf[Path[?]] && obj.asInstanceOf[Path[?]].plain.eqv(plain)
// scalafix:on
override def hashCode(): Int = representation.hashCode()
def isEmpty: Boolean = segments.isEmpty
def lastSegment: Option[String] =
if (isEmpty) None
else {
val slashSuffix = segments.reverse.takeWhile(_.isEmpty).map(_ => "/").toList.mkString
val lastNonEmpty = segments.reverse.dropWhile(_.isEmpty).headOption
lastNonEmpty.map(_ + slashSuffix)
}
/** Goes one level "up" and looses any information about the underlying path representation
*/
def up: Path.Plain = {
@tailrec
def dropLastSegment(segments: Chain[String], levels: Int): Chain[String] = segments.initLast match {
case Some((init, last)) => if (last.isEmpty && levels === 0) dropLastSegment(init, levels + 1) else init
case None => segments
}
val upSegments = dropLastSegment(segments, 0)
val representation = upSegments.mkString_("/")
plain match {
case AbsolutePath(_, _) => AbsolutePath(representation, upSegments)
case RootlessPath(_, _) => RootlessPath(representation, upSegments)
}
}
def parentPath: Path.Plain = up
def stripSlashSuffix: Path[A] = {
val newSegments = segments.initLast match {
case Some((init, "")) => init
case _ => segments
}
this match {
case AbsolutePath(representation, _) => AbsolutePath(representation, newSegments)
case RootlessPath(representation, _) => RootlessPath(representation, newSegments)
}
}
def fullName(implicit ev: A <:< FsObject): String = ev(representation).name
def size(implicit ev: A <:< FsObject): Option[Long] = ev(representation).size
def isDir(implicit ev: A <:< FsObject): Boolean = ev(representation).isDir
def lastModified(implicit ev: A <:< FsObject): Option[Instant] = ev(representation).lastModified
def storageClass[SC](implicit storageClassLookup: StorageClassLookup.Aux[A, SC]): Option[SC] =
storageClassLookup.storageClass(representation)
def fileName(implicit ev: A <:< FsObject): Option[String] = if (isDir) None else lastSegment
def dirName(implicit ev: A <:< FsObject): Option[String] = if (!isDir) None else lastSegment
}
object Path {
/** A plain path represented with a String
*
* See .apply for creating these
*/
type Plain = Path[String]
type AbsolutePlain = AbsolutePath[String]
type RootlessPlain = RootlessPath[String]
case class AbsolutePath[A] private[Path] (representation: A, segments: Chain[String]) extends Path[A]
object AbsolutePath {
def createFrom(s: String): AbsolutePath[String] = {
if (s.isEmpty || s === "/") AbsolutePath("/", Chain.empty)
else {
val nonEmpty = s.stripPrefix("/").split("/").toList
val empty = s.reverse.takeWhile(_ == '/').map(_ => "").toList
val concat = nonEmpty ++ empty
val chain = Chain(concat*)
AbsolutePath("/" + chain.mkString_("/"), chain)
}
}
def parse(s: String): Option[AbsolutePath[String]] = {
if (s === "/") AbsolutePath("/", Chain.empty).some
else if (s.startsWith("/")) {
val nonEmpty = s.stripPrefix("/").split("/").toList
val empty = s.reverse.takeWhile(_ == '/').map(_ => "").toList
val concat = nonEmpty ++ empty
Some(AbsolutePath(s, Chain(concat*)))
} else None
}
val root: AbsolutePath[String] = AbsolutePath("/", Chain.empty)
implicit def show[A]: Show[AbsolutePath[A]] = "/" + _.segments.mkString_("/")
implicit def order[A: Order]: Order[AbsolutePath[A]] =
(x, y) => Order[A].compare(x.representation, y.representation)
}
case class RootlessPath[A] private[Path] (representation: A, segments: Chain[String]) extends Path[A]
object RootlessPath {
def parse(s: String): Option[RootlessPath[String]] =
if (!s.startsWith("/")) {
val nonEmpty = s.split("/").toList
val empty = s.reverse.takeWhile(_ == '/').map(_ => "").toList
val concat = nonEmpty ++ empty
Some(RootlessPath(s, Chain(concat*)))
} else None
@tailrec
def createFrom(s: String): RootlessPath[String] = {
if (s.startsWith("/")) {
createFrom(s.stripPrefix("/"))
} else {
val nonEmpty = s.split("/").toList
val empty = s.reverse.takeWhile(_ == '/').map(_ => "").toList
val concat = nonEmpty ++ empty
RootlessPath(s, Chain(concat*))
}
}
def root: RootlessPath[String] = RootlessPath("", Chain.empty)
implicit def show[A]: Show[RootlessPath[A]] = _.segments.mkString_("/")
implicit def order[A: Order]: Order[RootlessPath[A]] =
(x, y) => Order[A].compare(x.representation, y.representation)
implicit def ordering[A: Order]: Ordering[RootlessPath[A]] = order[A].toOrdering
}
def empty: RootlessPath[String] = RootlessPath("", Chain.empty)
def createAbsolute(path: String): AbsolutePath[String] = {
val noPrefix = path.stripPrefix("/")
AbsolutePath("/" + noPrefix, Chain(noPrefix.split("/").toList*))
}
def createRootless(path: String): RootlessPath[String] = {
val noPrefix = path.stripPrefix("/")
RootlessPath(noPrefix, Chain(noPrefix.split("/").toList*))
}
def absolute(path: String): Option[AbsolutePath[String]] = AbsolutePath.parse(path)
def rootless(path: String): Option[RootlessPath[String]] = RootlessPath.parse(path)
def plain(path: String): Path.Plain = apply(path)
def apply(s: String): Path.Plain = {
AbsolutePath.parse(s).widen[Path.Plain].orElse(RootlessPath.parse(s)).getOrElse(
RootlessPath(
s,
Chain(s.split("/").toList*)
) // Paths either have a root or they don't, this block is never executed
)
}
def of[B](path: String, b: B): Path[B] = Path(path).as(b)
def cmp[A](one: Path[A], two: Path[A]): Int = {
one.show.compare(two.show)
}
def unapply[A](p: Path[A]): Option[(String, A, Chain[String])] = p match {
case AbsolutePath(representation, segments) => (p.show, representation, segments).some
case RootlessPath(representation, segments) => (p.show, representation, segments).some
}
implicit def order[A]: Order[Path[A]] = (x: Path[A], y: Path[A]) => cmp(x, y)
implicit def ordering[A]: Ordering[Path[A]] = order[A].toOrdering
implicit def show[A]: Show[Path[A]] = Show.show {
case a: AbsolutePath[?] => a.show
case r: RootlessPath[?] => r.show
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy