// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
enum JsonPath {
Root
Key(JsonPath, mut key~ : String)
Index(JsonPath, mut index~ : Int)
} derive(Eq, Debug)
///|
pub fn JsonPath::root() -> JsonPath {
Root
}
///|
pub fn JsonPath::add_index(self : JsonPath, index : Int) -> JsonPath {
Index(self, index~)
}
///|
pub fn JsonPath::add_key(self : JsonPath, key : String) -> JsonPath {
Key(self, key~)
}
///|
pub impl Show for JsonPath with output(self, logger) {
// Helper to write a JSON Pointer token without intermediate allocations
fn write_token(logger : &Logger, token : String) -> Unit {
if !token.contains_any(chars="~/") {
logger.write_string(token)
return
}
for ch in token.iter() {
match ch {
'~' => logger.write_string("~0")
'/' => logger.write_string("~1")
_ => logger.write_char(ch)
}
}
}
// Recursively build the JSON Pointer string
fn build_path(path : JsonPath, logger : &Logger) -> Unit {
match path {
Root => ()
Key(parent, key~) => {
build_path(parent, logger)
logger.write_char('/')
write_token(logger, key)
}
Index(parent, index~) => {
build_path(parent, logger)
logger.write_char('/')
logger.write_object(index)
}
}
}
build_path(self, logger)
}
///|
pub impl ToJson for JsonPath with to_json(self) {
Json::string(self.to_string())
}
///|
test "show JsonPath - JSON Pointer format" {
// Test root path
@builtin.inspect(Root, content="")
// Test simple key access
@builtin.inspect(Root.add_key("foo"), content="/foo")
// Test simple array index
@builtin.inspect(Root.add_index(0), content="/0")
// Test mixed path
let path : JsonPath = Key(Index(Root, index=0), key="foo")
@builtin.inspect(path, content="/0/foo")
@builtin.inspect(path.add_index(1), content="/0/foo/1")
@builtin.inspect(path.add_key("bar"), content="/0/foo/bar")
// Test complex paths
@builtin.inspect(
Root.add_key("foo").add_key("foo1").add_index(2),
content="/foo/foo1/2",
)
@builtin.inspect(Root.add_key("foo").add_key("foo1"), content="/foo/foo1")
@builtin.inspect(
Root.add_key("foo").add_key("foo1").add_index(2).add_key("bar"),
content="/foo/foo1/2/bar",
)
@builtin.inspect(
Root.add_key("foo").add_key("foo1").add_index(2).add_key("bar").add_index(3),
content="/foo/foo1/2/bar/3",
)
@builtin.inspect(
Root
.add_key("foo")
.add_key("foo1")
.add_index(2)
.add_key("bar")
.add_index(3)
.add_key("baz"),
content="/foo/foo1/2/bar/3/baz",
)
@builtin.inspect(
Root.add_key("foo").add_key("foo1").add_index(2).add_index(3).add_index(4),
content="/foo/foo1/2/3/4",
)
}
///|
test "show JsonPath - special characters escaping" {
// Test tilde escaping (~ becomes ~0)
@builtin.inspect(Root.add_key("foo~bar"), content="/foo~0bar")
@builtin.inspect(Root.add_key("foo~~bar~~"), content="/foo~0~0bar~0~0")
// Test slash escaping (/ becomes ~1)
@builtin.inspect(Root.add_key("foo/bar"), content="/foo~1bar")
@builtin.inspect(Root.add_key("foo//bar//"), content="/foo~1~1bar~1~1")
// Test both special characters
@builtin.inspect(Root.add_key("foo~/bar"), content="/foo~0~1bar")
@builtin.inspect(Root.add_key("~foo/bar~"), content="/~0foo~1bar~0")
// Test complex path with special characters
@builtin.inspect(
Root.add_key("a/b").add_key("c~d").add_index(0).add_key("e~/f"),
content="/a~1b/c~0d/0/e~0~1f",
)
// Test empty string key
@builtin.inspect(Root.add_key(""), content="/")
// Test key with only special characters
@builtin.inspect(Root.add_key("~/"), content="/~0~1")
}
///|
test "show JsonPath - RFC 6901 compliance examples" {
// Examples from RFC 6901
// "" // the whole document
@builtin.inspect(Root, content="")
// "/foo" ["bar", "baz"]
@builtin.inspect(Root.add_key("foo"), content="/foo")
// "/foo/0" "bar"
@builtin.inspect(Root.add_key("foo").add_index(0), content="/foo/0")
// "/" "value" (empty string key)
@builtin.inspect(Root.add_key(""), content="/")
// "/a~1b" 1 (key is "a/b")
@builtin.inspect(Root.add_key("a/b"), content="/a~1b")
// "/c%d" 2 (key is "c%d", % doesn't need escaping)
@builtin.inspect(Root.add_key("c%d"), content="/c%d")
// "/e^f" 3 (key is "e^f", ^ doesn't need escaping)
@builtin.inspect(Root.add_key("e^f"), content="/e^f")
// "/g|h" 4 (key is "g|h", | doesn't need escaping)
@builtin.inspect(Root.add_key("g|h"), content="/g|h")
// "/i\\j" 5 (key is "i\j", \ doesn't need escaping)
@builtin.inspect(Root.add_key("i\\j"), content="/i\\j")
// "/k\"l" 6 (key is "k"l", " doesn't need escaping)
@builtin.inspect(Root.add_key("k\"l"), content="/k\"l")
// "/ " 7 (key is " ", space doesn't need escaping)
@builtin.inspect(Root.add_key(" "), content="/ ")
// "/m~0n" 8 (key is "m~n")
@builtin.inspect(Root.add_key("m~n"), content="/m~0n")
}