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

pathy.Path.scala Maven / Gradle / Ivy

There is a newer version: 0.2.13
Show newest version
/*
 * Copyright 2014 - 2015 SlamData Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pathy

import scala.annotation.tailrec
import scalaz._, Scalaz._

sealed trait Path[+B,+T,+S] {
  def isAbsolute: Boolean
  def isRelative = !isAbsolute
}

object Path {
  sealed trait Rel
  sealed trait Abs

  sealed trait File
  sealed trait Dir

  sealed trait Sandboxed
  sealed trait Unsandboxed

  final case class FileName(value: String) extends AnyVal {
    def extension: String = {
      val idx = value.lastIndexOf(".")
      if (idx == -1) "" else value.substring(idx + 1)
    }

    def dropExtension: FileName = {
      val idx = value.lastIndexOf(".")
      if (idx == -1) this else FileName(value.substring(0, idx))
    }

    def changeExtension(f: String => String): FileName =
      FileName(dropExtension.value + "." + f(extension))
  }

  final case class DirName(value: String) extends AnyVal

  // Note: this ADT allows invalid paths, but the exposed functions
  // of the package do not.
  private case object Current extends Path[Nothing,Nothing,Nothing] {
    def isAbsolute = false
  }
  private case object Root extends Path[Nothing,Nothing,Nothing] {
    def isAbsolute = true
  }
  private final case class ParentIn[B,T,S](parent: Path[B,T,S]) extends Path[B,T,S] {
    def isAbsolute = parent.isAbsolute
  }
  private final case class DirIn[B,T,S](parent: Path[B,T,S], name: DirName) extends Path[B,T,S] {
    def isAbsolute = parent.isAbsolute
  }
  private final case class FileIn[B,T,S](parent: Path[B,T,S], name: FileName) extends Path[B,T,S] {
    def isAbsolute = parent.isAbsolute
  }

  type RelFile[S] = Path[Rel, File, S]
  type AbsFile[S] = Path[Abs, File, S]
  type RelDir[S] = Path[Rel, Dir, S]
  type AbsDir[S] = Path[Abs, Dir, S]

  def currentDir[S]: Path[Rel, Dir, S] = Current

  def rootDir[S]: Path[Abs, Dir, S] = Root

  def file[S](name: String): Path[Rel, File, S] = file1(FileName(name))

  def file1[S](name: FileName): Path[Rel, File, S] = FileIn(Current, name)

  def fileName[B,S](path: Path[B, File, S]): FileName = path match {
    case FileIn(_, name) => name
    case _               => sys.error("impossible!")
  }

  def dir[S](name: String): Path[Rel, Dir, S] = dir1(DirName(name))

  def dir1[S](name: DirName): Path[Rel, Dir, S] = DirIn(Current, name)

  def dirName[B,S](path: Path[B, Dir, S]): Option[DirName] = path match {
    case DirIn(_, name) => Some(name)
    case _              => None
  }

  implicit class PathOps[B,T,S](path: Path[B,T,S]) {
    def relativeTo[SS](dir: Path[B, Dir, SS]): Option[Path[Rel, T, SS]] = {
      def go[TT](p1: Path[B,TT,S], p2: Path[B,Dir,SS]): Option[Path[Rel,TT,SS]] =
        if (identicalPath(p1, p2)) Some(Current)
        else peel(p1) match {
          case None => (p1, p2) match {
            case (Root, Root)       => Some(Current)
            case (Current, Current) => Some(Current)
            case _                  => None
          }
          case Some((p1p, v)) =>
            go(p1p, p2).map(p => p  v.fold(DirIn(Current, _), FileIn(Current, _)))
          }
      go(canonicalize(path), canonicalize(dir))
    }
  }

  implicit class DirOps[B,S](dir: Path[B, Dir, S]) {
    def [T](rel: Path[Rel, T, S]): Path[B, T, S] =
      (dir, rel) match {
        case (Current,        Current) => Current
        case (Root,           Current) => Root
        case (ParentIn(p1),   Current) => ParentIn(p1  Current)
        case (FileIn(p1, f1), Current) => FileIn(p1  Current, f1)
        case (DirIn(p1, d1),  Current) => DirIn(p1  Current, d1)

        // these don't make sense, but cannot exist anyway
        case (Current,        Root) => Current
        case (Root,           Root) => Root
        case (ParentIn(p1),   Root) => ParentIn(p1  Current)
        case (FileIn(p1, f1), Root) => FileIn(p1  Current, f1)
        case (DirIn(p1, d1),  Root) => DirIn(p1  Current, d1)

        case (p1, ParentIn(p2))   => ParentIn(p1  p2)
        case (p1, FileIn(p2, f2)) => FileIn(p1  p2, f2)
        case (p1, DirIn(p2, d2))  => DirIn(p1  p2, d2)
      }

    // NB: scala doesn't cotton to `<..>`
    def <::>[T](rel: Path[Rel, T, S]): Path[B, T, Unsandboxed] =
      parentDir1(dir)  unsandbox(rel)
  }

  implicit class FileOps[B,S](file: Path[B, File, S]) {
    // NB: scala doesn't cotton to `<.>`
    def <:>(ext: String): Path[B, File, S] =
      renameFile(file, name => name.changeExtension(_ => ext))
  }

  def refineType[B,T,S](path: Path[B,T,S]): Path[B,Dir,S] \/ Path[B,File,S] = path match {
    case Current      => Current.left
    case Root         => Root.left
    case ParentIn(p)  => ParentIn(unsafeCoerceType(p)).left
    case FileIn(p, f) => FileIn(unsafeCoerceType(p), f).right
    case DirIn(p, d)  => DirIn(unsafeCoerceType(p), d).left
  }

  def maybeDir[B,T,S](path: Path[B,T,S]): Option[Path[B, Dir, S]] =
    refineType(path).swap.toOption

  def maybeFile[B,T,S](path: Path[B,T,S]): Option[Path[B, File, S]] =
    refineType(path).toOption

  def peel[B,T,S](path: Path[B,T,S]): Option[(Path[B, Dir, S], DirName \/ FileName)] = path match {
    case Current         => None
    case Root            => None
    case p @ ParentIn(_) =>
      val (c, p1) = canonicalize1(p)
      if (c) peel(p1) else None
    case DirIn(p, d)     => Some(unsafeCoerceType(p) -> -\/ (d))
    case FileIn(p, f)    => Some(unsafeCoerceType(p) ->  \/-(f))
  }

  def depth[B,T,S](path: Path[B,T,S]): Int = path match {
    case Current      => 0
    case Root         => 0
    case ParentIn(p)  => depth(p) - 1
    case FileIn(p, _) => depth(p) + 1
    case DirIn(p, _)  => depth(p) + 1
  }

  def parentDir[B,T,S](path: Path[B,T,S]): Option[Path[B,Dir,S]] =
    peel(path).map(_._1)

  def fileParent[B,S](file: Path[B, File, S]): Path[B, Dir, S] = file match {
    case FileIn(p, _) => unsafeCoerceType(p)
    case _            => sys.error("impossible!")
  }

  def unsandbox[B,T,S](path: Path[B,T,S]): Path[B, T, Unsandboxed] = path match {
    case Current      => Current
    case Root         => Root
    case ParentIn(p)  => ParentIn(unsandbox(p))
    case DirIn(p, d)  => DirIn(unsandbox(p), d)
    case FileIn(p, f) => FileIn(unsandbox(p), f)
  }

  /** Synonym for relativeTo, constrained to sandboxed dirs, and with a more evocative name. */
  def sandbox[B,T,S](dir: Path[B, Dir, Sandboxed], path: Path[B,T,S]): Option[Path[Rel,T,Sandboxed]] =
    path relativeTo dir

  def parentDir1[B,T,S](path: Path[B,T,S]): Path[B, Dir, Unsandboxed] =
    ParentIn(unsafeCoerceType(unsandbox(path)))

  private def unsafeCoerceType[B,T,TT,S](path: Path[B,T,S]): Path[B,TT,S] = path match {
    case Current      => Current
    case Root         => Root
    case ParentIn(p)  => ParentIn(unsafeCoerceType(p))
    case DirIn(p, d)  => DirIn(unsafeCoerceType(p), d)
    case FileIn(p, f) => FileIn(unsafeCoerceType(p), f)
  }

  def renameFile[B,S](path: Path[B, File, S], f: FileName => FileName): Path[B, File, S] =
    path match {
      case FileIn(p, f0) => FileIn(p, f(f0))
      case p             => p
    }

  def renameDir[B,S](path: Path[B, Dir, S], f: DirName => DirName): Path[B, Dir, S] =
    path match {
      case DirIn(p, d) => DirIn(p, f(d))
      case p           => p
    }

  def canonicalize[B,T,S](path: Path[B,T,S]): Path[B,T,S] =
    canonicalize1(path)._2

  private def canonicalize1[B,T,S](path: Path[B,T,S]): (Boolean, Path[B,T,S]) =
    path match {
      case Current                => false -> Current
      case Root                   => false -> Root
      case ParentIn(FileIn(p, f)) => true -> canonicalize1(p)._2
      case ParentIn(DirIn(p, f))  => true -> canonicalize1(p)._2
      case ParentIn(p)            =>
        val (ch, p1) = canonicalize1(p)
        val p2 = ParentIn(p1)
        if (ch) canonicalize1(p2) else ch -> p2  // ???
      case FileIn(p, f)           =>
        val (ch, p1) = canonicalize1(p)
        ch -> FileIn(p1, f)
      case DirIn(p, d)            =>
        val (ch, p1) = canonicalize1(p)
        ch -> DirIn(p1, d)
    }

  def flatten[X](root: => X, currentDir: => X, parentDir: => X, dirName: String => X, fileName: String => X, path: Path[_, _, _]): OneAnd[IList, X] = {
    @tailrec
    def go(xs: OneAnd[IList, X], at: Path[_, _, _]): OneAnd[IList, X] = {
      val tl = xs.head :: xs.tail

      at match {
        case Current      => OneAnd(currentDir, tl)
        case Root         => OneAnd(root, tl)
        case ParentIn(p)  => go(OneAnd(parentDir, tl), p)
        case DirIn(p, d)  => go(OneAnd(dirName(d.value), tl), p)
        case FileIn(p, f) => go(OneAnd(fileName(f.value), tl), p)
      }
    }

    path match {
      case Current      => OneAnd(currentDir, IList.empty)
      case Root         => OneAnd(root, IList.empty)
      case ParentIn(p)  => go(OneAnd(parentDir, IList.empty), p)
      case DirIn(p, d)  => go(OneAnd(dirName(d.value), IList.empty), p)
      case FileIn(p, f) => go(OneAnd(fileName(f.value), IList.empty), p)
    }
  }

  def identicalPath[B,T,S,BB,TT,SS](p1: Path[B,T,S], p2: Path[BB,TT,SS]): Boolean =
    p1.shows == p2.shows

  val posixCodec = PathCodec placeholder '/'

  val windowsCodec = PathCodec placeholder '\\'

  final case class PathCodec(separator: Char, escape: String => String, unescape: String => String) {

    def unsafePrintPath(path: Path[_, _, _]): String = {
      val s = flatten("", ".", "..", escape, escape, path)
                .intercalate(separator.toString)

      maybeDir(path) ? (s + separator) | s
    }

    def printPath[B, T](path: Path[B, T, Sandboxed]): String =
      unsafePrintPath(path)

    def parsePath[Z](
      rf: RelFile[Unsandboxed] => Z,
      af: AbsFile[Unsandboxed] => Z,
      rd: RelDir[Unsandboxed] => Z,
      ad: AbsDir[Unsandboxed] => Z)(str: String): Z =
    {
      val segs = str.split(separator)
      val isAbs = str.startsWith(separator.toString)
      val isDir = List(separator.toString, s"$separator.", s"$separator..").exists(str.endsWith) || str === "." || str === ".."

      def folder[B,S](base: Path[B,Dir,S], segments: String): Path[B,Dir,S] = segments match {
        case ""    => base
        case "."   => base
        case ".."  => ParentIn(base)
        case seg   => base  dir(unescape(seg))
      }

      if (str == "")
        rd(Current)
      else if (isAbs && !isDir)
        af(segs.init.foldLeft[AbsDir[Unsandboxed]](rootDir[Unsandboxed])(folder)  file[Unsandboxed](unescape(segs.last)))
      else if (isAbs && isDir)
        ad(segs.foldLeft[AbsDir[Unsandboxed]](rootDir[Unsandboxed])(folder))
      else if (!isAbs && !isDir)
        rf(segs.init.foldLeft[RelDir[Unsandboxed]](Current)(folder)  file[Unsandboxed](unescape(segs.last)))
      else
        rd(segs.foldLeft[RelDir[Unsandboxed]](Current)(folder))
    }

    val parseRelFile: String => Option[RelFile[Unsandboxed]] =
      parsePath[Option[RelFile[Unsandboxed]]](Some(_), _ => None, _ => None, _ => None)

    val parseAbsFile: String => Option[AbsFile[Unsandboxed]] =
      parsePath[Option[AbsFile[Unsandboxed]]](_ => None, Some(_), _ => None, _ => None)

    val parseRelDir: String => Option[RelDir[Unsandboxed]] =
      parsePath[Option[RelDir[Unsandboxed]]](_ => None, _ => None, Some(_), _ => None)

    val parseAbsDir: String => Option[AbsDir[Unsandboxed]] =
      parsePath[Option[AbsDir[Unsandboxed]]](_ => None, _ => None, _ => None, Some(_))

    private def asDir[B,S](path: Path[B, File, S]): Path[B, Dir, S] = path match {
      case FileIn(p, FileName(n)) => DirIn(unsafeCoerceType(p), DirName(n))
      case _ => sys.error("impossible!")
    }

    val parseRelAsDir: String => Option[RelDir[Unsandboxed]] =
      parsePath[Option[RelDir[Unsandboxed]]](p => Some(asDir(p)), _ => None, Some(_), _ => None)

    val parseAbsAsDir: String => Option[AbsDir[Unsandboxed]] =
      parsePath[Option[AbsDir[Unsandboxed]]](_ => None, p => Some(asDir(p)), _ => None, Some(_))

  }

  object PathCodec {

    /**
     * The placeholder codec, replaces literal instances of the separator
     * in segments with a placeholder as well as segments equal to either of the
     * relative dir literals, "." and "..".
     */
    def placeholder(sep: Char): PathCodec = {
      val escapeSep = (_: String).replaceAllLiterally(sep.toString, $sep$)
      val unescapeSep = (_: String).replaceAllLiterally($sep$, sep.toString)

      PathCodec(sep, escapeRel compose escapeSep, unescapeSep compose unescapeRel)
    }

    private val escapeRel = (s: String) =>
      if (s == "..") $dotdot$ else if (s == ".") $dot$ else s

    private val unescapeRel = (s: String) =>
      if (s == $dotdot$) ".." else if (s == $dot$) "." else s

    private val $sep$ = "$sep$"
    private val $dot$ = "$dot$"
    private val $dotdot$ = "$dotdot$"
  }

  implicit def PathShow[B,T,S]: Show[Path[B,T,S]] = new Show[Path[B,T,S]] {
    override def show(v: Path[B,T,S]) = v match {
      case Current                => "currentDir"
      case Root                   => "rootDir"
      case ParentIn(p)            => "parentDir(" + p.show + ")"
      case FileIn(p, FileName(f)) => p.show + "  file(" + f.show + ")"
      case DirIn(p, DirName(d))   => p.show + "  dir(" + d.show + ")"
    }
  }

  implicit def PathEqual[B,T,S]: Equal[Path[B,T,S]] = Equal.equalA
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy