Something went wrong. Try again.
A PDS proxy
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224//// RFC 6901 — JavaScript Object Notation (JSON) Pointer.//////// A pointer identifies a single value within a JSON document. Reference tokens//// are separated by `/`; within a token `~1` encodes `/` and `~0` encodes `~`.//// Evaluation resolves against a `gleam/dynamic.Dynamic` (as produced by//// `gleam/json`'s `parse`), descending object members by name and array//// elements by zero-based index.
import gleam/dictimport gleam/dynamic.{type Dynamic}import gleam/dynamic/decodeimport gleam/intimport gleam/jsonimport gleam/listimport gleam/option.{type Option, None, Some}import gleam/resultimport gleam/stringimport gleam/uri
/// A parsed JSON Pointer: an ordered list of decoded reference tokens. The empty/// list is the pointer `""`, which references the whole document.pub opaque type Pointer { Pointer(tokens: List(String))}
pub type Error { /// The pointer string does not conform to RFC 6901 syntax. InvalidSyntax(String) /// The pointer is valid but does not resolve to a value in the document. NotFound(token: String)}
// --- construction -----------------------------------------------------------
/// Parse a JSON Pointer in its JSON string form (Section 5), e.g. `"/foo/0"`./// The empty string is the whole-document pointer.pub fn parse(pointer: String) -> Result(Pointer, Error) { case pointer { "" -> Ok(Pointer([])) "/" <> rest -> split_and_decode(rest, percent: False) _ -> Error(InvalidSyntax("a non-empty pointer must start with '/'")) }}
/// Parse a JSON Pointer in its URI fragment form (Section 6), e.g. `"#/foo/0"`./// A leading `#` is optional.pub fn parse_fragment(fragment: String) -> Result(Pointer, Error) { let body = case fragment { "#" <> rest -> rest _ -> fragment } case body { "" -> Ok(Pointer([])) "/" <> rest -> split_and_decode(rest, percent: True) _ -> Error(InvalidSyntax("a non-empty fragment pointer must start with '/'")) }}
/// Build a pointer from already-decoded reference tokens.pub fn from_tokens(tokens: List(String)) -> Pointer { Pointer(tokens)}
/// The decoded reference tokens of a pointer.pub fn tokens(pointer: Pointer) -> List(String) { pointer.tokens}
// `rest` is everything after the leading `/`; splitting on `/` yields the// reference tokens (still escaped), at least one because of the leading `/`.fn split_and_decode( rest: String, percent percent: Bool,) -> Result(Pointer, Error) { string.split(rest, "/") |> list.try_map(fn(raw) { decode_token(raw, percent) }) |> result.map(Pointer)}
fn decode_token(raw: String, percent: Bool) -> Result(String, Error) { use decoded <- result.try(case percent { True -> uri.percent_decode(raw) |> result.replace_error(InvalidSyntax("invalid percent-encoding: " <> raw)) False -> Ok(raw) }) use _ <- result.try(check_escaping(decoded)) // Unescape `~1` -> `/` first, then `~0` -> `~` (order matters, Section 4). Ok( decoded |> string.replace("~1", "/") |> string.replace("~0", "~"), )}
// Every `~` must be part of a `~0` or `~1` escape sequence.fn check_escaping(token: String) -> Result(Nil, Error) { let stripped = token |> string.replace("~0", "") |> string.replace("~1", "") case string.contains(stripped, "~") { True -> Error(InvalidSyntax("invalid '~' escape in token: " <> token)) False -> Ok(Nil) }}
// --- serialization ----------------------------------------------------------
/// Render a pointer in its JSON string form (Section 5).pub fn to_string(pointer: Pointer) -> String { pointer.tokens |> list.map(fn(t) { "/" <> escape_token(t) }) |> string.concat}
/// Render a pointer in its URI fragment form (Section 6).pub fn to_fragment(pointer: Pointer) -> String { let body = pointer.tokens |> list.map(fn(t) { "/" <> uri.percent_encode(escape_token(t)) }) |> string.concat "#" <> body}
// Escape a raw token for the string form: `~` -> `~0` first, then `/` -> `~1`.fn escape_token(token: String) -> String { token |> string.replace("~", "~0") |> string.replace("/", "~1")}
// --- evaluation -------------------------------------------------------------
/// Evaluate a pointer against a parsed JSON value (Section 4), returning the/// referenced value. Descends object members by name and array elements by/// zero-based index; `-` (the position after the last element) and any missing/// member / out-of-range index fail with `NotFound`.pub fn get(document: Dynamic, pointer: Pointer) -> Result(Dynamic, Error) { descend(document, pointer.tokens)}
/// Parse a JSON document and a pointer string, then evaluate.pub fn get_from_string( document: String, pointer: String,) -> Result(Dynamic, Error) { use ptr <- result.try(parse(pointer)) use doc <- result.try( json.parse(document, decode.dynamic) |> result.replace_error(InvalidSyntax("invalid JSON document")), ) get(doc, ptr)}
fn descend(current: Dynamic, tokens: List(String)) -> Result(Dynamic, Error) { case tokens { [] -> Ok(current) [token, ..rest] -> { use next <- result.try(step(current, token)) descend(next, rest) } }}
fn step(current: Dynamic, token: String) -> Result(Dynamic, Error) { case decode.run(current, decode.dict(decode.string, decode.dynamic)) { Ok(object) -> dict.get(object, token) |> result.replace_error(NotFound(token)) Error(_) -> case decode.run(current, decode.list(decode.dynamic)) { Ok(array) -> index_array(array, token) // A scalar (string / number / bool / null) has no children. Error(_) -> Error(NotFound(token)) } }}
fn index_array(array: List(Dynamic), token: String) -> Result(Dynamic, Error) { case array_index(token) { // `-` references the (nonexistent) element after the last one. Ok(None) -> Error(NotFound("-")) Ok(Some(index)) -> list.drop(array, index) |> list.first |> result.replace_error(NotFound(token)) Error(e) -> Error(e) }}
// Parse an array index per Section 4: `0`, or digits without a leading zero;// `-` is the special after-last position. Anything else is invalid.fn array_index(token: String) -> Result(Option(Int), Error) { case token { "-" -> Ok(None) "0" -> Ok(Some(0)) _ -> case is_index(token) { True -> int.parse(token) |> result.map(Some) |> result.replace_error(NotFound(token)) False -> Error(NotFound(token)) } }}
fn is_index(token: String) -> Bool { case string.to_graphemes(token) { [] -> False // multi-digit indices may not have a leading zero ["0", ..] -> False chars -> list.all(chars, is_digit) }}
fn is_digit(grapheme: String) -> Bool { case grapheme { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> True _ -> False }}