Skip to content

Commit

Permalink
Prototype map.inner_join function
Browse files Browse the repository at this point in the history
  • Loading branch information
ptodev committed Oct 17, 2024
1 parent bbcfa11 commit eb6820f
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 2 deletions.
35 changes: 35 additions & 0 deletions docs/sources/reference/stdlib/map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
canonical: https://grafana.com/docs/alloy/latest/reference/stdlib/map/
description: Learn about map functions
aliases:
- ./concat/ # /docs/alloy/latest/reference/stdlib/concat/
menuTitle: map
title: map
---

# map

The `map` namespace contains functions related to objects.

## map.inner_join

The `map.inner_join` function allows you to join two arrays containing maps if certain keys have matching values in both maps.
It takes several inputs, in this order:

1. An array of maps (objects) which will be joined. The keys of the map are strings.
Its value could have any Alloy type such as a string, integer, map, or a capsule.
2. An array of maps (objects) which will be joined. The keys of the map are strings.
Its value could have any Alloy type such as a string, integer, map, or a capsule.
3. An array containing strings. The strings are the keys whose value has to match for maps to be joined.
If the set of keys don't identify a map uniquely, the resulting output may contain more maps than the total sum of maps from both input arrays.
4. (optional; default: `"update"`) A merge strategy. It describes which value will be used if there is Can be set to either of:
* `update`: If there is already a key with the same name in the first map, it will be updated with the value from the second map.
* `none`: If both maps have different values for the same key, no such key will be present in the output map.
5. (optional; default: `[]`) An array containing strings. Only keys listed in the array will be present in the output map.
6. (optional; default: `[]`) An array containing strings. Keyls listed in the array will not be present in the output map.

### Examples

```alloy
map.inner_join(prometheus.exporter.unix.default.targets, [{"instance"="1.1.1.1", "testLabelKey" = "testLabelVal", "BadKey" = "BadVal"}], ["instance"], "update", [], ["BadKey"])
```
181 changes: 181 additions & 0 deletions syntax/internal/stdlib/stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var Identifiers = map[string]interface{}{
"sys": sys,
"convert": convert,
"array": array,
"map": mapFuncs,
"encoding": encoding,
"string": str,
"file": file,
Expand Down Expand Up @@ -89,6 +90,10 @@ var array = map[string]interface{}{
"concat": concat,
}

var mapFuncs = map[string]interface{}{
"inner_join": innerJoin,
}

var convert = map[string]interface{}{
"nonsensitive": nonSensitive,
}
Expand Down Expand Up @@ -146,6 +151,182 @@ var concat = value.RawFunction(func(funcValue value.Value, args ...value.Value)
return value.Array(raw...), nil
})

func shouldJoinOld(left map[string]value.Value, right map[string]value.Value, conditions []string) bool {
for _, c := range conditions {
if !left[c].Equal(right[c]) {
return false
}
}
return true
}

func concatMapsOld(left map[string]value.Value, right map[string]value.Value) map[string]value.Value {
res := make(map[string]value.Value)
for k, v := range left {
res[k] = v
}
for k, v := range right {
res[k] = v
}
return res
}

// We can assume that conditions is an value.TypeArray,
// because it is checked in the innerJoin function.
func shouldJoin(left, right value.Value, conditions value.Value) bool {
for i := 0; i < conditions.Len(); i++ {
c := conditions.Index(i)
// if c.Type() != value.TypeString {
// //TODO: Throw error
// panic("")
// }
condition := c.Text()

// if left.Type() != value.TypeArray || right.Type() != value.TypeArray {
// //TODO: Throw error
// panic("")
// }

leftVal, ok := left.Key(condition)
if !ok {
//TODO: Throw error
panic("")
}

rightVal, ok := right.Key(condition)
if !ok {
//TODO: Throw error
panic("")
}

if !leftVal.Equal(rightVal) {
return false
}
}
return true
}

// TODO: Make strategy an enum
func concatMaps(left, right value.Value, strategy string, allowlist, denylist map[string]struct{}) value.Value {
res := make(map[string]value.Value)

for _, key := range left.Keys() {

// Insert the value into the result map
val, ok := left.Key(key)
if !ok {
//TODO: Throw error
panic("")
}
res[key] = val
}

for _, key := range right.Keys() {
if len(denylist) > 0 {
// Make sure the key is not in the denylist
_, found := denylist[key]
if found {
continue
}
}

if len(allowlist) > 0 {
// Make sure we only insert keys listed in the allowlist
_, found := allowlist[key]
if !found {
continue
}
}

val, ok := right.Key(key)
if !ok {
//TODO: Throw error
panic("")
}
res[key] = val
}

return value.Object(res)
}

// TODO: What if conditions are empty?
// Inputs:
// 1. []map[string]string
// 2. []map[string]string
// 3. []string
// 4. string: update strategy
// 5. []string: key allowlist
// 6. []string: key denylist
var innerJoin = value.RawFunction(func(funcValue value.Value, args ...value.Value) (value.Value, error) {
// left := make([]map[string]value.Value, 0)
// right := make([]map[string]value.Value, 0)
// conditions := make([]string, 0)

//TODO: Should the last 3 params be optional?
if len(args) < 3 {
return value.Value{}, fmt.Errorf("inner_join: expected at lest 3 arguments, got %d", len(args))
}

for i := range []int{0, 1} {
if args[i].Type() != value.TypeArray {
return value.Null, value.ArgError{
Function: funcValue,
Argument: args[i],
Index: i,
Inner: value.TypeError{
Value: args[i],
Expected: value.TypeArray,
},
}
}
for j := 0; j < args[i].Len(); j++ {
if args[i].Index(j).Type() != value.TypeObject {
return value.Null, value.ArgError{
Function: funcValue,
Argument: args[i].Index(j),
Index: j,
Inner: value.TypeError{
Value: args[i].Index(j),
Expected: value.TypeObject,
},
}
}
}
}

//TODO: Preallocate memory?
res := make([]value.Value, 0, 0)

conditions := args[2]

var strategy string
if len(args) >= 4 {
strategy = args[3].Text()
}

allowlist := make(map[string]struct{}, args[4].Len())
for i := 0; i < args[4].Len(); i++ {
allowlist[args[4].Index(i).Text()] = struct{}{}
}

denylist := make(map[string]struct{}, args[5].Len())
for i := 0; i < args[5].Len(); i++ {
denylist[args[5].Index(i).Text()] = struct{}{}
}

for i := 0; i < args[0].Len(); i++ {
for j := 0; j < args[1].Len(); j++ {
left := args[0].Index(i)
right := args[1].Index(j)
if shouldJoin(left, right, conditions) {
res = append(res, concatMaps(left, right, strategy, allowlist, denylist))
}
}
}

return value.Array(res...), nil
})

func jsonDecode(in string) (interface{}, error) {
var res interface{}
err := json.Unmarshal([]byte(in), &res)
Expand Down
18 changes: 16 additions & 2 deletions syntax/internal/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,11 @@ func (v Value) Text() string {
}
}

// Len returns the length of v. Panics if v is not an array or object.
// Len returns the length of v. Panics if v is not an array, an object, or nil.
func (v Value) Len() int {
switch v.ty {
case TypeNull:
return 0
case TypeArray:
return v.rv.Len()
case TypeObject:
Expand Down Expand Up @@ -396,7 +398,7 @@ func (v Value) Key(key string) (index Value, ok bool) {
//
// An ArgError will be returned if one of the arguments is invalid. An Error
// will be returned if the function call returns an error or if the number of
// arguments doesn't match.
// arguments doesn't match
func (v Value) Call(args ...Value) (Value, error) {
if v.ty != TypeFunction {
panic("syntax/value: Call called on non-function type")
Expand Down Expand Up @@ -553,3 +555,15 @@ func convertGoNumber(nval Number, target reflect.Type) reflect.Value {

panic("unsupported number conversion")
}

func (v Value) Equal(rhs Value) bool {
if v.Type() != rhs.Type() {
return false
}

if !v.rv.Equal(rhs.rv) {
return false
}

return true
}
17 changes: 17 additions & 0 deletions syntax/internal/value/value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ func (*pointerMarshaler) MarshalText() ([]byte, error) {
}

func TestValue_Call(t *testing.T) {
// t.Run("slices with maps", func(t *testing.T) {
// add := func(a, b []map[string]string) []map[string]string {
// res := make([]map[string]string, 0, len(a)+len(b))
// res = append(res, a...)
// res = append(res, b...)
// return res
// }
// addVal := value.Encode(add)

// res, err := addVal.Call(
// value.Array(map[string]string{"a": "b"}),
// value.Int(43),
// )
// require.NoError(t, err)
// require.Equal(t, int64(15+43), res.Int())
// })

t.Run("simple", func(t *testing.T) {
add := func(a, b int) int { return a + b }
addVal := value.Encode(add)
Expand Down
22 changes: 22 additions & 0 deletions syntax/vm/vm_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ func TestVM_Stdlib(t *testing.T) {
{"encoding.from_yaml nil field", "encoding.from_yaml(`foo: null`)", map[string]interface{}{"foo": nil}},
{"encoding.from_yaml nil array element", `encoding.from_yaml("[0, null]")`, []interface{}{0, nil}},
{"encoding.from_base64", `encoding.from_base64("Zm9vYmFyMTIzIT8kKiYoKSctPUB+")`, string(`foobar123!?$*&()'-=@~`)},

// Map tests
{
"map.inner_join",
`map.inner_join([{"a" = "a1", "b" = "b1"}], [{"a" = "a1", "c" = "c1"}], ["a"])`,
[]map[string]interface{}{{"a": "a1", "b": "b1", "c": "c1"}},
},
{
"map.inner_join",
`map.inner_join([{"a" = "a1", "b" = "b1"}, {"a" = "a1", "b" = "b1"}], [{"a" = "a1", "c" = "c1"}], ["a"])`,
[]map[string]interface{}{{"a": "a1", "b": "b1", "c": "c1"}, {"a": "a1", "b": "b1", "c": "c1"}},
},
{
"map.inner_join",
`map.inner_join([{"a" = "a1", "b" = "b1"}, {"a" = "a1", "b" = "b1"}], [{"a" = "a1", "c" = "c1"}], ["a"])`,
[]map[string]interface{}{{"a": "a1", "b": "b1", "c": "c1"}, {"a": "a1", "b": "b1", "c": "c1"}},
},
{
"map.inner_join",
`map.inner_join([{"a" = 1, "b" = 2.2}], [{"a" = 1, "c" = "c1"}], ["a"])`,
[]map[string]interface{}{{"a": 1, "b": 2.2, "c": "c1"}},
},
}

for _, tc := range tt {
Expand Down

0 comments on commit eb6820f

Please sign in to comment.