Skip to content

Commit

Permalink
feat(TKC-1465): expressions improvements - resolving structs, improve…
Browse files Browse the repository at this point in the history
…d finalizer (#5067)

* fix(TKC-1465): recognition of 'none' static value
* feat(TKC-1465): add helpers to compile and and resolve the expression immediately
* feat(TKC-1465): add mechanism to allow to resolve expressions in the objects via tags
* feat(TKC-1465): escape "{{" nicely
* chore(TKC-1465): avoid unnecessary string computation while resolving struct
* chore(TKC-1465): rename Resolve to SimplifyStruct
* feat(TKC-1465): add option to provide extended accessor in the expressions machine
* feat(TKC-1465): simplify expression finalizers, add expression machine utils
  • Loading branch information
rangoo94 authored Feb 27, 2024
1 parent 449616d commit d9b2c09
Show file tree
Hide file tree
Showing 19 changed files with 512 additions and 148 deletions.
4 changes: 2 additions & 2 deletions pkg/tcl/expressionstcl/accessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (s *accessor) Template() string {
return "{{" + s.String() + "}}"
}

func (s *accessor) SafeResolve(m ...MachineCore) (v Expression, changed bool, err error) {
func (s *accessor) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
if m == nil {
return s, false, nil
}
Expand All @@ -53,7 +53,7 @@ func (s *accessor) SafeResolve(m ...MachineCore) (v Expression, changed bool, er
return s, false, nil
}

func (s *accessor) Resolve(m ...MachineCore) (v Expression, err error) {
func (s *accessor) Resolve(m ...Machine) (v Expression, err error) {
return deepResolve(s, m...)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/tcl/expressionstcl/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s *call) resolvedArgs() []StaticValue {
return v
}

func (s *call) SafeResolve(m ...MachineCore) (v Expression, changed bool, err error) {
func (s *call) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
var ch bool
for i := range s.args {
s.args[i], ch, err = s.args[i].SafeResolve(m...)
Expand Down Expand Up @@ -117,7 +117,7 @@ func (s *call) SafeResolve(m ...MachineCore) (v Expression, changed bool, err er
return s, changed, nil
}

func (s *call) Resolve(m ...MachineCore) (v Expression, err error) {
func (s *call) Resolve(m ...Machine) (v Expression, err error) {
return deepResolve(s, m...)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/tcl/expressionstcl/conditional.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (s *conditional) Template() string {
return "{{" + s.String() + "}}"
}

func (s *conditional) SafeResolve(m ...MachineCore) (v Expression, changed bool, err error) {
func (s *conditional) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
var ch bool
s.condition, ch, err = s.condition.SafeResolve(m...)
changed = changed || ch
Expand Down Expand Up @@ -84,7 +84,7 @@ func (s *conditional) SafeResolve(m ...MachineCore) (v Expression, changed bool,
return s, changed, nil
}

func (s *conditional) Resolve(m ...MachineCore) (v Expression, err error) {
func (s *conditional) Resolve(m ...Machine) (v Expression, err error) {
return deepResolve(s, m...)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/tcl/expressionstcl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ type Expression interface {
SafeString() string
Template() string
Type() Type
SafeResolve(...MachineCore) (Expression, bool, error)
Resolve(...MachineCore) (Expression, error)
SafeResolve(...Machine) (Expression, bool, error)
Resolve(...Machine) (Expression, error)
Static() StaticValue
Accessors() map[string]struct{}
Functions() map[string]struct{}
Expand Down
67 changes: 57 additions & 10 deletions pkg/tcl/expressionstcl/finalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,72 @@
package expressionstcl

import (
"fmt"
"errors"
)

type finalizer struct {
machine MachineCore
handler FinalizerFn
}

type finalizerItem struct {
function bool
name string
}

type FinalizerItem interface {
Name() string
IsFunction() bool
}

type FinalizerResult int8

const (
FinalizerResultFail FinalizerResult = -1
FinalizerResultNone FinalizerResult = 0
FinalizerResultPreserve FinalizerResult = 1
)

type FinalizerFn = func(item FinalizerItem) FinalizerResult

func NewFinalizer(fn FinalizerFn) Machine {
return &finalizer{handler: fn}
}

func (f *finalizer) Get(name string) (Expression, bool, error) {
v, ok, err := f.machine.Get(name)
if !ok && err == nil {
result := f.handler(finalizerItem{name: name})
if result == FinalizerResultFail {
return nil, true, errors.New("unknown variable")
} else if result == FinalizerResultNone {
return None, true, nil
}
return v, ok, err
return nil, false, nil
}

func (f *finalizer) Call(name string, args ...StaticValue) (Expression, bool, error) {
v, ok, err := f.machine.Call(name, args...)
if !ok && err == nil {
return nil, true, fmt.Errorf(`"%s" function not resolved`, name)
func (f *finalizer) Call(name string, _ ...StaticValue) (Expression, bool, error) {
result := f.handler(finalizerItem{function: true, name: name})
if result == FinalizerResultFail {
return nil, true, errors.New("unknown function")
} else if result == FinalizerResultNone {
return None, true, nil
}
return v, ok, err
return nil, false, nil
}

func (f finalizerItem) IsFunction() bool {
return f.function
}

func (f finalizerItem) Name() string {
return f.name
}

func FinalizerFailFn(_ FinalizerItem) FinalizerResult {
return FinalizerResultFail
}

func FinalizerNoneFn(_ FinalizerItem) FinalizerResult {
return FinalizerResultNone
}

var FinalizerFail = NewFinalizer(FinalizerFailFn)
var FinalizerNone = NewFinalizer(FinalizerNoneFn)
167 changes: 167 additions & 0 deletions pkg/tcl/expressionstcl/generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2024 Testkube.
//
// Licensed as a Testkube Pro file under the Testkube Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt

package expressionstcl

import (
"fmt"
"reflect"
"strings"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/util/intstr"
)

type tagData struct {
key string
value string
}

func parseTag(tag string) tagData {
s := strings.Split(tag, ",")
if len(s) > 1 {
return tagData{key: s[0], value: s[1]}
}
return tagData{value: s[0]}
}

var unrecognizedErr = errors.New("unsupported value passed for resolving expressions")

func clone(v reflect.Value) reflect.Value {
if v.Kind() == reflect.String {
s := v.String()
return reflect.ValueOf(&s).Elem()
} else if v.Kind() == reflect.Struct {
r := reflect.New(v.Type()).Elem()
for i := 0; i < r.NumField(); i++ {
r.Field(i).Set(v.Field(i))
}
return r
}
return v
}

func resolve(v reflect.Value, t tagData, m []Machine) (err error) {
if t.key == "" && t.value == "" {
return
}

ptr := v
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
if v.IsNil() {
return
}
ptr = v
v = v.Elem()
}

if v.IsZero() || !v.IsValid() || (v.Kind() == reflect.Slice || v.Kind() == reflect.Map) && v.IsNil() {
return
}

switch v.Kind() {
case reflect.Struct:
// TODO: Cache the tags for structs for better performance
vv, ok := v.Interface().(intstr.IntOrString)
if ok {
if vv.Type == intstr.String {
return resolve(v.FieldByName("StrVal"), t, m)
}
} else if t.value == "include" {
tt := v.Type()
for i := 0; i < tt.NumField(); i++ {
f := tt.Field(i)
tag := parseTag(f.Tag.Get("expr"))
value := v.FieldByName(f.Name)
err = resolve(value, tag, m)
if err != nil {
return errors.Wrap(err, f.Name)
}
}
}
return
case reflect.Slice:
if t.value == "" {
return nil
}
for i := 0; i < v.Len(); i++ {
err := resolve(v.Index(i), t, m)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("%d", i))
}
}
return
case reflect.Map:
if t.value == "" && t.key == "" {
return nil
}
for _, k := range v.MapKeys() {
if t.value != "" {
// It's not possible to get a pointer to map element,
// so we need to copy it and reassign
item := clone(v.MapIndex(k))
err = resolve(item, t, m)
v.SetMapIndex(k, item)
if err != nil {
return errors.Wrap(err, k.String())
}
}
if t.key != "" {
key := clone(k)
err = resolve(key, tagData{value: t.key}, m)
if !key.Equal(k) {
item := clone(v.MapIndex(k))
v.SetMapIndex(k, reflect.Value{})
v.SetMapIndex(key, item)
}
if err != nil {
return errors.Wrap(err, "key("+k.String()+")")
}
}
}
return
case reflect.String:
if t.value == "expression" {
var expr Expression
expr, err = CompileAndResolve(v.String(), m...)
if err != nil {
return err
}
vv := expr.String()
if ptr.Kind() == reflect.String {
v.SetString(vv)
} else {
ptr.Set(reflect.ValueOf(&vv))
}
} else if t.value == "template" && !IsTemplateStringWithoutExpressions(v.String()) {
var expr Expression
expr, err = CompileAndResolveTemplate(v.String(), m...)
if err != nil {
return err
}
vv := expr.Template()
if ptr.Kind() == reflect.String {
v.SetString(vv)
} else {
ptr.Set(reflect.ValueOf(&vv))
}
}
return
}

// Fail for unrecognized values
return unrecognizedErr
}

func SimplifyStruct(t interface{}, m ...Machine) error {
v := reflect.ValueOf(t)
if v.Kind() != reflect.Pointer {
return errors.New("pointer needs to be passed to Resolve function")
}
return resolve(v, tagData{value: "include"}, m)
}
Loading

0 comments on commit d9b2c09

Please sign in to comment.