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

advxml.transform.XmlModifier.scala Maven / Gradle / Ivy

package advxml.transform

import advxml.data.{AttributeData, Predicate}
import cats.{MonadThrow, Monoid}
import cats.data.NonEmptyList

import scala.xml.{Elem, Group, NodeSeq, UnprefixedAttribute}

sealed trait XmlModifier {
  private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq]
}
object XmlModifier extends XmlModifierValues with XmlModifierInstances

private[advxml] trait XmlModifierValues
    extends FinalXmlModifierValues
    with ComposableXmlModifierValues

private[advxml] trait XmlModifierInstances extends ComposableXmlModifierInstances

//============================= FINAL ==============================
trait FinalXmlModifier extends XmlModifier

object FinalXmlModifier extends FinalXmlModifierValues

private[transform] sealed trait FinalXmlModifierValues {

  /** Remove selected nodes.
    */
  case object Remove extends FinalXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] =
      F.pure(NodeSeq.Empty)
  }

}

//=========================== COMPOSABLE ===========================
trait ComposableXmlModifier extends XmlModifier

object ComposableXmlModifier extends ComposableXmlModifierValues with ComposableXmlModifierInstances

private[transform] sealed trait ComposableXmlModifierValues {

  /** No-Action modifiers, equals to `Replace` passing an identity function.
    */
  lazy val NoAction: ComposableXmlModifier = Replace(identity[NodeSeq])

  /** Prepend nodes to current nodes. Supported only for `Node` and `Group` elements, in other case
    * will fail.
    * @param newNs
    *   Nodes to prepend.
    */
  case class Prepend(newNs: NodeSeq) extends ComposableXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] =
      collapse[F](ns.map {
        case e: Elem  => F.pure[NodeSeq](e.copy(child = newNs ++ e.child))
        case g: Group => F.pure[NodeSeq](g.copy(nodes = newNs ++ g.nodes))
        case o        => ExceptionF.unsupported[F](this, o)
      })
  }

  /** Append nodes to current nodes. Supported only for `Node` and `Group` elements, in other case
    * will fail.
    * @param newNs
    *   Nodes to append.
    */
  case class Append(newNs: NodeSeq) extends ComposableXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] =
      collapse[F](ns.map {
        case e: Elem  => F.pure[NodeSeq](e.copy(child = e.child ++ newNs))
        case g: Group => F.pure[NodeSeq](g.copy(nodes = g.nodes ++ newNs))
        case o        => ExceptionF.unsupported[F](this, o)
      })
  }

  /** Replace current nodes.
    * @param f
    *   Function to from current nodes to new nodes.
    */
  case class Replace(f: NodeSeq => NodeSeq) extends ComposableXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] =
      F.pure(f(ns))
  }

  /** Append or replace attributes to current node.
    *
    * Supported only for `Node` elements, in other case will fail.
    * @param f
    *   takes Elem (attribute container), returns Attributes data.
    */
  case class SetAttrs(f: Elem => NonEmptyList[AttributeData]) extends ComposableXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] =
      collapse[F](ns.map {
        case e: Elem =>
          F.pure[NodeSeq](
            e.copy(
              attributes = f(e).toList.foldRight(e.attributes)((data, metadata) =>
                new UnprefixedAttribute(data.key.value, data.value.get, metadata)
              )
            )
          )
        case o => ExceptionF.unsupported[F](this, o)
      })

  }
  object SetAttrs {

    /** Create a SetAttrs attributes action with specified data.
      *
      * Supported only for `Node` elements, in other case will fail.
      * @param d
      *   Attribute data.
      * @param ds
      *   Attributes data.
      */
    def apply(d: AttributeData, ds: AttributeData*): SetAttrs =
      SetAttrs(_ => NonEmptyList.of(d, ds*))

  }

  object SetAttr {

    /** Create a SetAttrs attributes action with specified data.
      *
      * Supported only for `Node` elements, in other case will fail.
      * @param f
      *   takes the Elem (attribute container), returns Attribute data.
      */
    def apply(f: Elem => AttributeData): SetAttrs =
      SetAttrs.apply(el => NonEmptyList.one(f(el)))
  }

  /** Remove attributes.
    *
    * Supported only for `Node` elements, in other case will fail.
    * @param ps
    *   Attribute predicates.
    */
  case class RemoveAttrs(ps: NonEmptyList[AttributeData => Boolean]) extends ComposableXmlModifier {
    override private[advxml] def apply[F[_]](ns: NodeSeq)(implicit F: MonadThrow[F]): F[NodeSeq] = {
      val attrsToRemoveP = ps.reduce[AttributeData => Boolean]((p1, p2) => Predicate.or(p1, p2))
      collapse[F](ns.map {
        case e: Elem =>
          val newAttrs = AttributeData
            .fromElem(e)
            .filter(attrsToRemoveP)
            .map(_.key)
            .foldLeft(e.attributes)((attrs, key) => attrs.remove(key.value))

          F.pure[NodeSeq](e.copy(attributes = newAttrs))
        case o => ExceptionF.unsupported[F](this, o)
      })
    }
  }
  object RemoveAttrs {

    /** Create a Remove attributes action with specified filters.
      * @param p
      *   Attribute predicate.
      * @param ps
      *   Attribute predicates.
      */
    def apply(p: AttributeData => Boolean, ps: (AttributeData => Boolean)*): RemoveAttrs =
      RemoveAttrs(
        NonEmptyList.of(p, ps*)
      )
  }

  private def collapse[F[_]: MonadThrow](seq: Seq[F[NodeSeq]]): F[NodeSeq] = {
    import cats.implicits.*
    seq.toList.sequence.map(_.reduce(_ ++ _))
  }

  private object ExceptionF {

    def apply[F[_]](msg: String)(implicit F: MonadThrow[F]): F[NodeSeq] =
      F.raiseError[NodeSeq](new RuntimeException(msg))

    def unsupported[F[_]: MonadThrow](modifier: XmlModifier, ns: NodeSeq): F[NodeSeq] =
      ExceptionF[F](s"Unsupported operation $modifier for type ${ns.getClass.getName}")
  }
}

private[transform] sealed trait ComposableXmlModifierInstances {

  implicit val composableXmlModifierMonoidInstance: Monoid[ComposableXmlModifier] =
    new Monoid[ComposableXmlModifier] {

      import cats.syntax.flatMap.*

      override def empty: ComposableXmlModifier = ComposableXmlModifier.NoAction

      override def combine(
        x: ComposableXmlModifier,
        y: ComposableXmlModifier
      ): ComposableXmlModifier =
        new ComposableXmlModifier {
          override def apply[F[_]: MonadThrow](ns: NodeSeq): F[NodeSeq] =
            x.apply[F](ns).flatMap(y.apply[F](_))
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy