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

ai.mantik.elements.MantikId.scala Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of the Mantik Project.
 * Copyright (c) 2020-2021 Mantik UG (Haftungsbeschränkt)
 * Authors: See AUTHORS file
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.
 *
 * Additionally, the following linking exception is granted:
 *
 * If you modify this Program, or any covered work, by linking or
 * combining it with other code, such other code is not for that reason
 * alone subject to any of the requirements of the GNU Affero GPL
 * version 3.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license.
 */
package ai.mantik.elements

import java.security.SecureRandom
import java.util.Base64

import ai.mantik.elements.errors.InvalidMantikIdException
import io.circe.Decoder.Result
import io.circe.{Decoder, DecodingFailure, Encoder, HCursor, Json}
import io.circe.syntax._

import scala.util.matching.Regex
import scala.language.implicitConversions

/**
  * Identifies a MantikArtifact. Can either be an (anonymous) ItemId or a
  * [[NamedMantikId]]
  */
sealed trait MantikId {

  /** Converts to String representation. */
  def toString: String
}

object MantikId {
  implicit val encoder: Encoder[MantikId] = new Encoder[MantikId] {
    override def apply(a: MantikId): Json = {
      Json.fromString(a.toString)
    }
  }

  implicit val decoder: Decoder[MantikId] = new Decoder[MantikId] {
    override def apply(c: HCursor): Result[MantikId] = {
      c.value.asString match {
        case None => Left(DecodingFailure("Expected strng", c.history))
        case Some(s) =>
          decodeString(s).left.map(_.wrapInDecodingFailure)
      }
    }
  }

  /** Automatic conversion from strings. */
  implicit def fromString(s: String): MantikId = decodeString(s).fold(e => throw e, identity)

  /** Decode a String into a MantikId. */
  def decodeString(s: String): Either[InvalidMantikIdException, MantikId] = {
    if (s.startsWith(ItemId.ItemIdPrefix)) {
      Right(ItemId.fromString(s))
    } else {
      NamedMantikId.decodeString(s)
    }
  }
}

/**
  * Provides a stable identifier for Mantik Items.
  *
  * In contrast to [[NamedMantikId]] this id always responds to the very same
  * item. In practice, all [[NamedMantikId]] link to a [[ItemId]] which link
  * to the item content.
  *
  * They can not be overwritten but deleted.
  *
  * In the moment they are random, but in future they shall represent hash values
  * of the items.
  */
final class ItemId private (
    private val id: String
) extends MantikId {
  override def toString: String = ItemId.ItemIdPrefix + id

  override def hashCode(): Int = id.hashCode

  override def equals(that: Any): Boolean = {
    that match {
      case that: ItemId => id == that.id
      case _            => false
    }
  }
}

object ItemId {
  val ByteCount = 32

  /** Prefix for string serialization of ItemIds. */
  val ItemIdPrefix = "@"

  private val generator = new SecureRandom()

  def generate(): ItemId = {
    val value = new Array[Byte](ByteCount)
    generator.nextBytes(value)
    new ItemId(encodeBinary(value))
  }

  def apply(s: String): ItemId = {
    fromString(s)
  }

  def fromString(s: String): ItemId = {
    require(s.startsWith(ItemIdPrefix), s"String encoding must start with ${ItemIdPrefix}")
    new ItemId(s.stripPrefix(ItemIdPrefix))
  }

  private def encodeBinary(array: Array[Byte]): String = {
    // Use URL Encoding, we do not want "/"
    Base64.getUrlEncoder.withoutPadding().encodeToString(array)
  }

  implicit val encoder: Encoder[ItemId] = new Encoder[ItemId] {
    override def apply(a: ItemId): Json = Json.fromString(a.toString)
  }

  implicit val decoder: Decoder[ItemId] = new Decoder[ItemId] {
    override def apply(c: HCursor): Result[ItemId] = c.value.asString match {
      case None    => Left(DecodingFailure("Expected string", c.history))
      case Some(s) => Right(fromString(s))
    }
  }
}

/**
  * A Named Mantik Artifact.
  *
  * @param account the users account, defaults to library.
  * @param name of the Mantik artifact. If it starts with @ it refers to a [[ItemId]].
  * @param version the version, defaults to latest.
  */
final case class NamedMantikId(
    // Note: ordering is this way, to force the user to use
    // named arguments to set name explicitly.
    // There is also an implicit conversion from string
    // which parses all elements and an apply function for conversion from string.
    account: String = NamedMantikId.DefaultAccount,
    name: String,
    version: String = NamedMantikId.DefaultVersion
) extends MantikId {

  override def toString: String = {
    val builder = new StringBuilder()
    if (account != NamedMantikId.DefaultAccount) {
      builder ++= account
      builder += '/'
    }
    builder ++= name
    if (version != NamedMantikId.DefaultVersion) {
      builder += ':'
      builder ++= version
    }
    return builder.result()
  }

  /** Returns Naming violatins. */
  def violations: Seq[String] =
    NamedMantikId.accountViolations(account) ++ NamedMantikId.nameViolations(name) ++ NamedMantikId.versionViolations(
      version
    )
}

object NamedMantikId {

  /** If no version is given, this version is accessed. */
  val DefaultVersion = "latest"

  /** The library account is the default and omitted on serializing. */
  val DefaultAccount = "library"

  /** Automatic conversion from strings. */
  @throws[InvalidMantikIdException]("On invalid Mantik Ids")
  implicit def fromString(s: String): NamedMantikId = {
    decodeString(s) match {
      case Left(e)  => throw e
      case Right(v) => v
    }
  }

  @throws[InvalidMantikIdException]("On invalid Mantik Ids")
  def apply(s: String): NamedMantikId = fromString(s)

  /** JSON Encoding to String. */
  implicit val encoder: Encoder[NamedMantikId] = new Encoder[NamedMantikId] {
    override def apply(a: NamedMantikId): Json = Json.fromString(a.toString)
  }

  /** JSON Encoding from String. */
  implicit val decoder: Decoder[NamedMantikId] = new Decoder[NamedMantikId] {
    override def apply(c: HCursor): Result[NamedMantikId] = {
      c.value.asString match {
        case None => Left(DecodingFailure("Expected string", c.history))
        case Some(s) =>
          decodeString(s).left.map(_.wrapInDecodingFailure)
      }
    }
  }

  /** Decodes NamedMantikId with error handling. */
  def decodeStringResult(s: String): Result[NamedMantikId] = {
    s.split(":").toList match {
      case List(accountName, version) =>
        decodeAccountName(accountName).map { case (account, name) =>
          NamedMantikId(account = account, name = name, version = version)
        }
      case List(accountName) =>
        decodeAccountName(accountName).map { case (account, name) =>
          NamedMantikId(
            name = name,
            account = account,
            version = NamedMantikId.DefaultVersion
          )
        }
      case _ =>
        Left(DecodingFailure(s"${s} is not a valid Mantik id", Nil))
    }
  }

  /** Like decodeStringResult but encodes errors in InvalidMantikIdException. */
  def decodeString(s: String): Either[InvalidMantikIdException, NamedMantikId] = {
    decodeStringResult(s).left.map(InvalidMantikIdException.fromDecodingFailure)
  }

  private def decodeAccountName(s: String): Result[(String, String)] = {
    s.split("/").toList match {
      case List(account, name) => Right((account, name))
      case List(name)          => Right((DefaultAccount, name))
      case _ =>
        Left(DecodingFailure(s"${s} is not a valid account / name combination", Nil))
    }
  }

  /** Encodes a mantik id within a string. */
  implicit val mantikIdCodec: Encoder[NamedMantikId] with Decoder[NamedMantikId] = new Encoder[NamedMantikId]
    with Decoder[NamedMantikId] {
    override def apply(a: NamedMantikId): Json = {
      Json.fromString(a.toString)
    }

    override def apply(c: HCursor): Result[NamedMantikId] = {
      for {
        s <- c.as[String]
        r <- decodeStringResult(s)
      } yield r
    }
  }

  /** Validates the account, returns violations. */
  def accountViolations(account: String): Seq[String] = {
    val violations = Seq.newBuilder[String]
    if (!AccountRegex.pattern.matcher(account).matches()) {
      violations += "Invalid Account"
    }
    violations.result()
  }

  /** Validates the name, returns violations. */
  def nameViolations(name: String): Seq[String] = {
    val violations = Seq.newBuilder[String]
    if (!NameRegex.pattern.matcher(name).matches()) {
      violations += "Invalid Name"
    }
    violations.result()
  }

  /** Validates the version, returns violations. */
  def versionViolations(version: String): Seq[String] = {
    val violations = Seq.newBuilder[String]
    if (!VersionRegex.pattern.matcher(version).matches()) {
      violations += "Invalid Version"
    }
    violations.result()
  }

  /**
    * Regex for a Name.
    * Note: in contrast to account names, also "_" and "." in the middle is allowed
    */
  val NameRegex: Regex = "^[a-z\\d](?:[a-z\\d_\\.]|-(?=[a-z\\d])){0,50}$".r

  /** Regex for account. */
  val AccountRegex: Regex = NameRegex

  /**
    * Regex for a Version.
    * Note: in contrast to account names, also "_" and "." in the middle is allowed
    */
  val VersionRegex: Regex = "^[a-z\\d]([a-z\\d_\\.\\-]*[a-z\\d])?$".r
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy