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

sec.metadata.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 Scala EventStoreDB Client
 *
 * 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 sec

import scala.concurrent.duration.*
import cats.{ApplicativeThrow, Endo}
import cats.syntax.all.*
import io.circe.Decoder.Result
import io.circe.*
import io.circe.syntax.*

//======================================================================================================================

final private[sec] case class StreamMetadata(
  state: MetaState,
  custom: Option[JsonObject]
)

private[sec] object StreamMetadata:

  final val empty: StreamMetadata             = StreamMetadata(MetaState.empty)
  def apply(state: MetaState): StreamMetadata = StreamMetadata(state, None)

  extension (sm: StreamMetadata)

    def truncateBefore: Option[StreamPosition.Exact] = sm.state.truncateBefore
    def maxAge: Option[MaxAge]                       = sm.state.maxAge
    def maxCount: Option[MaxCount]                   = sm.state.maxCount
    def cacheControl: Option[CacheControl]           = sm.state.cacheControl
    def acl: Option[StreamAcl]                       = sm.state.acl

    //

    private def modS(fn: MetaState => MetaState): StreamMetadata = sm.copy(state = fn(sm.state))

    def setTruncateBefore(value: Option[StreamPosition.Exact]): StreamMetadata = modS(_.copy(truncateBefore = value))
    def setMaxAge(value: Option[MaxAge]): StreamMetadata                       = modS(_.copy(maxAge = value))
    def setMaxCount(value: Option[MaxCount]): StreamMetadata                   = modS(_.copy(maxCount = value))
    def setCacheControl(value: Option[CacheControl]): StreamMetadata           = modS(_.copy(cacheControl = value))
    def setAcl(value: Option[StreamAcl]): StreamMetadata                       = modS(_.copy(acl = value))
    def setCustom(value: Option[JsonObject]): StreamMetadata                   = sm.copy(custom = value)

    def withTruncateBefore(tb: StreamPosition.Exact): StreamMetadata = sm.setTruncateBefore(tb.some)
    def withMaxAge(value: MaxAge): StreamMetadata                    = sm.setMaxAge(value.some)
    def withMaxCount(value: MaxCount): StreamMetadata                = sm.setMaxCount(value.some)
    def withCacheControl(value: CacheControl): StreamMetadata        = sm.setCacheControl(value.some)
    def withAcl(value: StreamAcl): StreamMetadata                    = sm.setAcl(value.some)
    def withCustom(value: JsonObject): StreamMetadata                = sm.setCustom(value.some)
    def withCustom(value: (String, Json)*): StreamMetadata = sm.withCustom(JsonObject.fromMap(Map.from(value)))

    //

    def getCustom[F[_]: ApplicativeThrow, T: Decoder]: F[Option[T]] =
      sm.custom.traverse(jo => Decoder[T].apply(Json.fromJsonObject(jo).hcursor).liftTo[F])

    def setCustom[T: Encoder.AsObject](custom: T): StreamMetadata =
      sm.withCustom(Encoder.AsObject[T].encodeObject(custom))

    def modifyCustom[F[_]: ApplicativeThrow, T: Codec.AsObject](fn: Endo[Option[T]]): F[StreamMetadata] =
      getCustom[F, T].map(c => sm.setCustom(fn(c).map(Encoder.AsObject[T].encodeObject)))

  //

  import MetaState.metadataKeys

  final val reservedKeys: Set[String] = Set(
    metadataKeys.MaxAge,
    metadataKeys.TruncateBefore,
    metadataKeys.MaxCount,
    metadataKeys.Acl,
    metadataKeys.CacheControl
  )

  given Codec.AsObject[StreamMetadata] = new Codec.AsObject[StreamMetadata]:

    val encodeSS: MetaState => JsonObject         = Encoder.AsObject[MetaState].encodeObject(_)
    val decodeSS: JsonObject => Result[MetaState] = jo => Decoder[MetaState].apply(jo.asJson.hcursor)

    def encodeObject(sm: StreamMetadata): JsonObject = sm.custom match
      case Some(c) if c.nonEmpty => encodeSS(sm.state).toList.foldLeft(c) { case (i, (k, v)) => i.add(k, v) }
      case _                     => encodeSS(sm.state)

    def apply(c: HCursor): Result[StreamMetadata] =
      Decoder.decodeJsonObject(c).map { both =>
        val (ours, theirs) = both.toList.partition(kv => reservedKeys.contains(kv._1))
        (JsonObject.fromIterable(ours), JsonObject.fromIterable(theirs))
      } >>= { case (s, c) =>
        decodeSS(s).map(StreamMetadata(_, Option.when(c.nonEmpty)(c)))
      }

//======================================================================================================================

/** The maximum age of events in the stream. Events older than this will be automatically removed.
  */
sealed abstract case class MaxAge(value: FiniteDuration)
object MaxAge:

  /** The maximum age of events in the stream. Events older than this will be automatically removed.
    *
    * @param maxAge
    *   must be greater than or equal to 1 second.
    */
  def apply(maxAge: FiniteDuration): Either[InvalidInput, MaxAge] =
    if (maxAge < 1.second) InvalidInput(s"maxAge must be >= 1 second, it was $maxAge.").asLeft
    else new MaxAge(maxAge) {}.asRight

  private[sec] def render(ma: MaxAge): String = ma.value.toString()

/** The maximum count of events in the stream. When the stream has more than max count then the oldest will be removed.
  */
sealed abstract case class MaxCount(value: Int)
object MaxCount:

  /** @param maxCount
    *   must be greater than or equal to 1.
    */
  def apply(maxCount: Int): Either[InvalidInput, MaxCount] =
    if (maxCount < 1) InvalidInput(s"max count must be >= 1, it was $maxCount.").asLeft
    else new MaxCount(maxCount) {}.asRight

  private[sec] def render(mc: MaxCount): String =
    s"${mc.value} event${if (mc.value == 1) "" else "s"}"

/** Used for the ATOM API of EventStoreDB. The head of a feed in the ATOM API is not cacheable. This value allows you to
  * specify a period of time you want it to be cacheable. Low numbers are best here, e.g. 30-60 seconds, and introducing
  * values here will introduce latency over the ATOM protocol if caching is occuring.
  */
sealed abstract case class CacheControl(value: FiniteDuration)
object CacheControl:

  /** @param cacheControl
    *   must be greater than or equal to 1 second.
    */
  def apply(cacheControl: FiniteDuration): Either[InvalidInput, CacheControl] =
    if (cacheControl < 1.second) InvalidInput(s"cache control must be >= 1, it was $cacheControl.").asLeft
    else new CacheControl(cacheControl) {}.asRight

  private[sec] def render(cc: CacheControl): String = cc.value.toString()

//======================================================================================================================

/** @param maxAge
  *   The maximum age of events in the stream. Items older than this will be automatically removed.
  *
  * @param maxCount
  *   The maximum count of events in the stream. When you have more than count the oldest will be removed.
  *
  * @param truncateBefore
  *   When set says that events with a stream position less than the truncated before value should be removed.
  *
  * @param cacheControl
  *   The head of a feed in the atom api is not cacheable. This allows you to specify a period of time you want it to be
  *   cacheable. Low numbers are best here (say 30-60 seconds) and introducing values here will introduce latency over
  *   the atom protocol if caching is occuring.
  *
  * @param acl
  *   The access control list for this stream.
  *
  * @note
  *   More details are here https://eventstore.org/docs/server/deleting-streams-and-events/index.html
  */
final private[sec] case class MetaState(
  maxAge: Option[MaxAge],
  maxCount: Option[MaxCount],
  truncateBefore: Option[StreamPosition.Exact],
  cacheControl: Option[CacheControl],
  acl: Option[StreamAcl]
)

private[sec] object MetaState:

  val empty: MetaState = MetaState(None, None, None, None, None)

  //

  private[sec] given Codec.AsObject[MetaState] =
    new Codec.AsObject[MetaState]:

      import Decoder.{decodeInt => di, decodeLong => dl}
      import Encoder.{encodeInt => ei, encodeLong => el}

      final private val cfd: Codec[FiniteDuration] =
        Codec.from(dl.map(FiniteDuration(_, SECONDS)), el.contramap(_.toSeconds))

      given Codec[MaxAge] =
        Codec.from(cfd.emap(MaxAge(_).leftMap(_.msg)), cfd.contramap(_.value))

      given Codec[MaxCount] =
        Codec.from(di.emap(MaxCount(_).leftMap(_.msg)), ei.contramap(_.value))

      given Codec[CacheControl] =
        Codec.from(cfd.emap(CacheControl(_).leftMap(_.msg)), cfd.contramap(_.value))

      given Codec[StreamPosition.Exact] =
        Codec.from(dl.map(StreamPosition(_)), el.contramap(_.value.toLong))

      //

      def encodeObject(ms: MetaState): JsonObject =

        val data = Map(
          metadataKeys.MaxAge         -> ms.maxAge.asJson,
          metadataKeys.TruncateBefore -> ms.truncateBefore.asJson,
          metadataKeys.MaxCount       -> ms.maxCount.asJson,
          metadataKeys.Acl            -> ms.acl.asJson,
          metadataKeys.CacheControl   -> ms.cacheControl.asJson
        )

        JsonObject.fromMap(data).mapValues(_.dropNullValues)

      def apply(c: HCursor): Result[MetaState] =
        for
          maxAge         <- c.get[Option[MaxAge]](metadataKeys.MaxAge)
          truncateBefore <- c.get[Option[StreamPosition.Exact]](metadataKeys.TruncateBefore)
          maxCount       <- c.get[Option[MaxCount]](metadataKeys.MaxCount)
          acl            <- c.get[Option[StreamAcl]](metadataKeys.Acl)
          cacheControl   <- c.get[Option[CacheControl]](metadataKeys.CacheControl)
        yield MetaState(maxAge, maxCount, truncateBefore, cacheControl, acl)

  private[sec] object metadataKeys:

    final val MaxAge: String         = "$maxAge"
    final val MaxCount: String       = "$maxCount"
    final val TruncateBefore: String = "$tb"
    final val CacheControl: String   = "$cacheControl"
    final val Acl: String            = "$acl"

  def renderMetaState(ms: MetaState): String =
    s"""
       |MetaState:
       |  max-age         = ${ms.maxAge.map(MaxAge.render).getOrElse("n/a")}
       |  max-count       = ${ms.maxCount.map(MaxCount.render).getOrElse("n/a")}
       |  cache-control   = ${ms.cacheControl.map(CacheControl.render).getOrElse("n/a")}
       |  truncate-before = ${ms.truncateBefore.map(e => s"${e.value.render}").getOrElse("n/a")}
       |  access-list     = ${ms.acl.map(StreamAcl.render).getOrElse("n/a")}
       |""".stripMargin

  extension (ms: MetaState)
    def render: String =
      MetaState.renderMetaState(ms)

end MetaState

//======================================================================================================================

/** Access Control List for a stream.
  *
  * @param readRoles
  *   Roles and users permitted to read the stream.
  * @param writeRoles
  *   Roles and users permitted to write to the stream.
  * @param deleteRoles
  *   Roles and users permitted to delete the stream.
  * @param metaReadRoles
  *   Roles and users permitted to read stream metadata.
  * @param metaWriteRoles
  *   Roles and users permitted to write stream metadata.
  */
final case class StreamAcl(
  readRoles: Set[String],
  writeRoles: Set[String],
  deleteRoles: Set[String],
  metaReadRoles: Set[String],
  metaWriteRoles: Set[String]
)

object StreamAcl:

  final val empty: StreamAcl =
    StreamAcl(Set.empty, Set.empty, Set.empty, Set.empty, Set.empty)

  extension (sa: StreamAcl)

    def withReadRoles(value: Set[String]): StreamAcl      = sa.copy(readRoles = value)
    def withWriteRoles(value: Set[String]): StreamAcl     = sa.copy(writeRoles = value)
    def withMetaReadRoles(value: Set[String]): StreamAcl  = sa.copy(metaReadRoles = value)
    def withMetaWriteRoles(value: Set[String]): StreamAcl = sa.copy(metaWriteRoles = value)
    def withDeleteRoles(value: Set[String]): StreamAcl    = sa.copy(deleteRoles = value)
    def render: String                                    = StreamAcl.renderStreamAcl(sa)

  //

  private[sec] given Codec.AsObject[StreamAcl] = new Codec.AsObject[StreamAcl]:

    def encodeObject(a: StreamAcl): JsonObject =

      val roles: Map[String, Set[String]] = Map(
        aclKeys.Read      -> a.readRoles,
        aclKeys.Write     -> a.writeRoles,
        aclKeys.Delete    -> a.deleteRoles,
        aclKeys.MetaRead  -> a.metaReadRoles,
        aclKeys.MetaWrite -> a.metaWriteRoles
      )

      val nonEmptyRoles = roles.collect {
        case (k, v) if v.nonEmpty => k -> v.asJson
      }

      JsonObject.fromMap(nonEmptyRoles)

    def apply(c: HCursor): Result[StreamAcl] =

      def get(k: String): Result[Set[String]] =
        c.getOrElse[Set[String]](k)(Set.empty)(Decoder[Set[String]].or(Decoder[String].map(Set(_))))

      (get(aclKeys.Read), get(aclKeys.Write), get(aclKeys.Delete), get(aclKeys.MetaRead), get(aclKeys.MetaWrite))
        .mapN(StreamAcl.apply)

  private[sec] object aclKeys:

    final val Read: String            = "$r"
    final val Write: String           = "$w"
    final val Delete: String          = "$d"
    final val MetaRead: String        = "$mr"
    final val MetaWrite: String       = "$mw"
    final val UserStreamAcl: String   = "$userStreamAcl"
    final val SystemStreamAcl: String = "$systemStreamAcl"

  def renderStreamAcl(sa: StreamAcl): String =

    def show(label: String, roles: Set[String]): String =
      s"$label: ${roles.mkString("[", ", ", "]")}"

    val r  = show("read", sa.readRoles)
    val w  = show("write", sa.writeRoles)
    val d  = show("delete", sa.deleteRoles)
    val mr = show("meta-read", sa.metaReadRoles)
    val mw = show("meta-write", sa.metaWriteRoles)

    s"$r, $w, $d, $mr, $mw"

end StreamAcl




© 2015 - 2024 Weber Informatics LLC | Privacy Policy