Skip to content

Implement JsonPath Parser #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 40 commits into
base: v0.1.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
eddfcd9
Result type
michaelahlers May 11, 2024
ffebe7a
Depend on Parsley
michaelahlers May 11, 2024
880b98d
Relax unused imports (for now)
michaelahlers May 11, 2024
81009c9
Start sketching operator and selector grammar
michaelahlers May 11, 2024
08cf748
diffx instances for Parsley
michaelahlers May 11, 2024
d7994a3
Split out wildcard
michaelahlers May 11, 2024
8861c08
Add tests
michaelahlers May 11, 2024
9a34d99
Factor out name
michaelahlers May 11, 2024
208547a
Only accept ASCII letters
michaelahlers May 11, 2024
58f4fd4
Factor out index term
michaelahlers May 11, 2024
0a1cea4
Split out anchors
michaelahlers May 11, 2024
feedc40
Split out deep-scan and revise tests
michaelahlers May 11, 2024
d5161c6
Handle dot-noted child
michaelahlers May 11, 2024
969f2a1
Formatting
michaelahlers May 11, 2024
93ec570
Combine existing parsers
michaelahlers May 11, 2024
53bafaf
Formatting
michaelahlers May 11, 2024
ddc52c7
Support bracket-notated children
michaelahlers May 11, 2024
970d250
Support array indexes
michaelahlers May 11, 2024
1b65db4
Support array slices
michaelahlers May 11, 2024
d301d32
Remove notion of anchoring
michaelahlers May 11, 2024
84e8d30
Rename packages
michaelahlers May 11, 2024
1dc867d
Formatting
michaelahlers May 11, 2024
1f99cfa
Split out from companion object
michaelahlers May 11, 2024
8a8dd34
Split out and unseal
michaelahlers May 11, 2024
8fc392c
Support any operator parser
michaelahlers May 11, 2024
d7a6bdb
There's no commonality between index and name
michaelahlers May 11, 2024
adc9acb
Sketch expressions
michaelahlers May 11, 2024
c02b3e9
Fix bug with non-empty sequences
michaelahlers May 11, 2024
d2cb094
Exclude logical (for now)
michaelahlers May 11, 2024
33ace80
Remove relax
michaelahlers May 11, 2024
50f6a27
IsEqual filter operator
michaelahlers May 12, 2024
edf19a3
IsNotEqual filter operator
michaelahlers May 12, 2024
a9a1dc3
Add references
michaelahlers May 12, 2024
213f738
Define all remaining filter operators
michaelahlers May 12, 2024
2e57d2c
Finish algebra
michaelahlers May 12, 2024
a6a0c8f
Finish all test support
michaelahlers May 12, 2024
4d28117
Finish remaining parsers
michaelahlers May 12, 2024
6639722
Make naming consistent
michaelahlers May 12, 2024
583a40c
Fix mismatches; finish parsers and tests
michaelahlers May 13, 2024
ed16212
Fix term
michaelahlers May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import org.typelevel.scalacoptions.ScalacOptions

tpolecatExcludeOptions += ScalacOptions.warnUnusedImports
7 changes: 7 additions & 0 deletions dependencies.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
ThisBuild / scalaVersion := "2.13.14"

/**
* Parsley is a fast and modern parser combinator library for Scala.
* @see [[https://github.com/j-mie6/parsley]]
*/
libraryDependencies +=
"com.github.j-mie6" %% "parsley" % "4.5.2"

/**
* ScalaTest is the most flexible and most popular testing tool in the Scala ecosystem.
* @see [[https://www.scalatest.org/]]
Expand Down
26 changes: 26 additions & 0 deletions src/main/scala/ahlers/tree/path/Mapped.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ahlers.tree.path

case class Mapped()
object Mapped {

case class Path(toSegments: Seq[Path.Segment])

object Path {
sealed trait Segment

object Segment {
case class Key(toKey: String) extends Segment
case class Index(toIndex: Int) extends Segment
}
}

sealed trait Value

object Value {
case class Text(toText: String) extends Value
case class Integer(toBigInt: BigInt) extends Value
case class Decimal(toBigDecimal: BigDecimal) extends Value
case class Boolean(toBoolean: Boolean) extends Value
}

}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/expressions/Expression.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.expressions

trait Expression {
def toText: String
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/expressions/Logical.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.expressions

case class Logical() extends Expression {
override val toText: String = "Logical"
}
7 changes: 7 additions & 0 deletions src/main/scala/ahlers/tree/path/expressions/Selector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ahlers.tree.path.expressions

import ahlers.tree.path.operators.Operator

case class Selector(toOperators: Seq[Operator]) extends Expression {
val toText: String = toOperators.map(_.toText).mkString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ahlers.tree.path.filterOperators

/**
* @see [[https://github.com/json-path/JsonPath/blob/45333e0a310af70ad48d34d306da30af1e8e6314/README.md#filter-operators]]
*/
trait FilterOperator {
def toText: String
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/filterOperators/HasSize.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object HasSize extends FilterOperator {
override val toText: String = "size"
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/filterOperators/IsAnyOf.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsAnyOf extends FilterOperator {
override val toText: String = "anyof"
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/filterOperators/IsEmpty.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsEmpty extends FilterOperator {
override val toText: String = "empty"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsEqualTo extends FilterOperator {
override val toText: String = "=="
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsGreaterThan extends FilterOperator {
override val toText: String = ">"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsGreaterThanOrEqualTo extends FilterOperator {
override val toText: String = ">="
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/filterOperators/IsIn.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsIn extends FilterOperator {
override val toText: String = "in"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsLessThan extends FilterOperator {
override val toText: String = "<"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsLessThanOrEqualTo extends FilterOperator {
override val toText: String = "<="
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsMatchOf extends FilterOperator {
override val toText: String = "=~"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsNoneOf extends FilterOperator {
override val toText: String = "noneof"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsNotEqualTo extends FilterOperator {
override val toText: String = "!="
}
7 changes: 7 additions & 0 deletions src/main/scala/ahlers/tree/path/filterOperators/IsNotIn.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ahlers.tree.path.filterOperators

object IsNotIn extends FilterOperator {

/** \m/ */
override val toText: String = "nin"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.filterOperators

object IsSubsetOf extends FilterOperator {
override val toText: String = "subsetof"
}
7 changes: 7 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/ArrayIndexes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ahlers.tree.path.operators

import ahlers.tree.path.terms.Index

case class ArrayIndexes(toIndexes: Seq[Index]) extends Operator {
override val toText: String = toIndexes.map(_.toText).mkString("[", ",", "]")
}
17 changes: 17 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/ArraySlice.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ahlers.tree.path.operators

import ahlers.tree.path.terms.Index

sealed trait ArraySlice extends Operator

object ArraySlice {
case class LeftBounded(start: Index) extends ArraySlice {
val toText: String = s"[${start.toText}:]"
}
case class RightBounded(end: Index) extends ArraySlice {
val toText: String = s"[:${end.toText}]"
}
case class Bounded(start: Index, end: Index) extends ArraySlice {
val toText: String = s"[${start.toText}:${end.toText}]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ahlers.tree.path.operators

import ahlers.tree.path.terms.Name

case class BracketNotatedChildren(toNames: Seq[BracketNotatedChildren.Child]) extends Operator {
override val toText: String = toNames.map(_.toText).mkString("[", ",", "]")
}

object BracketNotatedChildren {
sealed trait Child {
def toText: String
}
object Child {
case class MatchingName(toName: Name) extends Child {
val toText: String = s"'${toName.toText}'"
}

case object MatchingWildcard extends Child {
val toWildcard: Wildcard.type = Wildcard
val toText: String = s"${toWildcard.toText}"
}
}
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/CurrentNode.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.operators

case object CurrentNode extends Operator {
override val toText: String = "@"
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/DeepScan.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.operators

case object DeepScan extends Operator {
override val toText: String = ".."
}
16 changes: 16 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/DotNotatedChild.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ahlers.tree.path.operators

import ahlers.tree.path.terms.Name

sealed trait DotNotatedChild extends Operator

object DotNotatedChild {
case class MatchingName(toName: Name) extends DotNotatedChild {
val toText: String = s".${toName.toText}"
}

case object MatchingWildcard extends DotNotatedChild {
val toWildcard: Wildcard.type = Wildcard
val toText: String = s".${toWildcard.toText}"
}
}
8 changes: 8 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/Operator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ahlers.tree.path.operators

/**
* @see [[https://github.com/json-path/JsonPath/blob/45333e0a310af70ad48d34d306da30af1e8e6314/README.md#operators]]
*/
trait Operator {
def toText: String
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/RootElement.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.operators

case object RootElement extends Operator {
override val toText: String = "$"
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/operators/Wildcard.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.operators

case object Wildcard extends Operator {
override val toText: String = "*"
}
14 changes: 14 additions & 0 deletions src/main/scala/ahlers/tree/path/parsers/expression.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ahlers.tree.path.parsers

import ahlers.tree.path.expressions.Expression
import ahlers.tree.path.expressions.Selector
import parsley.Parsley
import parsley.Parsley.some

object expression {

val selector: Parsley[Selector] = some(operator.any).map(Selector)

val any: Parsley[Expression] = selector

}
38 changes: 38 additions & 0 deletions src/main/scala/ahlers/tree/path/parsers/filterOperator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ahlers.tree.path.parsers

import ahlers.tree.path.filterOperators.HasSize
import ahlers.tree.path.filterOperators.IsAnyOf
import ahlers.tree.path.filterOperators.IsEmpty
import ahlers.tree.path.filterOperators.IsEqualTo
import ahlers.tree.path.filterOperators.IsGreaterThan
import ahlers.tree.path.filterOperators.IsGreaterThanOrEqualTo
import ahlers.tree.path.filterOperators.IsIn
import ahlers.tree.path.filterOperators.IsLessThan
import ahlers.tree.path.filterOperators.IsLessThanOrEqualTo
import ahlers.tree.path.filterOperators.IsMatchOf
import ahlers.tree.path.filterOperators.IsNoneOf
import ahlers.tree.path.filterOperators.IsNotEqualTo
import ahlers.tree.path.filterOperators.IsNotIn
import ahlers.tree.path.filterOperators.IsSubsetOf
import parsley.Parsley
import parsley.character.char
import parsley.character.string

object filterOperator {

val isEqualTo: Parsley[IsEqualTo.type] = string("==").as(IsEqualTo)
val isNotEqualTo: Parsley[IsNotEqualTo.type] = string("!=").as(IsNotEqualTo)
val isLessThan: Parsley[IsLessThan.type] = char('<').as(IsLessThan)
val isLessThanOrEqualTo: Parsley[IsLessThanOrEqualTo.type] = string("<=").as(IsLessThanOrEqualTo)
val isGreaterThan: Parsley[IsGreaterThan.type] = char('>').as(IsGreaterThan)
val isGreaterThanOrEqualTo: Parsley[IsGreaterThanOrEqualTo.type] = string(">=").as(IsGreaterThanOrEqualTo)
val isMatchOf: Parsley[IsMatchOf.type] = string("=~").as(IsMatchOf)
val isIn: Parsley[IsIn.type] = string("in").as(IsIn)
val isNotIn: Parsley[IsNotIn.type] = string("nin").as(IsNotIn)
val isSubsetOf: Parsley[IsSubsetOf.type] = string("subsetof").as(IsSubsetOf)
val isAnyOf: Parsley[IsAnyOf.type] = string("anyof").as(IsAnyOf)
val isNoneOf: Parsley[IsNoneOf.type] = string("noneof").as(IsNoneOf)
val hasSize: Parsley[HasSize.type] = string("size").as(HasSize)
val isEmpty: Parsley[IsEmpty.type] = string("empty").as(IsEmpty)

}
59 changes: 59 additions & 0 deletions src/main/scala/ahlers/tree/path/parsers/operator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ahlers.tree.path.parsers

import ahlers.tree.path.operators.ArrayIndexes
import ahlers.tree.path.operators.ArraySlice
import ahlers.tree.path.operators.BracketNotatedChildren
import ahlers.tree.path.operators.CurrentNode
import ahlers.tree.path.operators.DeepScan
import ahlers.tree.path.operators.DotNotatedChild
import ahlers.tree.path.operators.Operator
import ahlers.tree.path.operators.RootElement
import ahlers.tree.path.operators.Wildcard
import ahlers.tree.path.parsers.term.index
import ahlers.tree.path.parsers.term.name
import parsley.Parsley
import parsley.Parsley.atomic
import parsley.character.char
import parsley.character.string
import parsley.character.whitespaces
import parsley.combinator.sepBy1

object operator {

val rootElement: Parsley[RootElement.type] = char('$').as(RootElement)

val currentNode: Parsley[CurrentNode.type] = char('@').as(CurrentNode)

val wildcard: Parsley[Wildcard.type] = char('*').as(Wildcard)

val deepScan: Parsley[DeepScan.type] = string("..").as(DeepScan)

val dotNotatedChildMatchingName: Parsley[DotNotatedChild.MatchingName] = (char('.') *> name).map(DotNotatedChild.MatchingName)
val dotNotatedChildMatchingWildcard: Parsley[DotNotatedChild.MatchingWildcard.type] = (char('.') *> wildcard).as(DotNotatedChild.MatchingWildcard)
val dotNotatedChild: Parsley[DotNotatedChild] = atomic(dotNotatedChildMatchingName) | atomic(dotNotatedChildMatchingWildcard)

val bracketNotatedChildren: Parsley[BracketNotatedChildren] = {
import BracketNotatedChildren.Child
val childMatchingName: Parsley[Child.MatchingName] = (char('\'') *> name <* char('\'')).map(Child.MatchingName)
val childMatchingWildcard: Parsley[Child.MatchingWildcard.type] = wildcard.as(Child.MatchingWildcard)
(char('[') *> sepBy1(atomic(childMatchingName) | atomic(childMatchingWildcard), whitespaces *> char(',') <* whitespaces) <* char(']')).map(BracketNotatedChildren(_))
}

val arrayIndexes: Parsley[ArrayIndexes] = (char('[') *> sepBy1(index, whitespaces *> char(',') <* whitespaces) <* char(']')).map(ArrayIndexes)

val arraySliceLeftBounded: Parsley[ArraySlice.LeftBounded] = (char('[') *> index <~ char(':') <* char(']')).map(ArraySlice.LeftBounded)
val arraySliceRightBounded: Parsley[ArraySlice.RightBounded] = (char('[') *> char(':') ~> index <~ char(']')).map(ArraySlice.RightBounded)
val arraySliceBounded: Parsley[ArraySlice.Bounded] = (char('[') *> index <~> char(':') *> index <~ char(']')).map((ArraySlice.Bounded(_, _)).tupled)
val arraySlice: Parsley[ArraySlice] = atomic(arraySliceLeftBounded) | atomic(arraySliceRightBounded) | atomic(arraySliceBounded)

val any: Parsley[Operator] =
atomic(rootElement) |
atomic(currentNode) |
atomic(wildcard) |
atomic(deepScan) |
atomic(dotNotatedChild) |
atomic(bracketNotatedChildren) |
atomic(arrayIndexes) |
atomic(arraySlice)

}
16 changes: 16 additions & 0 deletions src/main/scala/ahlers/tree/path/parsers/term.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ahlers.tree.path.parsers

import ahlers.tree.path.terms.Index
import ahlers.tree.path.terms.Name
import parsley.Parsley
import parsley.character.satisfy
import parsley.character.stringOfSome

object term {
val index: Parsley[Index] = stringOfSome(_.isDigit).map(_.toInt).map(Index)

val name: Parsley[Name] = {
val isValid: Set[Char] = ('a' to 'z').toSet ++ ('A' to 'Z').toSet ++ ('0' to '9').toSet
stringOfSome(satisfy(isValid)).map(Name)
}
}
5 changes: 5 additions & 0 deletions src/main/scala/ahlers/tree/path/terms/Index.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ahlers.tree.path.terms

case class Index(toInt: Int) {
val toText: String = toInt.toString
}
3 changes: 3 additions & 0 deletions src/main/scala/ahlers/tree/path/terms/Name.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ahlers.tree.path.terms

case class Name(toText: String)
Loading