Skip to content

Commit

Permalink
Merge pull request #317 from leviysoft/feature/xpath-support
Browse files Browse the repository at this point in the history
XPath support
  • Loading branch information
geirolz authored Jun 20, 2022
2 parents a2520f7 + 00646ae commit c0d8619
Show file tree
Hide file tree
Showing 11 changed files with 1,025 additions and 3 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ Advxml supports 2.13 and 3

**Sbt**
```sbt mdoc
libraryDependencies += "com.github.geirolz" %% "advxml-core" % 2.5.0
libraryDependencies ++= Seq(
"com.github.geirolz" %% "advxml-core" % "2.5.0",
"com.github.geirolz" %% "advxml-xpath" % "2.5.0" //optional, for xpath support
)
```

## Structure
Expand All @@ -34,3 +37,4 @@ The idea behind this library is offer a fluent syntax to edit and read xml.
- [@dcsobral](https://github.com/dcsobral)
- [@liff](https://github.com/liff)
- [@argast](https://github.com/argast)
- [@danslapman](https://github.com/danslapman)
12 changes: 11 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ lazy val advxml: Project = project
description := "A lightweight, simple and functional library DSL to work with XML in Scala with Cats",
organization := org
)
.aggregate(core)
.aggregate(core, xpath)

lazy val core: Project =
buildModule(
Expand All @@ -39,6 +39,16 @@ lazy val core: Project =
folder = "."
)

lazy val xpath: Project =
buildModule(
path = "xpath",
toPublish = true,
folder = "."
).dependsOn(core % "test->test;compile->compile")
.settings(
libraryDependencies ++= Dependencies.XPath.dedicated
)

//=============================== MODULES UTILS ===============================
def buildModule(path: String, toPublish: Boolean, folder: String = "modules"): Project = {
val keys = path.split("-")
Expand Down
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ Advxml supports 2.13 and 3

**Sbt**
```sbt mdoc
libraryDependencies += "com.github.geirolz" %% "advxml-core" % 2.4.2
libraryDependencies ++= Seq(
"com.github.geirolz" %% "advxml-core" % "2.5.0",
"com.github.geirolz" %% "advxml-xpath" % "2.5.0" //optional, for xpath support
)
```

## Structure
Expand All @@ -34,3 +37,4 @@ The idea behind this library is offer a fluent syntax to edit and read xml.
- [@dcsobral](https://github.com/dcsobral)
- [@liff](https://github.com/liff)
- [@argast](https://github.com/argast)
- [@danslapman](https://github.com/danslapman)
8 changes: 8 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ object Dependencies {
"org.scalacheck" %% "scalacheck" % "1.15.4" % Test cross CrossVersion.binary
)

object XPath {
lazy val dedicated: Seq[ModuleID] = Seq(parser).flatten

private val parser: Seq[ModuleID] = Seq(
"eu.cdevreeze.xpathparser" %% "xpathparser" % "0.8.0"
)
}

object Plugins {
lazy val compilerPluginsFor2: Seq[ModuleID] = Seq(
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3" cross CrossVersion.binary)
Expand Down
173 changes: 173 additions & 0 deletions xpath/src/main/scala/advxml/xpath/XmlPredicateBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package advxml.xpath

import advxml.data.Key
import advxml.data.SimpleValue
import advxml.data.XmlPredicate
import advxml.implicits.*
import advxml.xpath.error.XPathError
import cats.Endo
import cats.data.ValidatedNel
import cats.syntax.apply.*
import cats.syntax.foldable.*
import cats.syntax.traverse.*
import cats.syntax.validated.*
import eu.cdevreeze.xpathparser.ast.*

object XmlPredicateBuilder {
def build(expr: Expr): ValidatedNel[XPathError.NotSupportedConstruction, XmlPredicate] =
expr match {
case expr: CompoundComparisonExpr => buildCCE(expr)
case expr: RelativePathExpr => buildRPE(expr).map(_(_ => true))
case CompoundOrExpr(head, tail) =>
import advxml.xpath.utils.predicate.or.*
(build(head), tail.traverse(build(_))).mapN(_ +: _).map(_.combineAll)
case CompoundAndExpr(head, tail) =>
import advxml.xpath.utils.predicate.and.*
(build(head), tail.traverse(build(_))).mapN(_ +: _).map(_.combineAll)
case e => notSupported(e)
}

def buildCCE(
expr: CompoundComparisonExpr
): ValidatedNel[XPathError.NotSupportedConstruction, XmlPredicate] =
expr match {
case CompoundComparisonExpr(
cpe @ CompoundOrExact(
ForwardAxisStep(
AttributeAxisAbbrevForwardStep(SimpleNameTest(EQNameEx(name))),
EmptySeq()
)
),
GeneralComp.Eq,
StringLiteral(value)
) =>
buildRPE(cpe).map(p => p(XmlPredicate.attrs(Key(name) === value)))

case CompoundComparisonExpr(
cpe @ CompoundOrExact(
ForwardAxisStep(SimpleAbbrevForwardStep(SimpleNameTest(_)), EmptySeq())
),
comp: GeneralComp,
IntegerLiteral(value)
) =>
buildRPE(cpe).map(p =>
p(XmlPredicate.text(_.asOption[Int].exists(comp match {
case GeneralComp.Eq => _ == value.toInt
case GeneralComp.Ne => _ != value.toInt
case GeneralComp.Lt => _ < value.toInt
case GeneralComp.Le => _ <= value.toInt
case GeneralComp.Gt => _ > value.toInt
case GeneralComp.Ge => _ >= value.toInt
})))
)

case CompoundComparisonExpr(
cpe @ CompoundOrExact(ForwardAxisStep(SimpleAbbrevForwardStep(TextTest), EmptySeq())),
GeneralComp.Eq,
StringLiteral(value)
) =>
buildRPE(cpe).map(p => p(XmlPredicate.text(_ == value)))

case e => notSupported(e)
}

def buildRPE(
expr: RelativePathExpr
): ValidatedNel[XPathError.NotSupportedConstruction, Endo[XmlPredicate]] =
expr match {
case ForwardAxisStep(SimpleAbbrevForwardStep(SimpleNameTest(EQNameEx(name))), EmptySeq()) =>
xpred(XmlPredicate.hasImmediateChild(name, _)).validNel

case CompoundRelativePathExpr(init, StepOp.SingleSlash, lastStep) =>
(buildRPE(init), buildRPE(lastStep)).mapN { case (init, last) => init.andThen(last) }

case ForwardAxisStep(SimpleAbbrevForwardStep(TextTest), EmptySeq()) =>
(identity[XmlPredicate] _).validNel

case ForwardAxisStep(AttributeAxisAbbrevForwardStep(SimpleNameTest(_)), EmptySeq()) =>
(identity[XmlPredicate] _).validNel

case FunctionCall(
EQNameEx(fn @ ("contains" | "starts-with" | "ends-with")),
ArgumentList(
UnSeq(
ExprSingleArgument(ForwardAxisStep(SimpleAbbrevForwardStep(TextTest), EmptySeq())),
ExprSingleArgument(StringLiteral(value))
)
)
) =>
fn match {
case "contains" => const(XmlPredicate.text(_.contains(value))).validNel
case "starts-with" => const(XmlPredicate.text(_.startsWith(value))).validNel
case "ends-with" => const(XmlPredicate.text(_.endsWith(value))).validNel
}

case FunctionCall(
EQNameEx(fn @ ("contains" | "starts-with" | "ends-with")),
ArgumentList(
UnSeq(
ExprSingleArgument(
ForwardAxisStep(
AttributeAxisAbbrevForwardStep(SimpleNameTest(EQNameEx(attr))),
EmptySeq()
)
),
ExprSingleArgument(StringLiteral(value))
)
)
) =>
fn match {
case "contains" =>
const(
XmlPredicate.attrs(
Key(attr) -> ((_: SimpleValue).asOption[String].exists(_.contains(value)))
)
).validNel

case "starts-with" =>
const(
XmlPredicate.attrs(
Key(attr) -> ((_: SimpleValue).asOption[String].exists(_.startsWith(value)))
)
).validNel

case "ends-with" =>
const(
XmlPredicate.attrs(
Key(attr) -> ((_: SimpleValue).asOption[String].exists(_.endsWith(value)))
)
).validNel
}

case FunctionCall(
EQNameEx("not"),
ArgumentList(
UnSeq(
ExprSingleArgument(fc: FunctionCall)
)
)
) =>
buildRPE(fc).map(pred => pred.andThen(_.andThen(!_)))

case e => notSupported(e)
}

private def notSupported(
feature: XPathElem
): ValidatedNel[XPathError.NotSupportedConstruction, Nothing] =
XPathError.NotSupportedConstruction(feature).invalidNel

@inline
private def xpred(f: Endo[XmlPredicate]): Endo[XmlPredicate] = f

@inline
private def const(p: XmlPredicate): Endo[XmlPredicate] = _ => p

private object CompoundOrExact {
def unapply(expr: RelativePathExpr): Option[StepExpr] =
expr match {
case se: StepExpr => Some(se)
case crpe: CompoundRelativePathExpr => Some(crpe.lastStepExpr)
}
}
}
Loading

0 comments on commit c0d8619

Please sign in to comment.