Skip to content

Instantly share code, notes, and snippets.

@moust
Created April 5, 2022 12:55
Show Gist options
  • Save moust/10cfa75e9183855665a919ebbcc845c4 to your computer and use it in GitHub Desktop.
Save moust/10cfa75e9183855665a919ebbcc845c4 to your computer and use it in GitHub Desktop.
JsonPointer based on [[https://datatracker.ietf.org/doc/html/rfc6901 RFC 6901]]
import atto.Atto._
import atto.Parser
import io.circe.optics.JsonPath
import io.circe.optics.JsonPath.root
import scala.annotation.tailrec
import scala.util.Try
object JsonPointer {
def parse(path: String): Either[String, JsonPath] = jsonPointer.parseOnly(path).either.map(evalCursor(root, _))
// %x00-2E / %x30-7D / %x7F-10FFFF
// %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
private val unescaped: Parser[Char] = charRange(
Integer.parseInt("00", 16).toChar to Integer.parseInt("2E", 16).toChar,
Integer.parseInt("30", 16).toChar to Integer.parseInt("7D", 16).toChar,
Integer.parseInt("7F", 16).toChar to Integer.parseInt("10FFFF", 16).toChar,
)
// "~" ( "0" / "1" )
private val escaped: Parser[Char] = (char('~') ~ (char('0') | char('1'))).map {
case ('~', '0') => '~'
case ('~', '1') => '/'
case _ => throw new Exception("Invalid pointer syntax")
}
// *( unescaped / escaped )
private val referenceToken: Parser[String] = many(unescaped | escaped).map(_.mkString)
// *("/" reference-token)
private val jsonPointer: Parser[List[String]] = many(char('/') ~> referenceToken)
@tailrec
private def evalCursor(
root: JsonPath,
path: List[String],
): JsonPath =
path match {
case field :: index :: tail if Try(index.toInt).isSuccess =>
evalCursor(root.applyDynamic(field)(index.toInt), tail)
case field :: tail => evalCursor(root.selectDynamic(field), tail)
case Nil => root
}
}
import io.circe._
import io.circe.syntax._
class JsonPointerSuite extends munit.ScalaCheckSuite {
private val json: Json = Json.obj(
"foo" -> List("bar", "baz").asJson,
"" -> 0.asJson,
"a/b" -> 1.asJson,
"c%d" -> 2.asJson,
"e^f" -> 3.asJson,
"g|h" -> 4.asJson,
"i\\j" -> 5.asJson,
"k\"l" -> 6.asJson,
" " -> 7.asJson,
"m~n" -> 8.asJson,
)
private def testJsonPointer(
path: String,
exepected: Option[Json],
): Unit = {
println()
val obtained = JsonPointer.parse(path) match {
case Left(error) => fail(error)
case Right(jsonPath) => jsonPath.json.getOption(json)
}
test(s"""JsonPointer should parse json path "$path" as a valid Circe optic""") {
assertEquals(obtained, exepected, path)
}
}
testJsonPointer("", json.hcursor.focus)
testJsonPointer("/foo", json.hcursor.downField("foo").focus)
testJsonPointer("/foo/0", json.hcursor.downField("foo").downN(0).focus)
testJsonPointer("/", json.hcursor.downField("").focus)
testJsonPointer("/a~1b", json.hcursor.downField("a/b").focus)
testJsonPointer("/c%d", json.hcursor.downField("c%d").focus)
testJsonPointer("/e^f", json.hcursor.downField("e^f").focus)
testJsonPointer("/g|h", json.hcursor.downField("g|h").focus)
// testJsonPointer("i\\j", json.hcursor.downField("i\\j").focus) // TODO: do not work
// testJsonPointer("k\"l", json.hcursor.downField("k\"l").focus) // TODO: do not work
testJsonPointer("/ ", json.hcursor.downField(" ").focus)
testJsonPointer("/m~0n", json.hcursor.downField("m~n").focus)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment