Skip to content

Commit

Permalink
⭐ add simple accessors for dicts (#1695)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
arlimus authored Sep 14, 2023
1 parent c228290 commit 7d60c4d
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 6 deletions.
19 changes: 17 additions & 2 deletions mqlc/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package mqlc

import (
"errors"
"regexp"
"strconv"

"go.mondoo.com/cnquery/llx"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
37 changes: 34 additions & 3 deletions mqlc/mqlc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:]
}
Expand Down
34 changes: 34 additions & 0 deletions mqlc/mqlc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion providers/os/resources/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 7d60c4d

Please sign in to comment.