mirror of https://github.com/grafana/loki
LogQL: Simple JSON expressions (#3280)
* New approach, still rough Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding benchmark Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding tests Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Minor refactoring Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Appeasing the linter Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Further appeasing the linter Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding more tests Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding documentation Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Docs fixup Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Removing unnecessary condition Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding extra tests from suggestion in review Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding JSONParseErr Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding test to cover invalid JSON line Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding equivalent benchmarks for JSON and JSONExpression parsing Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding suffix if label would be overridden Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Reparenting jsonexpr directory to more appropriate location Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Setting empty label on non-matching expression, to retain parity with label_format Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Adding statement about returned complex JSON types Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Added check for valid label name Signed-off-by: Danny Kopping <danny.kopping@grafana.com> * Making json expressions shardable Signed-off-by: Danny Kopping <danny.kopping@grafana.com>pull/3321/head
parent
6c8fdd68aa
commit
feb7fb470b
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,13 @@ |
||||
package log |
||||
|
||||
type JSONExpression struct { |
||||
Identifier string |
||||
Expression string |
||||
} |
||||
|
||||
func NewJSONExpr(identifier, expression string) JSONExpression { |
||||
return JSONExpression{ |
||||
Identifier: identifier, |
||||
Expression: expression, |
||||
} |
||||
} |
@ -0,0 +1,56 @@ |
||||
// Inspired by https://github.com/sjjian/yacc-examples |
||||
|
||||
%{ |
||||
package jsonexpr |
||||
|
||||
func setScannerData(lex interface{}, data []interface{}) { |
||||
lex.(*Scanner).data = data |
||||
} |
||||
|
||||
%} |
||||
|
||||
%union { |
||||
empty struct{} |
||||
str string |
||||
field string |
||||
list []interface{} |
||||
int int |
||||
} |
||||
|
||||
%token<empty> DOT LSB RSB |
||||
%token<str> STRING |
||||
%token<field> FIELD |
||||
%token<int> INDEX |
||||
|
||||
%type<int> index index_access |
||||
%type<str> field key key_access |
||||
%type<list> values |
||||
|
||||
%% |
||||
|
||||
json: |
||||
values { setScannerData(JSONExprlex, $1) } |
||||
|
||||
values: |
||||
field { $$ = []interface{}{$1} } |
||||
| key_access { $$ = []interface{}{$1} } |
||||
| index_access { $$ = []interface{}{$1} } |
||||
| values key_access { $$ = append($1, $2) } |
||||
| values index_access { $$ = append($1, $2) } |
||||
| values DOT field { $$ = append($1, $3) } |
||||
; |
||||
|
||||
key_access: |
||||
LSB key RSB { $$ = $2 } |
||||
|
||||
index_access: |
||||
LSB index RSB { $$ = $2 } |
||||
|
||||
field: |
||||
FIELD { $$ = $1 } |
||||
|
||||
key: |
||||
STRING { $$ = $1 } |
||||
|
||||
index: |
||||
INDEX { $$ = $1 } |
@ -0,0 +1,517 @@ |
||||
// Code generated by goyacc -p JSONExpr -o pkg/logql/log/jsonexpr/jsonexpr.y.go pkg/logql/log/jsonexpr/jsonexpr.y. DO NOT EDIT.
|
||||
|
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:4
|
||||
package jsonexpr |
||||
|
||||
import __yyfmt__ "fmt" |
||||
|
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:4
|
||||
|
||||
func setScannerData(lex interface{}, data []interface{}) { |
||||
lex.(*Scanner).data = data |
||||
} |
||||
|
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:12
|
||||
type JSONExprSymType struct { |
||||
yys int |
||||
empty struct{} |
||||
str string |
||||
field string |
||||
list []interface{} |
||||
int int |
||||
} |
||||
|
||||
const DOT = 57346 |
||||
const LSB = 57347 |
||||
const RSB = 57348 |
||||
const STRING = 57349 |
||||
const FIELD = 57350 |
||||
const INDEX = 57351 |
||||
|
||||
var JSONExprToknames = [...]string{ |
||||
"$end", |
||||
"error", |
||||
"$unk", |
||||
"DOT", |
||||
"LSB", |
||||
"RSB", |
||||
"STRING", |
||||
"FIELD", |
||||
"INDEX", |
||||
} |
||||
|
||||
var JSONExprStatenames = [...]string{} |
||||
|
||||
const JSONExprEofCode = 1 |
||||
const JSONExprErrCode = 2 |
||||
const JSONExprInitialStackSize = 16 |
||||
|
||||
//line yacctab:1
|
||||
var JSONExprExca = [...]int{ |
||||
-1, 1, |
||||
1, -1, |
||||
-2, 0, |
||||
} |
||||
|
||||
const JSONExprPrivate = 57344 |
||||
|
||||
const JSONExprLast = 19 |
||||
|
||||
var JSONExprAct = [...]int{ |
||||
3, 13, 7, 14, 6, 6, 17, 16, 10, 7, |
||||
4, 15, 5, 8, 1, 9, 2, 11, 12, |
||||
} |
||||
|
||||
var JSONExprPact = [...]int{ |
||||
-3, -1000, 4, -1000, -1000, -1000, -1000, -6, -1000, -1000, |
||||
-4, 1, 0, -1000, -1000, -1000, -1000, -1000, |
||||
} |
||||
|
||||
var JSONExprPgo = [...]int{ |
||||
0, 18, 12, 0, 17, 10, 16, 14, |
||||
} |
||||
|
||||
var JSONExprR1 = [...]int{ |
||||
0, 7, 6, 6, 6, 6, 6, 6, 5, 2, |
||||
3, 4, 1, |
||||
} |
||||
|
||||
var JSONExprR2 = [...]int{ |
||||
0, 1, 1, 1, 1, 2, 2, 3, 3, 3, |
||||
1, 1, 1, |
||||
} |
||||
|
||||
var JSONExprChk = [...]int{ |
||||
-1000, -7, -6, -3, -5, -2, 8, 5, -5, -2, |
||||
4, -4, -1, 7, 9, -3, 6, 6, |
||||
} |
||||
|
||||
var JSONExprDef = [...]int{ |
||||
0, -2, 1, 2, 3, 4, 10, 0, 5, 6, |
||||
0, 0, 0, 11, 12, 7, 8, 9, |
||||
} |
||||
|
||||
var JSONExprTok1 = [...]int{ |
||||
1, |
||||
} |
||||
|
||||
var JSONExprTok2 = [...]int{ |
||||
2, 3, 4, 5, 6, 7, 8, 9, |
||||
} |
||||
|
||||
var JSONExprTok3 = [...]int{ |
||||
0, |
||||
} |
||||
|
||||
var JSONExprErrorMessages = [...]struct { |
||||
state int |
||||
token int |
||||
msg string |
||||
}{} |
||||
|
||||
//line yaccpar:1
|
||||
|
||||
/* parser for yacc output */ |
||||
|
||||
var ( |
||||
JSONExprDebug = 0 |
||||
JSONExprErrorVerbose = false |
||||
) |
||||
|
||||
type JSONExprLexer interface { |
||||
Lex(lval *JSONExprSymType) int |
||||
Error(s string) |
||||
} |
||||
|
||||
type JSONExprParser interface { |
||||
Parse(JSONExprLexer) int |
||||
Lookahead() int |
||||
} |
||||
|
||||
type JSONExprParserImpl struct { |
||||
lval JSONExprSymType |
||||
stack [JSONExprInitialStackSize]JSONExprSymType |
||||
char int |
||||
} |
||||
|
||||
func (p *JSONExprParserImpl) Lookahead() int { |
||||
return p.char |
||||
} |
||||
|
||||
func JSONExprNewParser() JSONExprParser { |
||||
return &JSONExprParserImpl{} |
||||
} |
||||
|
||||
const JSONExprFlag = -1000 |
||||
|
||||
func JSONExprTokname(c int) string { |
||||
if c >= 1 && c-1 < len(JSONExprToknames) { |
||||
if JSONExprToknames[c-1] != "" { |
||||
return JSONExprToknames[c-1] |
||||
} |
||||
} |
||||
return __yyfmt__.Sprintf("tok-%v", c) |
||||
} |
||||
|
||||
func JSONExprStatname(s int) string { |
||||
if s >= 0 && s < len(JSONExprStatenames) { |
||||
if JSONExprStatenames[s] != "" { |
||||
return JSONExprStatenames[s] |
||||
} |
||||
} |
||||
return __yyfmt__.Sprintf("state-%v", s) |
||||
} |
||||
|
||||
func JSONExprErrorMessage(state, lookAhead int) string { |
||||
const TOKSTART = 4 |
||||
|
||||
if !JSONExprErrorVerbose { |
||||
return "syntax error" |
||||
} |
||||
|
||||
for _, e := range JSONExprErrorMessages { |
||||
if e.state == state && e.token == lookAhead { |
||||
return "syntax error: " + e.msg |
||||
} |
||||
} |
||||
|
||||
res := "syntax error: unexpected " + JSONExprTokname(lookAhead) |
||||
|
||||
// To match Bison, suggest at most four expected tokens.
|
||||
expected := make([]int, 0, 4) |
||||
|
||||
// Look for shiftable tokens.
|
||||
base := JSONExprPact[state] |
||||
for tok := TOKSTART; tok-1 < len(JSONExprToknames); tok++ { |
||||
if n := base + tok; n >= 0 && n < JSONExprLast && JSONExprChk[JSONExprAct[n]] == tok { |
||||
if len(expected) == cap(expected) { |
||||
return res |
||||
} |
||||
expected = append(expected, tok) |
||||
} |
||||
} |
||||
|
||||
if JSONExprDef[state] == -2 { |
||||
i := 0 |
||||
for JSONExprExca[i] != -1 || JSONExprExca[i+1] != state { |
||||
i += 2 |
||||
} |
||||
|
||||
// Look for tokens that we accept or reduce.
|
||||
for i += 2; JSONExprExca[i] >= 0; i += 2 { |
||||
tok := JSONExprExca[i] |
||||
if tok < TOKSTART || JSONExprExca[i+1] == 0 { |
||||
continue |
||||
} |
||||
if len(expected) == cap(expected) { |
||||
return res |
||||
} |
||||
expected = append(expected, tok) |
||||
} |
||||
|
||||
// If the default action is to accept or reduce, give up.
|
||||
if JSONExprExca[i+1] != 0 { |
||||
return res |
||||
} |
||||
} |
||||
|
||||
for i, tok := range expected { |
||||
if i == 0 { |
||||
res += ", expecting " |
||||
} else { |
||||
res += " or " |
||||
} |
||||
res += JSONExprTokname(tok) |
||||
} |
||||
return res |
||||
} |
||||
|
||||
func JSONExprlex1(lex JSONExprLexer, lval *JSONExprSymType) (char, token int) { |
||||
token = 0 |
||||
char = lex.Lex(lval) |
||||
if char <= 0 { |
||||
token = JSONExprTok1[0] |
||||
goto out |
||||
} |
||||
if char < len(JSONExprTok1) { |
||||
token = JSONExprTok1[char] |
||||
goto out |
||||
} |
||||
if char >= JSONExprPrivate { |
||||
if char < JSONExprPrivate+len(JSONExprTok2) { |
||||
token = JSONExprTok2[char-JSONExprPrivate] |
||||
goto out |
||||
} |
||||
} |
||||
for i := 0; i < len(JSONExprTok3); i += 2 { |
||||
token = JSONExprTok3[i+0] |
||||
if token == char { |
||||
token = JSONExprTok3[i+1] |
||||
goto out |
||||
} |
||||
} |
||||
|
||||
out: |
||||
if token == 0 { |
||||
token = JSONExprTok2[1] /* unknown char */ |
||||
} |
||||
if JSONExprDebug >= 3 { |
||||
__yyfmt__.Printf("lex %s(%d)\n", JSONExprTokname(token), uint(char)) |
||||
} |
||||
return char, token |
||||
} |
||||
|
||||
func JSONExprParse(JSONExprlex JSONExprLexer) int { |
||||
return JSONExprNewParser().Parse(JSONExprlex) |
||||
} |
||||
|
||||
func (JSONExprrcvr *JSONExprParserImpl) Parse(JSONExprlex JSONExprLexer) int { |
||||
var JSONExprn int |
||||
var JSONExprVAL JSONExprSymType |
||||
var JSONExprDollar []JSONExprSymType |
||||
_ = JSONExprDollar // silence set and not used
|
||||
JSONExprS := JSONExprrcvr.stack[:] |
||||
|
||||
Nerrs := 0 /* number of errors */ |
||||
Errflag := 0 /* error recovery flag */ |
||||
JSONExprstate := 0 |
||||
JSONExprrcvr.char = -1 |
||||
JSONExprtoken := -1 // JSONExprrcvr.char translated into internal numbering
|
||||
defer func() { |
||||
// Make sure we report no lookahead when not parsing.
|
||||
JSONExprstate = -1 |
||||
JSONExprrcvr.char = -1 |
||||
JSONExprtoken = -1 |
||||
}() |
||||
JSONExprp := -1 |
||||
goto JSONExprstack |
||||
|
||||
ret0: |
||||
return 0 |
||||
|
||||
ret1: |
||||
return 1 |
||||
|
||||
JSONExprstack: |
||||
/* put a state and value onto the stack */ |
||||
if JSONExprDebug >= 4 { |
||||
__yyfmt__.Printf("char %v in %v\n", JSONExprTokname(JSONExprtoken), JSONExprStatname(JSONExprstate)) |
||||
} |
||||
|
||||
JSONExprp++ |
||||
if JSONExprp >= len(JSONExprS) { |
||||
nyys := make([]JSONExprSymType, len(JSONExprS)*2) |
||||
copy(nyys, JSONExprS) |
||||
JSONExprS = nyys |
||||
} |
||||
JSONExprS[JSONExprp] = JSONExprVAL |
||||
JSONExprS[JSONExprp].yys = JSONExprstate |
||||
|
||||
JSONExprnewstate: |
||||
JSONExprn = JSONExprPact[JSONExprstate] |
||||
if JSONExprn <= JSONExprFlag { |
||||
goto JSONExprdefault /* simple state */ |
||||
} |
||||
if JSONExprrcvr.char < 0 { |
||||
JSONExprrcvr.char, JSONExprtoken = JSONExprlex1(JSONExprlex, &JSONExprrcvr.lval) |
||||
} |
||||
JSONExprn += JSONExprtoken |
||||
if JSONExprn < 0 || JSONExprn >= JSONExprLast { |
||||
goto JSONExprdefault |
||||
} |
||||
JSONExprn = JSONExprAct[JSONExprn] |
||||
if JSONExprChk[JSONExprn] == JSONExprtoken { /* valid shift */ |
||||
JSONExprrcvr.char = -1 |
||||
JSONExprtoken = -1 |
||||
JSONExprVAL = JSONExprrcvr.lval |
||||
JSONExprstate = JSONExprn |
||||
if Errflag > 0 { |
||||
Errflag-- |
||||
} |
||||
goto JSONExprstack |
||||
} |
||||
|
||||
JSONExprdefault: |
||||
/* default state action */ |
||||
JSONExprn = JSONExprDef[JSONExprstate] |
||||
if JSONExprn == -2 { |
||||
if JSONExprrcvr.char < 0 { |
||||
JSONExprrcvr.char, JSONExprtoken = JSONExprlex1(JSONExprlex, &JSONExprrcvr.lval) |
||||
} |
||||
|
||||
/* look through exception table */ |
||||
xi := 0 |
||||
for { |
||||
if JSONExprExca[xi+0] == -1 && JSONExprExca[xi+1] == JSONExprstate { |
||||
break |
||||
} |
||||
xi += 2 |
||||
} |
||||
for xi += 2; ; xi += 2 { |
||||
JSONExprn = JSONExprExca[xi+0] |
||||
if JSONExprn < 0 || JSONExprn == JSONExprtoken { |
||||
break |
||||
} |
||||
} |
||||
JSONExprn = JSONExprExca[xi+1] |
||||
if JSONExprn < 0 { |
||||
goto ret0 |
||||
} |
||||
} |
||||
if JSONExprn == 0 { |
||||
/* error ... attempt to resume parsing */ |
||||
switch Errflag { |
||||
case 0: /* brand new error */ |
||||
JSONExprlex.Error(JSONExprErrorMessage(JSONExprstate, JSONExprtoken)) |
||||
Nerrs++ |
||||
if JSONExprDebug >= 1 { |
||||
__yyfmt__.Printf("%s", JSONExprStatname(JSONExprstate)) |
||||
__yyfmt__.Printf(" saw %s\n", JSONExprTokname(JSONExprtoken)) |
||||
} |
||||
fallthrough |
||||
|
||||
case 1, 2: /* incompletely recovered error ... try again */ |
||||
Errflag = 3 |
||||
|
||||
/* find a state where "error" is a legal shift action */ |
||||
for JSONExprp >= 0 { |
||||
JSONExprn = JSONExprPact[JSONExprS[JSONExprp].yys] + JSONExprErrCode |
||||
if JSONExprn >= 0 && JSONExprn < JSONExprLast { |
||||
JSONExprstate = JSONExprAct[JSONExprn] /* simulate a shift of "error" */ |
||||
if JSONExprChk[JSONExprstate] == JSONExprErrCode { |
||||
goto JSONExprstack |
||||
} |
||||
} |
||||
|
||||
/* the current p has no shift on "error", pop stack */ |
||||
if JSONExprDebug >= 2 { |
||||
__yyfmt__.Printf("error recovery pops state %d\n", JSONExprS[JSONExprp].yys) |
||||
} |
||||
JSONExprp-- |
||||
} |
||||
/* there is no state on the stack with an error shift ... abort */ |
||||
goto ret1 |
||||
|
||||
case 3: /* no shift yet; clobber input char */ |
||||
if JSONExprDebug >= 2 { |
||||
__yyfmt__.Printf("error recovery discards %s\n", JSONExprTokname(JSONExprtoken)) |
||||
} |
||||
if JSONExprtoken == JSONExprEofCode { |
||||
goto ret1 |
||||
} |
||||
JSONExprrcvr.char = -1 |
||||
JSONExprtoken = -1 |
||||
goto JSONExprnewstate /* try again in the same state */ |
||||
} |
||||
} |
||||
|
||||
/* reduction by production JSONExprn */ |
||||
if JSONExprDebug >= 2 { |
||||
__yyfmt__.Printf("reduce %v in:\n\t%v\n", JSONExprn, JSONExprStatname(JSONExprstate)) |
||||
} |
||||
|
||||
JSONExprnt := JSONExprn |
||||
JSONExprpt := JSONExprp |
||||
_ = JSONExprpt // guard against "declared and not used"
|
||||
|
||||
JSONExprp -= JSONExprR2[JSONExprn] |
||||
// JSONExprp is now the index of $0. Perform the default action. Iff the
|
||||
// reduced production is ε, $1 is possibly out of range.
|
||||
if JSONExprp+1 >= len(JSONExprS) { |
||||
nyys := make([]JSONExprSymType, len(JSONExprS)*2) |
||||
copy(nyys, JSONExprS) |
||||
JSONExprS = nyys |
||||
} |
||||
JSONExprVAL = JSONExprS[JSONExprp+1] |
||||
|
||||
/* consult goto table to find next state */ |
||||
JSONExprn = JSONExprR1[JSONExprn] |
||||
JSONExprg := JSONExprPgo[JSONExprn] |
||||
JSONExprj := JSONExprg + JSONExprS[JSONExprp].yys + 1 |
||||
|
||||
if JSONExprj >= JSONExprLast { |
||||
JSONExprstate = JSONExprAct[JSONExprg] |
||||
} else { |
||||
JSONExprstate = JSONExprAct[JSONExprj] |
||||
if JSONExprChk[JSONExprstate] != -JSONExprn { |
||||
JSONExprstate = JSONExprAct[JSONExprg] |
||||
} |
||||
} |
||||
// dummy call; replaced with literal code
|
||||
switch JSONExprnt { |
||||
|
||||
case 1: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:32
|
||||
{ |
||||
setScannerData(JSONExprlex, JSONExprDollar[1].list) |
||||
} |
||||
case 2: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:35
|
||||
{ |
||||
JSONExprVAL.list = []interface{}{JSONExprDollar[1].str} |
||||
} |
||||
case 3: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:36
|
||||
{ |
||||
JSONExprVAL.list = []interface{}{JSONExprDollar[1].str} |
||||
} |
||||
case 4: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:37
|
||||
{ |
||||
JSONExprVAL.list = []interface{}{JSONExprDollar[1].int} |
||||
} |
||||
case 5: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-2 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:38
|
||||
{ |
||||
JSONExprVAL.list = append(JSONExprDollar[1].list, JSONExprDollar[2].str) |
||||
} |
||||
case 6: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-2 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:39
|
||||
{ |
||||
JSONExprVAL.list = append(JSONExprDollar[1].list, JSONExprDollar[2].int) |
||||
} |
||||
case 7: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-3 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:40
|
||||
{ |
||||
JSONExprVAL.list = append(JSONExprDollar[1].list, JSONExprDollar[3].str) |
||||
} |
||||
case 8: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-3 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:44
|
||||
{ |
||||
JSONExprVAL.str = JSONExprDollar[2].str |
||||
} |
||||
case 9: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-3 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:47
|
||||
{ |
||||
JSONExprVAL.int = JSONExprDollar[2].int |
||||
} |
||||
case 10: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:50
|
||||
{ |
||||
JSONExprVAL.str = JSONExprDollar[1].field |
||||
} |
||||
case 11: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:53
|
||||
{ |
||||
JSONExprVAL.str = JSONExprDollar[1].str |
||||
} |
||||
case 12: |
||||
JSONExprDollar = JSONExprS[JSONExprpt-1 : JSONExprpt+1] |
||||
//line pkg/logql/log/jsonexpr/jsonexpr.y:56
|
||||
{ |
||||
JSONExprVAL.int = JSONExprDollar[1].int |
||||
} |
||||
} |
||||
goto JSONExprstack /* stack new state and value */ |
||||
} |
@ -0,0 +1,131 @@ |
||||
package jsonexpr |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestJSONExpressionParser(t *testing.T) { |
||||
// {"app":"foo","field with space":"value","field with ÜFT8👌":true,"namespace":"prod","pod":{"uuid":"foo","deployment":{"ref":"foobar", "params": [{"param": true},2,3]}}}
|
||||
|
||||
tests := []struct { |
||||
name string |
||||
expression string |
||||
want []interface{} |
||||
error error |
||||
}{ |
||||
{ |
||||
"single field", |
||||
"app", |
||||
[]interface{}{"app"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"top-level field with spaces", |
||||
`["field with space"]`, |
||||
[]interface{}{"field with space"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"top-level field with UTF8", |
||||
`["field with ÜFT8👌"]`, |
||||
[]interface{}{"field with ÜFT8👌"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"top-level array access", |
||||
`[0]`, |
||||
[]interface{}{0}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"nested field", |
||||
`pod.uuid`, |
||||
[]interface{}{"pod", "uuid"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"nested field alternate syntax", |
||||
`pod["uuid"]`, |
||||
[]interface{}{"pod", "uuid"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"nested field alternate syntax 2", |
||||
`["pod"]["uuid"]`, |
||||
[]interface{}{"pod", "uuid"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"array access", |
||||
`pod.deployment.params[0]`, |
||||
[]interface{}{"pod", "deployment", "params", 0}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"multi-level array access", |
||||
`pod.deployment.params[0].param`, |
||||
[]interface{}{"pod", "deployment", "params", 0, "param"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"multi-level array access alternate syntax", |
||||
`pod.deployment.params[0]["param"]`, |
||||
[]interface{}{"pod", "deployment", "params", 0, "param"}, |
||||
nil, |
||||
}, |
||||
{ |
||||
"empty", |
||||
``, |
||||
nil, |
||||
nil, |
||||
}, |
||||
|
||||
{ |
||||
"invalid field access", |
||||
`field with space`, |
||||
nil, |
||||
fmt.Errorf("syntax error: unexpected FIELD"), |
||||
}, |
||||
{ |
||||
"missing opening square bracket", |
||||
`"pod"]`, |
||||
nil, |
||||
fmt.Errorf("syntax error: unexpected STRING, expecting LSB or FIELD"), |
||||
}, |
||||
{ |
||||
"missing closing square bracket", |
||||
`["pod"`, |
||||
nil, |
||||
fmt.Errorf("syntax error: unexpected $end, expecting RSB"), |
||||
}, |
||||
{ |
||||
"missing closing square bracket", |
||||
`["pod""deployment"]`, |
||||
nil, |
||||
fmt.Errorf("syntax error: unexpected STRING, expecting RSB"), |
||||
}, |
||||
{ |
||||
"invalid nesting", |
||||
`pod..uuid`, |
||||
nil, |
||||
fmt.Errorf("syntax error: unexpected DOT, expecting FIELD"), |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
parsed, err := Parse(tt.expression, false) |
||||
|
||||
require.Equal(t, tt.want, parsed) |
||||
|
||||
if tt.error == nil { |
||||
return |
||||
} |
||||
|
||||
require.NotNil(t, err) |
||||
require.Equal(t, tt.error.Error(), err.Error()) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,164 @@ |
||||
package jsonexpr |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"io" |
||||
"strconv" |
||||
"text/scanner" |
||||
) |
||||
|
||||
type Scanner struct { |
||||
buf *bufio.Reader |
||||
data []interface{} |
||||
err error |
||||
debug bool |
||||
} |
||||
|
||||
func NewScanner(r io.Reader, debug bool) *Scanner { |
||||
return &Scanner{ |
||||
buf: bufio.NewReader(r), |
||||
debug: debug, |
||||
} |
||||
} |
||||
|
||||
func (sc *Scanner) Error(s string) { |
||||
sc.err = fmt.Errorf(s) |
||||
fmt.Printf("syntax error: %s\n", s) |
||||
} |
||||
|
||||
func (sc *Scanner) Reduced(rule, state int, lval *JSONExprSymType) bool { |
||||
if sc.debug { |
||||
fmt.Printf("rule: %v; state %v; lval: %v\n", rule, state, lval) |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (sc *Scanner) Lex(lval *JSONExprSymType) int { |
||||
return sc.lex(lval) |
||||
} |
||||
|
||||
func (sc *Scanner) lex(lval *JSONExprSymType) int { |
||||
for { |
||||
r := sc.read() |
||||
|
||||
if r == 0 { |
||||
return 0 |
||||
} |
||||
if isWhitespace(r) { |
||||
continue |
||||
} |
||||
|
||||
if isDigit(r) { |
||||
sc.unread() |
||||
val, err := sc.scanInt() |
||||
if err != nil { |
||||
sc.err = fmt.Errorf(err.Error()) |
||||
return 0 |
||||
} |
||||
|
||||
lval.int = val |
||||
return INDEX |
||||
} |
||||
|
||||
switch true { |
||||
case r == '[': |
||||
return LSB |
||||
case r == ']': |
||||
return RSB |
||||
case r == '.': |
||||
return DOT |
||||
case isIdentifier(r): |
||||
sc.unread() |
||||
lval.field = sc.scanField() |
||||
return FIELD |
||||
case r == '"': |
||||
sc.unread() |
||||
lval.str = sc.scanStr() |
||||
return STRING |
||||
default: |
||||
sc.err = fmt.Errorf("unexpected char %c", r) |
||||
return 0 |
||||
} |
||||
} |
||||
} |
||||
|
||||
func isIdentifier(r rune) bool { |
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' |
||||
} |
||||
|
||||
func (sc *Scanner) scanField() string { |
||||
var str []rune |
||||
|
||||
for { |
||||
r := sc.read() |
||||
if !isIdentifier(r) { |
||||
sc.unread() |
||||
break |
||||
} |
||||
|
||||
if r == '.' || r == scanner.EOF || r == rune(0) { |
||||
sc.unread() |
||||
break |
||||
} |
||||
|
||||
str = append(str, r) |
||||
} |
||||
return string(str) |
||||
} |
||||
|
||||
func (sc *Scanner) scanStr() string { |
||||
var str []rune |
||||
//begin with ", end with "
|
||||
r := sc.read() |
||||
if r != '"' { |
||||
sc.err = fmt.Errorf("unexpected char %c", r) |
||||
return "" |
||||
} |
||||
|
||||
for { |
||||
r := sc.read() |
||||
if r == '"' || r == ']' { |
||||
break |
||||
} |
||||
str = append(str, r) |
||||
} |
||||
return string(str) |
||||
} |
||||
|
||||
func (sc *Scanner) scanInt() (int, error) { |
||||
var number []rune |
||||
|
||||
for { |
||||
r := sc.read() |
||||
if r == '.' && len(number) > 0 { |
||||
return 0, fmt.Errorf("cannot use float as array index") |
||||
} |
||||
|
||||
if isWhitespace(r) || r == '.' || r == ']' { |
||||
sc.unread() |
||||
break |
||||
} |
||||
|
||||
if !isDigit(r) { |
||||
return 0, fmt.Errorf("non-integer value: %c", r) |
||||
} |
||||
|
||||
number = append(number, r) |
||||
} |
||||
|
||||
return strconv.Atoi(string(number)) |
||||
} |
||||
|
||||
func (sc *Scanner) read() rune { |
||||
ch, _, _ := sc.buf.ReadRune() |
||||
return ch |
||||
} |
||||
|
||||
func (sc *Scanner) unread() { _ = sc.buf.UnreadRune() } |
||||
|
||||
func isWhitespace(ch rune) bool { return ch == ' ' || ch == '\t' || ch == '\n' } |
||||
|
||||
func isDigit(r rune) bool { |
||||
return r >= '0' && r <= '9' |
||||
} |
@ -0,0 +1,19 @@ |
||||
package jsonexpr |
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
func init() { |
||||
JSONExprErrorVerbose = true |
||||
} |
||||
|
||||
func Parse(expr string, debug bool) ([]interface{}, error) { |
||||
s := NewScanner(strings.NewReader(expr), debug) |
||||
JSONExprParse(s) |
||||
|
||||
if s.err != nil { |
||||
return nil, s.err |
||||
} |
||||
return s.data, nil |
||||
} |
Loading…
Reference in new issue