Created
April 5, 2022 12:55
-
-
Save moust/10cfa75e9183855665a919ebbcc845c4 to your computer and use it in GitHub Desktop.
JsonPointer based on [[https://datatracker.ietf.org/doc/html/rfc6901 RFC 6901]]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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