Skip to content

Commit

Permalink
add (delete) function
Browse files Browse the repository at this point in the history
  • Loading branch information
xrstf committed Nov 22, 2023
1 parent dcdbdd2 commit 14b9bdd
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 3 deletions.
47 changes: 46 additions & 1 deletion pkg/eval/builtin/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func setFunction(ctx types.Context, args []ast.Expression) (types.Context, any,
if p := symbol.PathExpression; p != nil {
pathExpr, err = eval.EvalPathExpression(ctx, p)
if err != nil {
return ctx, nil, fmt.Errorf("argument #1: invalid path expression: %w", err)
return ctx, nil, fmt.Errorf("argument #0: invalid path expression: %w", err)
}
}

Expand Down Expand Up @@ -261,6 +261,51 @@ func setFunction(ctx types.Context, args []ast.Expression) (types.Context, any,
return ctx, newValue, nil
}

// (delete VAR:Variable)
// (delete EXPR:PathExpression)
func deleteFunction(ctx types.Context, args []ast.Expression) (any, error) {
if size := len(args); size != 1 {
return nil, fmt.Errorf("expected 1 argument, got %d", size)
}

symbol, ok := args[0].(ast.Symbol)
if !ok {
return nil, fmt.Errorf("argument #0 is not a symbol, but %T", args[0])
}

// catch symbols that are technically invalid
if symbol.PathExpression == nil {
return nil, fmt.Errorf("argument #0: must be path expression or variable, got %s", symbol.ExpressionName())
}

// pre-evaluate the path
pathExpr, err := eval.EvalPathExpression(ctx, symbol.PathExpression)
if err != nil {
return nil, fmt.Errorf("argument #0: invalid path expression: %w", err)
}

// get the current value
var currentValue any

if symbol.Variable != nil {
varName := string(*symbol.Variable)

// a non-existing variable is fine, this is how you define new variables in the first place
currentValue, _ = ctx.GetVariable(varName)
} else {
doc := ctx.GetDocument()
currentValue = doc.Get()
}

// delete the desired path in the value
updatedValue, err := pathexpr.Delete(currentValue, pathexpr.FromEvaluatedPath(*pathExpr))
if err != nil {
return nil, fmt.Errorf("cannot delete %s in %T: %w", pathExpr, currentValue, err)
}

return types.Must(types.WrapNative(updatedValue)), nil
}

// (empty? VALUE:any)
func isEmptyFunction(ctx types.Context, args []ast.Expression) (any, error) {
if size := len(args); size != 1 {
Expand Down
83 changes: 83 additions & 0 deletions pkg/eval/builtin/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,89 @@ func TestSetFunction(t *testing.T) {
}
}

func TestDeleteFunction(t *testing.T) {
testcases := []coreTestcase{
{
expr: `(delete)`,
invalid: true,
},
{
expr: `(delete "too" "many")`,
invalid: true,
},
{
expr: `(delete $var)`,
invalid: true,
},
{
// TODO: This should be valid.
expr: `(delete [1 2 3][1])`,
invalid: true,
},
{
// TODO: This should be valid.
expr: `(delete {foo "bar"}.foo)`,
invalid: true,
},
// allow removing everything
{
expr: `(delete .)`,
document: map[string]any{"foo": "bar"},
expected: nil,
},
{
expr: `(delete .)`,
document: map[string]any{"foo": "bar"},
expected: nil,
},
// delete does not update the target
{
expr: `(delete .) .`,
document: map[string]any{"foo": "bar"},
expected: map[string]any{"foo": "bar"},
},
// can remove a key
{
expr: `(delete .foo)`,
document: map[string]any{"foo": "bar"},
expected: map[string]any{},
},
// non-existent key is okay
{
expr: `(delete .bar)`,
document: map[string]any{"foo": "bar"},
expected: map[string]any{"foo": "bar"},
},
// path must be sane though
{
expr: `(delete .[1])`,
document: map[string]any{"foo": "bar"},
invalid: true,
},
// can delete from array
{
expr: `(delete .[1])`,
document: []any{"a", "b", "c"},
expected: []any{"a", "c"},
},
// vector bounds are checked
{
expr: `(delete .[-1])`,
document: []any{"a", "b", "c"},
invalid: true,
},
{
expr: `(delete .[3])`,
document: []any{"a", "b", "c"},
invalid: true,
},
}

for _, testcase := range testcases {
t.Run(testcase.expr, testcase.Test)
}
}

func TestDoFunction(t *testing.T) {
testcases := []coreTestcase{
{
Expand Down
1 change: 1 addition & 0 deletions pkg/eval/builtin/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var Functions = types.Functions{
"default": stateless(defaultFunction),
"try": stateless(tryFunction),
"set": setFunction,
"delete": stateless(deleteFunction),
"empty?": stateless(isEmptyFunction),

// math
Expand Down
2 changes: 1 addition & 1 deletion pkg/eval/types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func WrapNative(val any) (ast.Literal, error) {
}
}

func Must(val any, _ error) any {
func Must[T any](val T, _ error) T {
return val
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/pathexpr/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func removeSliceItem(slice []any, index int) []any {

func Delete(dest any, path Path) (any, error) {
if len(path) == 0 {
return dest, nil
return nil, nil
}

target, err := types.UnwrapType(dest)
Expand Down
6 changes: 6 additions & 0 deletions pkg/pathexpr/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ func TestDelete(t *testing.T) {
path: Path{"foo"},
invalid: true,
},
{
name: "delete everything",
dest: map[string]any{"foo": "bar", "other": "value"},
path: Path{},
expected: nil,
},
{
name: "delete root key",
dest: map[string]any{"foo": "bar", "other": "value"},
Expand Down

0 comments on commit 14b9bdd

Please sign in to comment.