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

org.scaladebugger.api.pipelines.Pipeline.scala Maven / Gradle / Ivy

package org.scaladebugger.api.pipelines

import java.io.Closeable

import scala.collection.GenTraversableOnce
import scala.concurrent.{Promise, Future}
import scala.util.Try

/**
 * Represents a pipeline of instructions used to perform a series of operations
 * over an arbitrary collection of data.
 *
 * @tparam A The incoming data type
 * @tparam B The outgoing data type
 * @param operation The operation to apply to incoming data
 * @param closeFunc The function to invoke to close the pipeline
 * @param metadataMap The map of metadata to hold in the pipeline instance
 */
class Pipeline[A, B] private[pipelines] (
  val operation: Operation[A, B],
  private[pipelines] val closeFunc: Pipeline.CloseFunctionWithData,
  private val metadataMap: Pipeline.Metadata
) extends Closeable {
  /**
   * Creates a new instance of the pipeline with the specified operation. The
   * close function is considered a no-op on this pipeline. The metadata is an
   * empty map for this pipeline.
   *
   * @param operation The operation to apply to incoming data
   *
   * @return The new pipeline instance
   */
  private[pipelines] def this(operation: Operation[A, B]) =
    this(operation, Pipeline.DefaultCloseFunc, Pipeline.DefaultMetadataMap)

  /**
   * Creates a new instance of the pipeline with the specified operation and
   * close function. The metadata is an empty map for this pipeline.
   *
   * @param operation The operation to apply to incoming data
   * @param closeFunc The function to invoke to close the pipeline
   *
   * @return The new pipeline instance
   */
  private[pipelines] def this(
    operation: Operation[A, B],
    closeFunc: Pipeline.CloseFunctionWithData
  ) = this(operation, closeFunc, Pipeline.DefaultMetadataMap)

  /**
   * Creates a new instance of the pipeline with the specified operation and
   * close function. The metadata is an empty map for this pipeline.
   *
   * @param operation The operation to apply to incoming data
   * @param closeFunc The function to invoke to close the pipeline
   *
   * @return The new pipeline instance
   */
  private[pipelines] def this(
    operation: Operation[A, B],
    closeFunc: Pipeline.CloseFunction
  ) = this(operation, (_) => closeFunc(), Pipeline.DefaultMetadataMap)

  /**
   * Creates a new instance of the pipeline with the specified operation and
   * metadata. The close function is considered a no-op on this pipeline.
   *
   * @param operation The operation to apply to incoming data
   * @param metadataMap The map of metadata to hold in the pipeline instance
   *
   * @return The new pipeline instance
   */
  private[pipelines] def this(
    operation: Operation[A, B],
    metadataMap: Pipeline.Metadata
  ) = this(operation, Pipeline.DefaultCloseFunc, metadataMap)

  /**
   * Creates a new instance of the pipeline with the specified operation and
   * close function. The metadata is an empty map for this pipeline.
   *
   * @param operation The operation to apply to incoming data
   * @param closeFunc The function to invoke to close the pipeline
   * @param metadataMap The map of metadata to hold in the pipeline instance
   *
   * @return The new pipeline instance
   */
  private[pipelines] def this(
    operation: Operation[A, B],
    closeFunc: Pipeline.CloseFunction,
    metadataMap: Pipeline.Metadata
  ) = this(operation, (_) => closeFunc(), metadataMap)

  /** Any child pipelines whose input is the output of this pipeline. */
  @volatile private var _children: Seq[Pipeline[B, _]] = Nil

  /** Failure route at this stage in the pipeline. */
  private lazy val _failure = newPipeline(new NoOperation[Throwable])

  /** Metadata route at this stage in the pipeline. */
  private lazy val _metadata = map(data => (data, currentMetadata))

  /**
   * Retrieves the metadata route of the current pipeline stage.
   *
   * @return The pipeline to process the metadata for the pipeline, containing
   *         the data and metadata as a tuple
   */
  def metadata: Pipeline[B, (B, Pipeline.Metadata)] = _metadata

  /**
   * Retrieves the current metadata at this stage in the pipeline.
   *
   * @return The map of metadata
   */
  def currentMetadata: Pipeline.Metadata = metadataMap

  /**
   * Adds additional metadata by creating a new stage in the pipeline with the
   * additional metadata merged with the existing metadata.
   *
   * @param metadataMap The new metadata to add to future pipeline stages
   *
   * @return The resulting pipeline instance with the updated metadata
   */
  def withMetadata(metadataMap: Pipeline.Metadata): Pipeline[B, B] = {
    val childPipeline = newPipeline(
      new NoOperation[B],
      closeFunc,
      currentMetadata ++ metadataMap
    )

    addChildPipeline(childPipeline)
  }

  /**
   * Retrieves the failure route of the current pipeline stage.
   *
   * @return The pipeline to process the throwable from the failure
   */
  def failed: Pipeline[Throwable, Throwable] = _failure

  /**
   * Retrieves the collection of children pipelines for the current pipeline.
   *
   * @return The collection of pipelines that are children to this pipeline
   */
  def children: Seq[Pipeline[B, _]] = _children

  /**
   * Adds the specified pipeline as a child of this pipeline.
   *
   * @param childPipeline The pipeline to add as a child
   * @tparam C The outgoing type of the child pipeline
   *
   * @return The added child pipeline
   */
  protected def addChildPipeline[C](
    childPipeline: Pipeline[B, C]
  ): Pipeline[B, C] = {
    _children :+= childPipeline
    childPipeline
  }

  /**
   * Creates a new pipeline using the given operation. This is used to generate
   * pipelines within the pipeline itself and can be overridden to generate
   * a different kind of pipeline. It is recommended to override this method
   * when subclassing pipeline.
   *
   * @param operation The operation to provide to the new pipeline
   * @param closeFunc The function used to close the new pipeline
   * @param metadataMap The map of metadata to hold in the pipeline instance
   * @tparam C The input type of the new pipeline
   * @tparam D The output type of the new pipeline
   *
   * @return The new pipeline
   */
  protected def newPipeline[C, D](
    operation: Operation[C, D],
    closeFunc: Pipeline.CloseFunctionWithData,
    metadataMap: Pipeline.Metadata
  ): Pipeline[C, D] = new Pipeline[C, D](operation, closeFunc, metadataMap)

  /**
   * Creates a new pipeline using the given operation. This is used to generate
   * pipelines within the pipeline itself and can be overridden to generate
   * a different kind of pipeline. It is recommended to override this method
   * when subclassing pipeline.
   *
   * @note This implementation passes down the current pipeline's
   *       close function and metadata map.
   *
   * @param operation The operation to provide to the new pipeline
   * @tparam C The input type of the new pipeline
   * @tparam D The output type of the new pipeline
   *
   * @return The new pipeline
   */
  protected def newPipeline[C, D](operation: Operation[C, D]): Pipeline[C, D] =
    newPipeline[C, D](operation, closeFunc, metadataMap)

  /**
   * Processes the provided data through this specific pipeline instance and
   * all subsequent children of this pipeline instance. No parent pipeline
   * instance will be used during the processing of the data.
   *
   * @param data The data to process
   *
   * @return If successful, the transformed collection of data at this
   *         specific pipeline instance, otherwise the thrown exception
   */
  def process(data: A*): Try[Seq[B]] = {
    val results = Try(operation.process(data))

    // If successful, continue down children paths
    results.foreach(r => children.foreach(_.process(r: _*)))

    // If failed, continue down failure path
    results.failed.foreach(throwable => failed.process(throwable))

    results
  }

  /**
   * Transforms the output of this pipeline using the provided operation.
   *
   * @note Inherits the close function of the pipeline.
   *
   * @param operation The operation to use to transform the output of this
   *                  pipeline instance
   * @tparam C The resulting type of the output from the operation
   *
   * @return The resulting pipeline instance from applying the operation
   */
  def transform[C](operation: Operation[B, C]): Pipeline[B, C] = {
    val childPipeline = newPipeline(operation)
    addChildPipeline(childPipeline)
  }

  /**
   * Maps the output of this pipeline instance to new values.
   *
   * @note Inherits the close function of the pipeline.
   *
   * @param f The function to use for mapping data to new values
   * @tparam C The resulting type of the new values
   *
   * @return The resulting pipeline instance with the mapped data
   */
  def map[C](f: (B) => C): Pipeline[B, C] = {
    transform(new MapOperation(f))
  }

  /**
   * Maps the output of this pipeline instance to new values and then flattens
   * the results.
   *
   * @param f The function to use for mapping data to new values that will
   *          be flattened
   * @tparam C The resulting type of the new values
   *
   * @return The resulting pipeline instance with the mapped and flattened data
   */
  def flatMap[C](f: (B) => GenTraversableOnce[C]): Pipeline[B, C] = {
    transform(new FlatMapOperation(f))
  }

  /**
   * Filters the output of this pipeline instance.
   *
   * @param f The function to use for filtering data (only true results will
   *          remain in output)
   *
   * @return The resulting pipeline instance with the filtered data
   */
  def filter(f: (B) => Boolean): Pipeline[B, B] = {
    transform(new FilterOperation(f))
  }

  /**
   * Filters the output of this pipeline instance.
   *
   * @param f The function to use for filtering data (only false results will
   *          remain in output)
   *
   * @return The resulting pipeline instance with the filtered data
   */
  def filterNot(f: (B) => Boolean): Pipeline[B, B] = {
    transform(new FilterNotOperation(f))
  }

  /**
   * Applies the provided function to the output of this pipeline, returning
   * nothing from the function.
   *
   * @param f The function to apply
   */
  def foreach(f: (B) => Unit): Unit = {
    transform(new ForeachOperation(f))
  }

  /**
   * Unions this pipeline with another pipeline that has the same input such
   * that input from either pipeline is used for both.
   *
   * @param other The other pipeline whose input to union together
   *
   * @return The unioned pipeline
   */
  def unionInput(other: Pipeline[A, _]): Pipeline[A, A] = {
    val parentPipeline = newPipeline(
      new NoOperation[A],
      Pipeline.DefaultCloseFunc,
      Pipeline.DefaultMetadataMap
    )

    parentPipeline.addChildPipeline(this)
    parentPipeline.addChildPipeline(other)

    parentPipeline
  }

  /**
   * Unions this pipeline with another pipeline that has the same output such
   * that output from either pipeline flows through the union.
   *
   * @param other The other pipeline to whose output to union together
   *
   * @return The unioned pipeline
   */
  def unionOutput(other: Pipeline[_, B]): Pipeline[B, B] = {
    val childPipeline = newPipeline(new NoOperation[B], (data) => {
      val result1 = Try(this.close(now = true, data = data.orNull))
      val result2 = Try(other.close(now = true, data = data.orNull))

      // Throw the exceptions if they occur AFTER processing both closes
      result1.get
      result2.get
    }, this.currentMetadata ++ other.currentMetadata)

    this.addChildPipeline(childPipeline)
    other.addChildPipeline(childPipeline)

    childPipeline
  }

  /**
   * Applies a no-op on the current pipeline.
   *
   * @return The pipeline after a no-op has been applied
   */
  def noop(): Pipeline[B, B] = transform(new NoOperation[B])

  /**
   * Closes the pipeline.
   *
   * @param now If true, should perform the closing action immediately rather
   *            than on the next data fed through the pipeline
   * @param data Any data to be provided to the close function
   */
  def close(now: Boolean = true, data: Any = null): Unit = {
    if (now) closeFunc(Option(data))
    else transform(new CloseOperation(() => closeFunc(Option(data))))
  }

  /**
   * Closes the pipeline immediately. No data is provided.
   */
  def close(): Unit = close(now = true, data = null)

  /**
   * Transforms the pipeline into a future that will be evaluated once and then
   * closes the underlying pipeline.
   *
   * @return The future representing this pipeline
   */
  def toFuture: Future[B] = {
    val pipelinePromise = Promise[B]()

    foreach(value => {
      pipelinePromise.success(value)
      close()
    })

    failed.foreach(throwable => {
      pipelinePromise.failure(throwable)
      close()
    })

    pipelinePromise.future
  }
}

/**
 * Contains helper utilities for pipeline creation.
 */
object Pipeline {
  /** Represents a pipeline whose input and output types are the same. */
  type IdentityPipeline[A] = Pipeline[A, A]

  /** Represents the metadata for a pipeline. */
  type Metadata = Map[String, Any]

  /** Represents a close function with data. */
  type CloseFunctionWithData = (Option[Any]) => Unit

  /** Represents a close function with no data. */
  type CloseFunction = () => Unit

  /** Default close function. Does nothing. */
  val DefaultCloseFunc: CloseFunctionWithData = (_) => {}

  /** Default metadata map. Is empty. */
  val DefaultMetadataMap: Metadata = Map()

  /**
   * Creates an empty pipeline expecting data of the specified type. The
   * associated close function is equivalent to a no-op.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   *
   * @return The new pipeline
   */
  def newPipeline[A](klass: Class[A]): IdentityPipeline[A] =
    new Pipeline(new NoOperation[A])

  /**
   * Creates an empty, closeable pipeline expecting data of the specified type.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   * @param closeFunc The function to invoke when closing the pipeline
   *
   * @return The new pipeline
   */
  def newPipeline[A](
    klass: Class[A],
    closeFunc: CloseFunction
  ): Pipeline[A, A] = new Pipeline(new NoOperation[A], closeFunc)

  /**
   * Creates an empty, closeable pipeline expecting data of the specified type.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   * @param closeFunc The function to invoke when closing the pipeline
   *
   * @return The new pipeline
   */
  def newPipeline[A](
    klass: Class[A],
    closeFunc: CloseFunctionWithData
  ): Pipeline[A, A] = new Pipeline(new NoOperation[A], closeFunc)

  /**
   * Creates an empty pipeline expecting data of the specified type and
   * containing the provided metadata. The associated close function is
   * equivalent to a no-op.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   * @param metadataMap The map of metadata to hold in the pipeline instance
   *
   * @return The new pipeline instance
   */
  def newPipeline[A](
    klass: Class[A],
    metadataMap: Pipeline.Metadata
  ): Pipeline[A, A] = new Pipeline(new NoOperation[A], metadataMap)

  /**
   * Creates an empty pipeline expecting data of the specified type and
   * containing the provided metadata.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   * @param closeFunc The function to invoke when closing the pipeline
   * @param metadataMap The map of metadata to hold in the pipeline instance
   *
   * @return The new pipeline instance
   */
  def newPipeline[A](
    klass: Class[A],
    closeFunc: CloseFunction,
    metadataMap: Pipeline.Metadata
  ): Pipeline[A, A] = new Pipeline(new NoOperation[A], closeFunc, metadataMap)

  /**
   * Creates an empty pipeline expecting data of the specified type and
   * containing the provided metadata.
   *
   * @tparam A The type of incoming data
   * @param klass The class representing the input of the new pipeline
   * @param closeFunc The function to invoke when closing the pipeline
   * @param metadataMap The map of metadata to hold in the pipeline instance
   *
   * @return The new pipeline instance
   */
  def newPipeline[A](
    klass: Class[A],
    closeFunc: CloseFunctionWithData,
    metadataMap: Pipeline.Metadata
  ): Pipeline[A, A] = new Pipeline(new NoOperation[A], closeFunc, metadataMap)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy