From 7d60c4d25212a697814b3f55ad08f3a5af5e77da Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Thu, 14 Sep 2023 06:28:18 -0700 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=20add=20simple=20accessors=20for=20di?= =?UTF-8?q?cts=20(#1695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accessing dict structures sometimes feels a bit painful: ```coffee dict["one"]["more"]["field"] ``` Luckily for us, this pattern is well-known in the framework that birthed the scripting part of MQL: JS. With that in mind: ```coffee dict.one.more.field ``` is a much better way of expressing this use-case. Now: combine this with the power of GraphQL, and all of a sudden you get something like this: ```coffee dict { one { more.field } another } ``` Here is an example of using it in this repo: ```graphql { Name Version Connectors { Name Short } } ``` If we run it like this: ```bash cnquery run -c "parse.json('providers/os/dist/os.json').params{Name Version Connectors {Name Short}} --json" ``` and prettify the output: ![image](https://github.com/mondoohq/cnquery/assets/1307529/195090b7-4c4e-40aa-83a7-f1744252032e) --------- Signed-off-by: Dominik Richter --- mqlc/labels.go | 19 +++++++++++++++-- mqlc/mqlc.go | 37 ++++++++++++++++++++++++++++++--- mqlc/mqlc_test.go | 34 ++++++++++++++++++++++++++++++ providers/os/resources/parse.go | 2 +- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/mqlc/labels.go b/mqlc/labels.go index 9c747e086b..0050d14321 100644 --- a/mqlc/labels.go +++ b/mqlc/labels.go @@ -5,6 +5,7 @@ package mqlc import ( "errors" + "regexp" "strconv" "go.mondoo.com/cnquery/llx" @@ -73,12 +74,20 @@ func createLabel(res *llx.CodeBundle, ref uint64, schema llx.Schema) (string, er case types.Int: label = "[" + strconv.FormatInt(idx.(int64), 10) + "]" case types.String: - label = "[" + idx.(string) + "]" + if chunk.Function.Type == string(types.Dict) && isAccessor(idx.(string)) { + label = idx.(string) + } else { + label = "[" + idx.(string) + "]" + } default: panic("cannot label array index of type " + arg.Type.Label()) } if parentLabel != "" { - label = parentLabel + label + if label != "" && label[0] == '[' { + label = parentLabel + label + } else { + label = parentLabel + "." + label + } } case "{}", "${}": label = parentLabel @@ -101,6 +110,12 @@ func createLabel(res *llx.CodeBundle, ref uint64, schema llx.Schema) (string, er return stripCtlAndExtFromUnicode(label), nil } +var reAccessor = regexp.MustCompile(`^[\p{L}\d_]+$`) + +func isAccessor(s string) bool { + return reAccessor.MatchString(s) +} + // Unicode normalization and filtering, see http://blog.golang.org/normalization and // http://godoc.org/golang.org/x/text/unicode/norm for more details. func stripCtlAndExtFromUnicode(str string) string { diff --git a/mqlc/mqlc.go b/mqlc/mqlc.go index 9b6cb00074..5cdeca522e 100644 --- a/mqlc/mqlc.go +++ b/mqlc/mqlc.go @@ -1266,6 +1266,22 @@ func (c *compiler) compileIdentifier(id string, callBinding *variable, calls []* return restCalls, typ, err } + // Support easy accessors for dicts and maps, e.g: + // json.params { A.B.C } => json.params { _["A"]["B"]["C"] } + if callBinding != nil && callBinding.typ == types.Dict { + c.addChunk(&llx.Chunk{ + Call: llx.Chunk_FUNCTION, + Id: "[]", + Function: &llx.Function{ + Type: string(callBinding.typ), + Binding: callBinding.ref, + Args: []*llx.Primitive{llx.StringPrimitive(id)}, + }, + }) + c.standalone = false + return restCalls, callBinding.typ, err + } + // suggestions if callBinding == nil { addResourceSuggestions(c.Schema, id, c.Result) @@ -1510,11 +1526,26 @@ func (c *compiler) compileOperand(operand *parser.Operand) (*llx.Primitive, erro return nil, err } if !found { - addFieldSuggestions(availableFields(c, typ), id, c.Result) - return nil, errors.New("cannot find field '" + id + "' in " + typ.Label()) + if typ != types.Dict || !reAccessor.MatchString(id) { + addFieldSuggestions(availableFields(c, typ), id, c.Result) + return nil, errors.New("cannot find field '" + id + "' in " + typ.Label()) + } + + // Support easy accessors for dicts and maps, e.g: + // json.params.A.B.C => json.params["A"]["B"]["C"] + c.addChunk(&llx.Chunk{ + Call: llx.Chunk_FUNCTION, + Id: "[]", + Function: &llx.Function{ + Type: string(typ), + Binding: ref, + Args: []*llx.Primitive{llx.StringPrimitive(id)}, + }, + }) + } else { + typ = resType } - typ = resType if call != nil && len(calls) > 0 { calls = calls[1:] } diff --git a/mqlc/mqlc_test.go b/mqlc/mqlc_test.go index 3c1ded231f..da03b2fb7b 100644 --- a/mqlc/mqlc_test.go +++ b/mqlc/mqlc_test.go @@ -489,6 +489,40 @@ func TestCompiler_Props(t *testing.T) { }) } +func TestCompiler_Dict(t *testing.T) { + compileProps(t, "props.d.A.B", map[string]*llx.Primitive{ + "d": {Type: string(types.Dict)}, + }, func(res *llx.CodeBundle) { + assertProperty(t, "d", types.Dict, res.CodeV2.Blocks[0].Chunks[0]) + assert.Equal(t, []uint64{(1 << 32) | 3}, res.CodeV2.Entrypoints()) + assertFunction(t, "[]", &llx.Function{ + Type: string(types.Dict), + Binding: (1 << 32) | 1, + Args: []*llx.Primitive{llx.StringPrimitive("A")}, + }, res.CodeV2.Blocks[0].Chunks[1]) + assertFunction(t, "[]", &llx.Function{ + Type: string(types.Dict), + Binding: (1 << 32) | 2, + Args: []*llx.Primitive{llx.StringPrimitive("B")}, + }, res.CodeV2.Blocks[0].Chunks[2]) + assert.Equal(t, map[string]string{"d": string(types.Dict)}, res.Props) + }) + + compileProps(t, "props.d.A-1", map[string]*llx.Primitive{ + "d": {Type: string(types.Dict)}, + }, func(res *llx.CodeBundle) { + assertProperty(t, "d", types.Dict, res.CodeV2.Blocks[0].Chunks[0]) + assert.Equal(t, []uint64{(1 << 32) | 2, (1 << 32) | 3}, res.CodeV2.Entrypoints()) + assertFunction(t, "[]", &llx.Function{ + Type: string(types.Dict), + Binding: (1 << 32) | 1, + Args: []*llx.Primitive{llx.StringPrimitive("A")}, + }, res.CodeV2.Blocks[0].Chunks[1]) + assertPrimitive(t, llx.IntPrimitive(-1), res.CodeV2.Blocks[0].Chunks[2]) + assert.Equal(t, map[string]string{"d": string(types.Dict)}, res.Props) + }) +} + func TestCompiler_If(t *testing.T) { compileT(t, "if ( true ) { return 1 } else if ( false ) { return 2 } else { return 3 }", func(res *llx.CodeBundle) { assertFunction(t, "if", &llx.Function{ diff --git a/providers/os/resources/parse.go b/providers/os/resources/parse.go index 0e3b4fb9ec..5d8871d114 100644 --- a/providers/os/resources/parse.go +++ b/providers/os/resources/parse.go @@ -114,7 +114,7 @@ func (s *mqlParseJson) id() (string, error) { } func (s *mqlParseJson) content(file *mqlFile) (string, error) { - c := file.Content + c := file.GetContent() return c.Data, c.Error }