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

quasar.physical.mongodb.collection.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014–2017 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 quasar.physical.mongodb

import slamdata.Predef._
import quasar.{RenderTree, Terminal}
import quasar.contrib.pathy._
import quasar.fp.ski._
import quasar.fs._

import scala.AnyVal
import scala.util.parsing.combinator._

import com.mongodb.MongoNamespace
import scalaz._, Scalaz._
import pathy.Path.{dir => pDir, file => pFile, _}


// TODO: use Refined to constrain the value here
final case class DatabaseName(value: String) extends AnyVal {
  def bson: Bson = Bson.Text(value)
}
object DatabaseName {
  implicit def equal: Equal[DatabaseName] = Equal.equalA
}

// TODO: use Refined to constrain the value here
final case class CollectionName(value: String) extends AnyVal {
  def isDescendantOf(ancestor: CollectionName): Boolean =
    if (ancestor.value == "") true
    else value startsWith (ancestor.value + ".")

  def bson: Bson = Bson.Text(value)
}
object CollectionName {
  implicit def equal: Equal[CollectionName] = Equal.equalA
}

/** Identifies a collection in a specific database. Sometimes referred to as a
  * "namespace" in MongoDB docs.
  */
final case class Collection(database: DatabaseName, collection: CollectionName) {
  import Collection._

  /** Convert this collection to a file. */
  def asFile: AFile = {
    val db   = DatabaseNameUnparser(database)
    val segs = CollectionNameUnparser(collection).reverse
    val f    = segs.headOption getOrElse db

    (segs ::: List(db)).drop(1).foldRight(rootDir)((d, p) => p  pDir(d))  pFile(f)
  }

  def asNamespace: MongoNamespace = new MongoNamespace(database.value, collection.value)
}

object Collection {

  /** The collection represented by the given file. */
  def fromFile(file: AFile): PathError \/ Collection =
    fromPath(file)

  /** The name of a collection represented by the given directory. */
  def prefixFromDir(dir: ADir): PathError \/ CollectionName =
    fromPath(dir) map (_.collection)

  /** Returns the database name determined by the given path. */
  def dbNameFromPath(path: APath): PathError \/ DatabaseName =
    dbNameAndRest(path) bimap (PathError.invalidPath(path, _), _._1)

  /** Returns the directory name derived from the given database name. */
  def dirNameFromDbName(dbName: DatabaseName): DirName =
    DirName(DatabaseNameUnparser(dbName))

  private def fromPath(path: APath): PathError \/ Collection = {
    import PathError._

    val collResult = for {
      tpl  <- dbNameAndRest(path)
      (db, r) = tpl
      ss   <- r.toNel.toRightDisjunction("path names a database, but no collection")
      segs <- ss.traverse(CollectionSegmentParser(_))
      coll =  CollectionName(segs.toList mkString ".")
      len  =  utf8length(db.value) + 1 + utf8length(coll.value)
      _    <- if (len > 120)
                s"database+collection name too long ($len > 120 bytes): $db.$coll".left
              else ().right
    } yield Collection(db, coll)

    collResult leftMap (invalidPath(path, _))
  }

  private def dbNameAndRest(path: APath): String \/ (DatabaseName, IList[String]) =
    flatten(None, None, None, Some(_), Some(_), path)
      .toIList.unite.uncons(
        "no database specified".left,
        (h, t) => DatabaseNameParser(h) strengthR t)

  private trait PathParser extends RegexParsers {
    override def skipWhitespace = false

    protected def substitute(pairs: List[(String, String)]): Parser[String] =
      pairs.foldLeft[Parser[String]](failure("no match")) {
        case (acc, (a, b)) => (a ^^ κ(b)) | acc
      }
  }

  def utf8length(str: String) = str.getBytes("UTF-8").length

  val DatabaseNameEscapes = List(
    " "  -> "+",
    "."  -> "~",
    "%"  -> "%%",
    "+"  -> "%add",
    "~"  -> "%tilde",
    "/"  -> "%div",
    "\\" -> "%esc",
    "\"" -> "%quot",
    "*"  -> "%mul",
    "<"  -> "%lt",
    ">"  -> "%gt",
    ":"  -> "%colon",
    "|"  -> "%bar",
    "?"  -> "%qmark")

  private object DatabaseNameParser extends PathParser {
    def name: Parser[DatabaseName] =
      char.* ^^ { cs => DatabaseName(cs.mkString) }

    def char: Parser[String] = substitute(DatabaseNameEscapes) | "(?s).".r

    def apply(input: String): String \/ DatabaseName = parseAll(name, input) match {
      case Success(name, _) if utf8length(name.value) > 64 =>
        s"database name too long (> 64 bytes): ${name.value}".left
      case Success(name, _) =>
        name.right
      case failure : NoSuccess =>
        s"failed to parse ‘$input’: ${failure.msg}".left
    }
  }

  private object DatabaseNameUnparser extends PathParser {
    def name = nameChar.* ^^ { _.mkString }

    def nameChar = substitute(DatabaseNameEscapes.map(_.swap)) | "(?s).".r

    def apply(input: DatabaseName): String = parseAll(name, input.value) match {
      case Success(result, _) => result
      case failure : NoSuccess => scala.sys.error("doesn't happen")
    }
  }

  val CollectionNameEscapes = List(
    "."  -> "\\.",
    "$"  -> "\\d",
    "\\" -> "\\\\")

  private object CollectionSegmentParser extends PathParser {
    def seg: Parser[String] =
      char.* ^^ { _.mkString }

    def char: Parser[String] = substitute(CollectionNameEscapes) | "(?s).".r

    /**
      * @return If implemented correctly, should always return a [[String]] in the right hand of the [[Disjunction]]
      */
    def apply(input: String): String \/ String = parseAll(seg, input) match {
      case Success(seg, _) =>
        seg.right
      case failure : NoSuccess =>
        s"failed to parse ‘$input’: ${failure.msg}".left
    }
  }

  private object CollectionNameUnparser extends PathParser {
    def name = repsep(seg, ".")

    def seg = segChar.* ^^ { _.mkString }

    def segChar = substitute(CollectionNameEscapes.map(_.swap)) | "(?s)[^.]".r

    def apply(input: CollectionName): List[String] = parseAll(name, input.value) match {
      case Success(result, _) => result
      case failure : NoSuccess => scala.sys.error("doesn't happen")
    }
  }

  implicit val order: Order[Collection] =
    Order.orderBy(c => (c.database.value, c.collection.value))

  implicit val renderTree: RenderTree[Collection] =
    new RenderTree[Collection] {
      def render(v: Collection) =
        Terminal(List("Collection"), Some(v.database.value + "; " + v.collection.value))
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy