JSON pointer: Difference between revisions
m →{{header|Wren}}: Changed to Wren S/H |
m Promote to full task |
||
Line 1: | Line 1: | ||
{{ |
{{task}} |
||
'''JSON Pointer''' is a syntax for targeting a value in a [https://en.wikipedia.org/wiki/JSON JSON] (JavaScript Object Notation) document. |
'''JSON Pointer''' is a syntax for targeting a value in a [https://en.wikipedia.org/wiki/JSON JSON] (JavaScript Object Notation) document. |
Revision as of 06:46, 21 June 2024
![Task](http://static.miraheze.org/rosettacodewiki/thumb/b/ba/Rcode-button-task-crushed.png/64px-Rcode-button-task-crushed.png)
You are encouraged to solve this task according to the task description, using any language you may know.
JSON Pointer is a syntax for targeting a value in a JSON (JavaScript Object Notation) document.
Standardized in RFC 6901, a JSON Pointer is a Unicode string containing slash (/
) separated tokens. Each token is either a potential property name for a JSON object, or a potential index for a JSON array. When a property name contains a slash (/
) or a tilde (~
), they are encoded as ~1
and ~0
, respectively.
- Pseudocode
Evaluating or resolving a JSON Pointer against an arbitrary JSON document might look like this.
set the current node to the document root FOR each token in the pointer decode the token IF the current node is an array IF the token is a string representation of an array index AND the index is in range set the current node to node[index] ELSE error condition ENDIF ELSE IF the current node is an object IF the current node has a property matching token set the current node to node[token] ELSE error condition ENDIF ELSE error condition ENDIF ENDFOR
Barring any error conditions, the result is the value of the current node after the loop has completed. For example, evaluating the JSON Pointer /foo/bar/1
against the JSON document {"foo": {"bar": ["hello", "world"]}}
, would result in the value "world"
.
See RFC 6901 for details.
- Task
Demonstrate parsing and evaluation of JSON Pointers using, at least, the following examples. Display, on this page, either the resulting JSON value for each test case or a helpful error message.
- Example JSON document
{ "wiki": { "links": [ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] }, "": "Rosetta", " ": "Code", "g/h": "chrestomathy", "i~j": "site", "abc": ["is", "a"], "def": { "": "programming" } }
- Example JSON pointers
JSON pointers are surrounded by quotes to emphasize whitespace and the empty pointer. The quotes are not part of the pointer.
JSON pointer | Expected result |
---|---|
"" |
The entire input document. |
"/" |
"Rosetta"
|
"/ " |
"Code"
|
"/abc" |
["is", "a"]
|
"/def/" |
"programming"
|
"/g~1h" |
"chrestomathy"
|
"/i~0j" |
"site"
|
"/wiki/links/0" |
"https://rosettacode.org/wiki/Rosetta_Code"
|
"/wiki/links/1" |
"https://discord.com/channels/1011262808001880065"
|
"/wiki/links/2" |
Error condition. |
"/wiki/name" |
Error condition. |
"/no/such/thing" |
Error condition. |
"bad/pointer" |
Error condition. |
- Reference
- Related tasks
Go
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
)
var reIndex = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
type JSONPointer []string
func NewJSONPointer(pointer string) (*JSONPointer, error) {
var tokens JSONPointer
if pointer == "" {
return &tokens, nil
}
p, slash := strings.CutPrefix(pointer, "/")
if !slash && len(p) > 0 {
return nil, fmt.Errorf(
"\"%s\" pointers must start with a slash or be the empty string", pointer)
}
for _, token := range strings.Split(p, "/") {
tokens = append(tokens,
strings.ReplaceAll(strings.ReplaceAll(token, "~1", "/"), "~0", "~"),
)
}
return &tokens, nil
}
func (p JSONPointer) Resolve(data interface{}) (interface{}, error) {
obj := data
var found bool
for i, token := range p {
obj, found = getItem(obj, token)
if !found {
return nil, fmt.Errorf("\"%s\" does not exist", encode(p[:i+1]))
}
}
return obj, nil
}
func (p JSONPointer) String() string {
return encode(p)
}
func encode(tokens []string) string {
var encoded []string
for _, token := range tokens {
encoded = append(encoded,
strings.ReplaceAll(strings.ReplaceAll(token, "~", "~0"), "/", "~1"))
}
if len(encoded) == 0 {
return ""
}
return "/" + strings.Join(encoded, "/")
}
func getItem(data interface{}, token string) (interface{}, bool) {
switch data.(type) {
case []interface{}:
return getArrayItem(data.([]interface{}), token)
case map[string]interface{}:
return getObjectItem(data.(map[string]interface{}), token)
default:
return nil, false
}
}
func getArrayItem(array []interface{}, token string) (interface{}, bool) {
if reIndex.MatchString(token) {
if idx, err := strconv.Atoi(token); err == nil && idx < len(array) {
return array[idx], true
}
}
return nil, false
}
func getObjectItem(object map[string]interface{}, token string) (interface{}, bool) {
if val, found := object[token]; found {
return val, true
}
return nil, false
}
func prettyJSON(data interface{}) string {
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Fatal(err)
}
return string(b)
}
func loadJSON(f string) interface{} {
bytes, err := os.ReadFile(f)
if err != nil {
log.Fatal(err)
}
var obj interface{}
err = json.Unmarshal(bytes, &obj)
if err != nil {
log.Fatal(err)
}
return obj
}
var examples = []string{
"",
"/",
"/ ",
"/abc",
"/def/",
"/g~1h",
"/i~0j",
"/wiki/links/0",
"/wiki/links/1",
"/wiki/links/2",
"/wiki/name",
"/no/such/thing",
"bad/pointer",
}
func main() {
doc := loadJSON("example.json")
for _, s := range examples {
p, err := NewJSONPointer(s)
if err != nil {
fmt.Printf("error: %v\n\n", err)
continue
}
if result, err := p.Resolve(doc); err != nil {
fmt.Printf("error: %v\n\n", err)
} else {
fmt.Printf("\"%s\" -> %s\n\n", p, prettyJSON(result))
}
}
}
- Output:
"" -> { "": "Rosetta", " ": "Code", "abc": [ "is", "a" ], "def": { "": "programming" }, "g/h": "chrestomathy", "i~j": "site", "wiki": { "links": [ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] } } "/" -> "Rosetta" "/ " -> "Code" "/abc" -> [ "is", "a" ] "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "https://rosettacode.org/wiki/Rosetta_Code" "/wiki/links/1" -> "https://discord.com/channels/1011262808001880065" error: "/wiki/links/2" does not exist error: "/wiki/name" does not exist error: "/no" does not exist error: "bad/pointer" pointers must start with a slash or be the empty string
JavaScript
ES2022
class JSONPointer {
#tokens;
constructor(pointer) {
this.#tokens = this.#parse(pointer);
}
resolve(data) {
return this.#tokens.reduce(this.#getItem.bind(this), data);
}
toString() {
return this.#encode(this.#tokens);
}
#parse(pointer) {
if (pointer.length && !pointer.startsWith("/")) {
throw new JSONPointerError(
`\"${pointer}\" pointers must start with a slash or be the empty string`
);
}
return pointer
.split("/")
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"))
.slice(1);
}
#getItem(obj, token, idx) {
// NOTE:
// - string primitives "have own" indices and `length`.
// - Arrays have a `length` property.
// - A property might exist with the value `undefined`.
// - obj[1] is equivalent to obj["1"].
if (
typeof obj === "object" &&
!(Array.isArray(obj) && token === "length") &&
Object.hasOwn(obj, token)
)
return obj[token];
throw new JSONPointerError(
`\"${this.#encode(this.#tokens.slice(0, idx + 1))}\" does not exist`
);
}
#encode(tokens) {
if (!tokens.length) return "";
return (
"/" +
tokens
.map((token) => token.replaceAll("~", "~0").replaceAll("/", "~1"))
.join("/")
);
}
}
class JSONPointerError extends Error {}
const doc = {
wiki: {
links: [
"https://rosettacode.org/wiki/Rosetta_Code",
"https://discord.com/channels/1011262808001880065",
],
},
"": "Rosetta",
" ": "Code",
"g/h": "chrestomathy",
"i~j": "site",
abc: ["is", "a"],
def: { "": "programming" },
};
const examples = [
"",
"/",
"/ ",
"/abc",
"/def/",
"/g~1h",
"/i~0j",
"/wiki/links/0",
"/wiki/links/1",
"/wiki/links/2",
"/wiki/name",
"/no/such/thing",
"bad/pointer",
];
for (const p of examples) {
try {
const pointer = new JSONPointer(p);
const result = pointer.resolve(doc);
console.log(`"${p}" -> ${JSON.stringify(result, undefined, 2)}\n`);
} catch (err) {
if (err instanceof JSONPointerError) {
console.log(`error: ${err.message}\n`);
} else {
throw err;
}
}
}
- Output:
"" -> { "wiki": { "links": [ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] }, "": "Rosetta", " ": "Code", "g/h": "chrestomathy", "i~j": "site", "abc": [ "is", "a" ], "def": { "": "programming" } } "/" -> "Rosetta" "/ " -> "Code" "/abc" -> [ "is", "a" ] "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "https://rosettacode.org/wiki/Rosetta_Code" "/wiki/links/1" -> "https://discord.com/channels/1011262808001880065" error: "/wiki/links/2" does not exist error: "/wiki/name" does not exist error: "/no" does not exist error: "bad/pointer" pointers must start with a slash or be the empty string
jq
Also works with gojq, the Go implementation of jq.
The approach taken here is to piggy-back off jq's support for "array paths", that is, JSON arrays of strings and integers representing paths in much the same way as JSON Pointer.
Note that jq's `getpath/1` cannot be used directly as it does not raise errors in the way required of JSON Pointer.
# JSON Pointer
# The characters '~' (%x7E) and '/' (%x2F) have special meanings in JSON Pointer,
# so '~' needs to be encoded as '~0'
# and '/' needs to be encoded as '~1'
# when these characters appear in a reference token.
# The JSON Pointer spec allows 0 both for indexing an array and for retrieving a key named "0".
# disambiguate($k) accordingly disambiguates $k w.r.t. `.`.
# $k should be a string or integer.
# If $k is a string and . is an object then: if has($k) then $k else null end.
# If $k is an integer and . is an array, then emit $k.
# If $k is an integer-valued string and . is an array then exit $k|tonumber.
# Otherwise emit null
def disambiguate( $k ):
if ($k|type) == "string"
then if type == "object" then $k
elif type == "array" and ($k|test("^[0-9]+$"))
then ($k|tonumber)
else null
end
elif ($k|type) == "number" and type == "array"
then $k
else null
end;
# $array should be an array of strings and integers.
# Emit the disambiguated array, suitable for running getpath/1.
# Emit null if disambiguation fails at any point.
def disambiguatePath($array):
. as $in
| reduce $array[] as $x ([];
if . then . as $path
| ($in | getpath($path) | disambiguate($x)) as $y
| if $y then . + [ $y ]
else null
end
else .
end);
# $p is an array as for getpath
def stepwisegetpath($p):
if ($p|length) == 0 then .
else $p[0] as $x
| if (type == "object" and ($x|type) == "string" and has($x))
or (type == "array" and ($x|type) == "number" and $x > -1 and $x < length)
then .[$x] | stepwisegetpath($p[1:])
else "JSON Pointer mismatch" | error
end
end;
# getjsonpointer() is like jq's getpath() but for jsonpointer pointers,
# and an error condition is raised if there is no value at the location.
def getjsonpointer($pointer):
if $pointer == "" then .
elif $pointer[:1] != "/" then "Invalid JSON Pointer: \($pointer)" | error
else . as $in
# first decode ~1, then ~0
| ($pointer | split("/") | .[1:]
| map(gsub("~1"; "/") | gsub("~0"; "~"))) as $array
| disambiguatePath($array) as $apath
| if $apath then stepwisegetpath($apath)
else "JSON Pointer disambiguation failed: \($pointer)" | error
end
end;
The Task
def check($p; $result):
if getjsonpointer($p) == $result
then empty
else "whoops: \($p)"
end;
def checkError($p):
(try getjsonpointer($p) catch nan)
| . as $result
| if isnan then empty
else "\($p) should have raised an error but yielded \($result)"
end;
check(""; .), # The entire input document.
check("/"; "Rosetta"),
check("/ "; "Code"),
check("/abc"; ["is", "a"]),
check("/def/"; "programming"),
check("/g~1h"; "chrestomathy"),
check("/i~0j"; "site"),
check("/wiki/links/0"; "https://rosettacode.org/wiki/Rosetta_Code"),
check("/wiki/links/1"; "https://discord.com/channels/1011262808001880065"),
checkError("/wiki/links/2"),
checkError("/wiki/name"),
checkError("/no/such/thing"),
checkError("bad/pointer")
- Output:
Nothing, by design.
Julia
""" rosettacode.org/wiki/JSON_pointer """
struct JSON_Pointer
tokens::Vector{String}
end
JSON_Pointer(s::AbstractString) = JSON_Pointer(jp_parse(s))
resolve(p::JSON_Pointer, data) = reduce(getitem, p.tokens, init=data)
function jp_unencode_join(p::JSON_Pointer)
isempty(p.tokens) && return ""
return "/" * mapreduce(x -> replace(replace(x, "~" => "~0"), "/" => "~1"),
(s1, s2) -> s1 * "/" * s2, p.tokens)
end
Base.print(io::IO, p::JSON_Pointer) = print(io, jp_unencode_join(p))
function jp_parse(s::AbstractString)
if isempty(s)
return String[]
elseif s[begin] != '/'
error("Non-empty JSON pointers must begin with /")
else
return map(x -> replace(replace(x, "~1" => "/"), "~0" => "~"),
split(s, "/"))[begin+1:end]
end
end
"""
NOTE:
- to keep with the JavaScript convention, arrays are 0-based
- string primitives "have own" indices and `length`.
- Arrays have a `length` property.
- A property might exist with the value `undefined`.
- obj[1] is equivalent to obj["1"].
"""
getitem(obj::Vector, token::Integer) = obj[token+1]
getitem(obj::Vector, token::AbstractString) = obj[parse(Int, token)+1]
getitem(obj::Dict, token) = obj[token]
const doc = Dict(
"wiki" => Dict(
"links" => [
"https://rosettacode.org/wiki/Rosetta_Code",
"https://discord.com/channels/1011262808001880065",
],
),
"" => "Rosetta",
" " => "Code",
"g/h" => "chrestomathy",
"i~j" => "site",
"abc" => ["is", "a"],
"def" => Dict("" => "programming"),
)
const examples = [
"",
"/",
"/ ",
"/abc",
"/def/",
"/g~1h",
"/i~0j",
"/wiki/links/0",
"/wiki/links/1",
"/wiki/links/2",
"/wiki/name",
"/no/such/thing",
"bad/pointer",
]
for ex in examples
try
pointer = JSON_Pointer(ex)
result = resolve(pointer, doc)
println("{$ex} -> {$result}")
catch y
println("Error: $ex does not exist: $y")
end
end
- Output:
{} -> {Dict{String, Any}("abc" => ["is", "a"], "wiki" => Dict("links" => ["https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065"]), " " => "Code", "i~j" => "site", "def" => Dict("" => "programming"), "" => "Rosetta", "g/h" => "chrestomathy")} {/} -> {Rosetta} {/ } -> {Code} {/abc} -> {["is", "a"]} {/def/} -> {programming} {/g~1h} -> {chrestomathy} {/i~0j} -> {site} {/wiki/links/0} -> {https://rosettacode.org/wiki/Rosetta_Code} {/wiki/links/1} -> {https://discord.com/channels/1011262808001880065} Error: /wiki/links/2 does not exist: BoundsError(["https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065"], (3,)) Error: /wiki/name does not exist: KeyError("name") Error: /no/such/thing does not exist: KeyError("no") Error: bad/pointer does not exist: ErrorException("Non-empty JSON pointers must begin with /")
Phix
Note that parse_json() in version 1.0.3 and earlier does not support unquoted keys or trailing commas, fixed in 1.0.4 but for now at least the dtxt constant below has a few additional quotes added and trailing commas removed.
with javascript_semantics include builtins\json.e function parseJSONPointer(string p) if p="" then return {} end if if p[1]!='/' then throw("pointers must start with a slash or be the empty string") end if return apply(true,substitute_all,{split(p,'/',false),{{"~1","~0"}},{{"/","~"}}})[2..$] end function function uri_percent_decode(string s) -- eg `/k%22l` -> `/k"l` sequence pc = find_all('%',s), rc = repeat(" ",length(pc)) for j,i in pc do integer nb1 = 0, nb2 = 0 if i<=length(s)-2 then nb1 = find(s[i+1],"0123456789ABCDEF") nb2 = find(s[i+2],"0123456789ABCDEF") end if if nb1=0 or nb2=0 then pc[j] = 0 else rc[j][1] = (nb1-1)*16+nb2-1 end if end for for i,j in reverse(pc) do if j then s[j..j+2] = rc[-i] end if end for return s end function function resolve(object json, sequence tokens) for t in tokens do integer jtype = iff(sequence(json) and length(json)?json[1]:0) if jtype=JSON_ARRAY then -- Note: we expect "JSON Pointers" to contain 0-based indices, -- but Phix indices are 1-based, //and// parse_json() returns -- sequences with first element of JSON_ARRAY/OBJECT/KEYWORD. integer k = to_integer(t,-1)+1 if k<1 or k>=length(json) then throw("bad index: "&t) end if json = json[k+1] elsif jtype=JSON_OBJECT then object jt = extract_json_field(json, t, {}) if jt={} then jt = extract_json_field(json,uri_percent_decode(t),{}) end if -- Aside: parse_json() returns {JSON_OBJECT} not {}, and -- likwise {JSON_ARRAY} to represent the empty array [], -- hence {} is a sound choice for the "not found" value. if jt={} then throw(t&": no such field") end if json = jt else -- native/keyword throw("non subscriptable: %v [%s]",{json,t}) end if end for return json end function constant dtxt = """ { "wiki": { "links": [ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] }, "": "Rosetta", " ": "Code", "g/h": "chrestomathy", "i~j": "site", "abc": ["is", "a"], "def": { "": "programming" } }""", doc = parse_json(dtxt) constant tests = { "", "/", "/ ", "/abc", "/def/", "/g~1h", -- or "/g%2Fh", "/i~0j", -- or "/i%7Ej", "/wiki/links/0", "/wiki/links/1", "/wiki/links/2", "/wiki/name", "/no/such/thing", "bad/pointer", } for p in tests do try sequence ptr = parseJSONPointer(p) object result = resolve(doc,ptr) printf(1,"%v -> %s\n",{p,print_json("",result)}) catch e printf(1,"%v => Error %s\n\n",{p,e[E_USER]}) end try end for
- Output:
"" -> { "wiki":{ "links":[ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] }, "":"Rosetta", " ":"Code", "g/h":"chrestomathy", "i~j":"site", "abc":[ "is", "a" ], "def":{ "":"programming" } } "/" -> "Rosetta" "/ " -> "Code" "/abc" -> [ "is", "a" ] "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "https://rosettacode.org/wiki/Rosetta_Code" "/wiki/links/1" -> "https://discord.com/channels/1011262808001880065" "/wiki/links/2" => Error bad index: 2 "/wiki/name" => Error name: no such field "/no/such/thing" => Error no: no such field "bad/pointer" => Error pointers must start with a slash or be the empty string
Python
""" rosettacode.org/wiki/JSON_pointer """
from functools import reduce
class JSONPointer:
""" datatracker.ietf.org/doc/html/rfc6901 """
def __init__(self, pstring):
""" create a JSON ponter from a string """
self.tokens = parse(pstring)
def resolve(self, data):
""" use the pointer on an object """
return reduce(get_item, self.tokens, data)
def encode(self):
""" output pointer in string form """
ret = ''
for tok in self.tokens:
ret = ret + '/' + tok.replace('~', '~0').replace('/', '~1')
return ret
def to_string(self):
""" output pointer in string form """
return self.encode()
def parse(pst):
""" tokenize a string for use as JSON pointer """
if pst == '':
return []
if pst[0] != '/':
raise SyntaxError('Non-empty JSON pointers must begin with /')
return [a.replace('~1', '/').replace('~0', '~') for a in pst.split('/')][1:]
def get_item(obj, token):
"""
NOTE:
- string primitives 'have own' indices and `length`.
- Arrays have a `length` property.
- A property might exist with the value `undefined`.
- obj[1] is equivalent to obj['1'].
"""
if isinstance(obj, list) and isinstance(token, str):
return obj[int(token)]
return obj[token]
if __name__ == '__main__':
DOC = {
'wiki': {
'links': [
'https://rosettacode.org/wiki/Rosetta_Code',
'https://discord.com/channels/1011262808001880065',
],
},
'': 'Rosetta',
' ': 'Code',
'g/h': 'chrestomathy',
'i~j': 'site',
'abc': ['is', 'a'],
'def': {'': 'programming'},
}
EXAMPLES = [
'',
'/',
'/ ',
'/abc',
'/def/',
'/g~1h',
'/i~0j',
'/wiki/links/0',
'/wiki/links/1',
'/wiki/links/2',
'/wiki/name',
'/no/such/thing',
'bad/pointer',
]
for exa in EXAMPLES:
try:
pointer = JSONPointer(exa)
result = pointer.resolve(DOC)
print(f'"{exa}" -> "{result}"')
except (SyntaxError, IndexError, KeyError) as error:
print(f'Error: {exa} does not exist: {error}')
- Output:
"" -> "{'wiki': {'links': ['https://rosettacode.org/wiki/Rosetta_Code', 'https://discord.com/channels/1011262808001880065']}, '': 'Rosetta', ' ': 'Code', 'g/h': 'chrestomathy', 'i~j': 'site', 'abc': ['is', 'a'], 'def': {'': 'programming'}}" "/" -> "Rosetta" "/ " -> "Code" "/abc" -> "['is', 'a']" "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "https://rosettacode.org/wiki/Rosetta_Code" "/wiki/links/1" -> "https://discord.com/channels/1011262808001880065" Error: /wiki/links/2 does not exist: list index out of range Error: /wiki/name does not exist: 'name' Error: /no/such/thing does not exist: 'no' Error: bad/pointer does not exist: Non-empty JSON pointers must begin with /
Rust
use serde_json::{ json, Value };
type JSONPointer = Vec<String>;
fn new_json_pointer(pstring: &str) -> Result<JSONPointer, String> {
return jp_parse(pstring);
}
fn resolve(p: JSONPointer, data: Value) -> Result<Value, String> {
return p.iter().fold(Ok(data), |accum, val| get_item(accum, val));
}
fn jp_parse(pst: &str) -> Result<Vec<String>, String> {
if pst == "" {
return Ok([].to_vec());
}
if pst.chars().nth(0).unwrap() != '/' {
return Err(String::from("Non-empty JSON pointers must begin with /"));
}
return Ok(
pst
.split("/")
.map(|s| String::from(s.replace("~1", "/").replace("~0", "~")))
.collect::<Vec<String>>()[1..]
.to_vec()
);
}
fn get_item<'a>(obj: Result<Value, String>, token: &str) -> Result<Value, String> {
match obj {
Err(_) => {
return obj; // propagate along
}
Ok(ob) => {
match ob {
Value::Array(arr) => {
let idx = usize::from_str_radix(token, 10);
match idx {
Err(..) => {
return Err(String::from("ParseIntErr"));
}
Ok(i) => {
if i < arr.len() {
return Ok(Value::String(arr[i].to_string()));
}
return Err(String::from(format!("Index {:?} out of range", token)));
}
}
}
Value::Object(dic) => {
if dic.contains_key(token) {
return Ok(dic[token].clone());
}
return Err(String::from(format!("Key error with {:?}", token)));
}
_ => {
return Err(String::from("Unknown object"));
}
}
}
}
}
fn main() {
let doc =
json!({
"wiki" : {
"links" : [
"https://rosettacode.org/wiki/Rosetta_Code",
"https://discord.com/channels/1011262808001880065",
],
},
"" : "Rosetta",
" " : "Code",
"g/h" : "chrestomathy",
"i~j" : "site",
"abc" : ["is", "a"],
"def" : {"" : "programming"},
});
let examples = [
"",
"/",
"/ ",
"/abc",
"/def/",
"/g~1h",
"/i~0j",
"/wiki/links/0",
"/wiki/links/1",
"/wiki/links/2",
"/wiki/name",
"/no/such/thing",
"bad/pointer",
];
for p in examples {
let jspointer = new_json_pointer(p);
match jspointer {
Err(error) => println!("JSON pointer creation error: {error}"),
Ok(pointer) => {
let result = resolve(pointer, doc.clone());
match result {
Ok(val) => println!("\"{p}\" -> {val}"),
Err(err) => println!("Error: {p} does not exist: {err}"),
}
}
}
}
}
- Output:
"" -> {"":"Rosetta"," ":"Code","abc":["is","a"],"def":{"":"programming"},"g/h":"chrestomathy","i~j":"site","wiki":{"links":["https://rosettacode.org/wiki/Rosetta_Code","https://discord.com/channels/1011262808001880065"]}} "/" -> "Rosetta" "/ " -> "Code" "/abc" -> ["is","a"] "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "\"https://rosettacode.org/wiki/Rosetta_Code\"" "/wiki/links/1" -> "\"https://discord.com/channels/1011262808001880065\"" Error: /wiki/links/2 does not exist: Index "2" out of range Error: /wiki/name does not exist: Key error with "name" Error: /no/such/thing does not exist: Key error with "no" JSON pointer creation error: Non-empty JSON pointers must begin with /
Wren
import "./json" for JSON
import "./iterate" for Indexed
var parseJSONPointer = Fn.new { |pointer|
var tokens = []
if (pointer == "") return [tokens, null]
if (!pointer.startsWith("/")) {
return [tokens, "pointers must start with a slash or be empty"]
}
for (token in pointer[1..-1].split("/")) {
tokens.add(token.replace("~1", "/").replace("~0", "~"))
}
return [tokens, null]
}
var getItem = Fn.new { |data, token|
if (data is List) {
var idx = Num.fromString(token)
if (idx && idx.isInteger && idx >= 0 && idx < data.count) {
return [data[idx], true]
}
} else if (data is Map) {
if (data.containsKey(token)) return [data[token], true]
}
return [null, false]
}
var encode = Fn.new { |tokens|
var encoded = tokens.map { |t| t.replace("~", "~0").replace("/", "~1") }
if (encoded.isEmpty) return ""
return "/" + encoded.join("/")
}
var resolve = Fn.new { |docMap, p|
var obj = docMap
for (se in Indexed.new(p)) {
var token = se.value
var item = getItem.call(obj, token)
if (!item[1]) {
return [null, "\"%(encode.call(p[0..se.index]))\" does not exist"]
}
obj = item[0]
}
return [obj, null]
}
var doc = """
{
"wiki": {
"links": [
"https://rosettacode.org/wiki/Rosetta_Code",
"https://discord.com/channels/1011262808001880065"
]
},
"": "Rosetta",
" ": "Code",
"g/h": "chrestomathy",
"i~j": "site",
"abc": ["is", "a"],
"def": { "": "programming" }
}
"""
var docMap = JSON.parse(doc)
var tests = [
"",
"/",
"/ ",
"/abc",
"/def/",
"/g~1h",
"/i~0j",
"/wiki/links/0",
"/wiki/links/1",
"/wiki/links/2",
"/wiki/name",
"/no/such/thing",
"bad/pointer"
]
for (test in tests) {
var data = parseJSONPointer.call(test)
test = "\"" + test + "\""
if (data[1]) {
System.print("error: %(test) %(data[1])")
} else {
var res = resolve.call(docMap, data[0])
if (res[1]) {
System.print("error: %(res[1])")
} else if (docMap == res[0]) {
System.print("%(test) -> %(doc)")
} else {
System.print("%(test) -> %(JSON.stringify(res[0]))")
}
}
System.print()
}
- Output:
"" -> { "wiki": { "links": [ "https://rosettacode.org/wiki/Rosetta_Code", "https://discord.com/channels/1011262808001880065" ] }, "": "Rosetta", " ": "Code", "g/h": "chrestomathy", "i~j": "site", "abc": ["is", "a"], "def": { "": "programming" } } "/" -> "Rosetta" "/ " -> "Code" "/abc" -> ["is","a"] "/def/" -> "programming" "/g~1h" -> "chrestomathy" "/i~0j" -> "site" "/wiki/links/0" -> "https://rosettacode.org/wiki/Rosetta_Code" "/wiki/links/1" -> "https://discord.com/channels/1011262808001880065" error: "/wiki/links/2" does not exist error: "/wiki/name" does not exist error: "/no" does not exist error: "bad/pointer" pointers must start with a slash or be empty