// Port of sqlglot/jsonpath.py
///|
priv struct JsonPathParser {
path : String
tokens : Array[Token]
var_tokens : Array[TokenType]
mut i : Int
}
///|
fn JsonPathParser::curr(self : JsonPathParser) -> TokenType? {
if self.i < self.tokens.length() {
Some(self.tokens[self.i].token_type)
} else {
None
}
}
///|
fn JsonPathParser::prev(self : JsonPathParser) -> Token {
self.tokens[self.i - 1]
}
///|
fn JsonPathParser::advance(self : JsonPathParser) -> Token {
self.i += 1
self.prev()
}
///|
fn JsonPathParser::error(self : JsonPathParser, msg : String) -> String {
"\{msg} at index \{self.i}: \{self.path}"
}
///|
fn JsonPathParser::match_(self : JsonPathParser, tt : TokenType) -> Token? {
if self.curr() == Some(tt) {
return Some(self.advance())
}
None
}
///|
fn JsonPathParser::match_set(
self : JsonPathParser,
types : Array[TokenType],
) -> Token? {
match self.curr() {
Some(t) if types.contains(t) => Some(self.advance())
_ => None
}
}
///|
/// Literal results: Str(text) | Node(expr) | Int(n) | Bool(false)
fn JsonPathParser::parse_literal(
self : JsonPathParser,
) -> Value raise SqlglotError {
match self.match_(STRING) {
Some(t) => return Str(t.text)
None => ()
}
match self.match_(IDENTIFIER) {
Some(t) => return Str(t.text)
None => ()
}
if self.match_(STAR) is Some(_) {
return Node(mk0(JSONPathWildcard))
}
if self.match_(PLACEHOLDER) is Some(_) || self.match_(L_PAREN) is Some(_) {
let script = self.prev().text == "("
let start = self.i
while true {
if self.match_(L_BRACKET) is Some(_) {
self.parse_bracket() |> ignore
}
match self.curr() {
Some(R_BRACKET) | None => break
_ => ()
}
self.advance() |> ignore
}
let kind = if script { JSONPathScript } else { JSONPathFilter }
let size = self.tokens.length()
let end = if self.i < size {
self.tokens[self.i].end
} else {
self.tokens[size - 1].end
}
return Node(mk1(kind, substr(self.path, self.tokens[start].start, end)))
}
let mut number = if self.match_(DASH) is Some(_) { "-" } else { "" }
match self.match_(NUMBER) {
Some(t) => number += t.text
None => ()
}
if !number.is_empty() {
// Python `int(number)`: a ValueError (not a ParseError) for non-integers.
return match parse_int_checked(number) {
Some(n) => Int(n)
None =>
raise ValueError(
"invalid literal for int() with base 10: \{py_repr_str(number)}",
)
}
}
Bool(false)
}
///|
fn JsonPathParser::parse_slice(
self : JsonPathParser,
) -> Value raise SqlglotError {
let start = self.parse_literal()
let end : Value? = if self.match_(COLON) is Some(_) {
Some(self.parse_literal())
} else {
None
}
let step : Value? = if self.match_(COLON) is Some(_) {
Some(self.parse_literal())
} else {
None
}
if end is None && step is None {
return start
}
Node(mk(JSONPathSlice, [("start", start), ("end", end), ("step", step)]))
}
///|
fn JsonPathParser::parse_bracket(
self : JsonPathParser,
) -> Expr raise SqlglotError {
let literal = self.parse_slice()
if literal is Bool(false) {
raise ParseError(self.error("Cannot have empty segment"), [])
}
let indexes = [literal]
while self.match_(COMMA) is Some(_) {
let literal = self.parse_slice()
if !(literal is Bool(false)) {
indexes.push(literal)
}
}
let node = if indexes.length() == 1 {
match indexes[0] {
Str(s) => mk1(JSONPathKey, s)
Node(n) if n.kind.is_any([JSONPathScript, JSONPathFilter]) =>
mk1(JSONPathSelector, n)
v => mk1(JSONPathSubscript, v)
}
} else {
mk(JSONPathUnion, [("expressions", indexes)])
}
if self.match_(R_BRACKET) is None {
raise ParseError(self.error("Expected \{TokenType::R_BRACKET}"), [])
}
node
}
///|
fn JsonPathParser::parse_var_text(self : JsonPathParser) -> String {
let prev_index = self.i - 2
while self.match_set(self.var_tokens) is Some(_) {
()
}
let start = if prev_index < 0 { 0 } else { self.tokens[prev_index].end + 1 }
if self.i >= self.tokens.length() {
substr(self.path, start, py_len(self.path))
} else {
substr(self.path, start, self.tokens[self.i].start)
}
}
///|
/// Takes in a JSON path string and parses it into a JSONPath expression.
pub fn parse_json_path(
path : String,
dialect : Dialect,
) -> Expr raise SqlglotError {
let tokenizer = Tokenizer::new(
dialect.jsonpath_tokenizer,
numbers_can_be_underscore_separated=dialect.cfg.numbers_can_be_underscore_separated,
identifiers_can_start_with_digit=dialect.cfg.identifiers_can_start_with_digit,
unescaped_sequences=dialect.cfg.unescaped_sequences,
)
let p : JsonPathParser = {
path,
tokens: tokenizer.tokenize(path),
var_tokens: dialect.jsonpath_var_tokens,
i: 0,
}
p.match_(DOLLAR) |> ignore
let expressions = [mk0(JSONPathRoot)]
while p.curr() is Some(_) {
if p.match_(DOT) is Some(_) || p.match_(COLON) is Some(_) {
let recursive = p.prev().text == ".."
let value : Value? = if p.match_set(p.var_tokens) is Some(_) {
Some(Str(p.parse_var_text()))
} else if p.match_(IDENTIFIER) is Some(_) {
Some(Str(p.prev().text))
} else if p.match_(STAR) is Some(_) {
Some(Node(mk0(JSONPathWildcard)))
} else {
None
}
if recursive {
expressions.push(mk1(JSONPathRecursive, value))
} else if value is Some(v) && v.truthy() {
expressions.push(mk1(JSONPathKey, v))
} else if !dialect.cfg.json_path_single_dot_is_wildcard {
raise ParseError(p.error("Expected key name or * after DOT"), [])
}
} else if p.match_(L_BRACKET) is Some(_) {
expressions.push(p.parse_bracket())
} else if p.match_set(p.var_tokens) is Some(_) {
expressions.push(mk1(JSONPathKey, p.parse_var_text()))
} else if p.match_(IDENTIFIER) is Some(_) {
expressions.push(mk1(JSONPathKey, p.prev().text))
} else if p.match_(STAR) is Some(_) {
expressions.push(mk0(JSONPathWildcard))
} else {
raise ParseError(p.error("Unexpected \{p.tokens[p.i].token_type}"), [])
}
}
mk(JSONPath, [("expressions", expressions)])
}
///|
/// `Dialect.to_json_path(path)`: parses string/number literals into JSONPath expressions.
pub fn Dialect::to_json_path(
self : Dialect,
path : Expr?,
) -> Expr? raise SqlglotError {
match self.hooks.to_json_path {
Some(f) => f(self, path)
None => self.to_json_path_base(path)
}
}
///|
/// The base `Dialect.to_json_path` implementation.
pub fn Dialect::to_json_path_base(
self : Dialect,
path : Expr?,
) -> Expr? raise SqlglotError {
match path {
Some(p) if p.kind == Literal => {
let mut path_text = p.name()
if p.is_number() {
path_text = "[\{path_text}]"
}
Some(parse_json_path(path_text, self)) catch {
// Python only catches ParseError and TokenError here
e if !(e is (ParseError(_) | TokenError(_))) => raise e
e => {
let stripped = path_text.trim_start().to_string()
if self.cfg.strict_json_path_syntax &&
!(stripped.has_prefix("lax") || stripped.has_prefix("strict")) {
log_warning("Invalid JSON path syntax. \{e.message()}")
}
path
}
}
}
_ => path
}
}