Skip to content

Commit

Permalink
builtin delete for maps and arrays (#250)
Browse files Browse the repository at this point in the history
* added builtin delete function and unit tests

* added vm tests for builtin delete

* added doc for builtin delete

* update doc
  • Loading branch information
Ozan HACIBEKİROĞLU authored Feb 19, 2020
1 parent 6fb0df7 commit ac53405
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 0 deletions.
32 changes: 32 additions & 0 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ var builtinFuncs = []*BuiltinFunction{
Name: "append",
Value: builtinAppend,
},
{
Name: "delete",
Value: builtinDelete,
},
{
Name: "string",
Value: builtinString,
Expand Down Expand Up @@ -500,3 +504,31 @@ func builtinAppend(args ...Object) (Object, error) {
}
}
}

// builtinDelete deletes Map keys
// usage: delete(map, "key")
// key must be a string
func builtinDelete(args ...Object) (Object, error) {
argsLen := len(args)
if argsLen != 2 {
return nil, ErrWrongNumArguments
}
switch arg := args[0].(type) {
case *Map:
if key, ok := args[1].(*String); ok {
delete(arg.Value, key.Value)
return UndefinedValue, nil
}
return nil, ErrInvalidArgumentType{
Name: "second",
Expected: "string",
Found: args[1].TypeName(),
}
default:
return nil, ErrInvalidArgumentType{
Name: "first",
Expected: "map",
Found: arg.TypeName(),
}
}
}
97 changes: 97 additions & 0 deletions builtins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tengo

import (
"errors"
"reflect"
"testing"
)

func Test_builtinDelete(t *testing.T) {
type args struct {
args []Object
}
tests := []struct {
name string
args args
want Object
wantErr bool
wantedErr error
target interface{}
}{
//Map
{name: "invalid-arg", args: args{[]Object{&String{}, &String{}}}, wantErr: true,
wantedErr: ErrInvalidArgumentType{Name: "first", Expected: "map", Found: "string"}},
{name: "no-args", wantErr: true, wantedErr: ErrWrongNumArguments},
{name: "empty-args", args: args{[]Object{}}, wantErr: true, wantedErr: ErrWrongNumArguments},
{name: "3-args", args: args{[]Object{(*Map)(nil), (*String)(nil), (*String)(nil)}}, wantErr: true, wantedErr: ErrWrongNumArguments},
{name: "nil-map-empty-key", args: args{[]Object{&Map{}, &String{}}}, want: UndefinedValue},
{name: "nil-map-nonstr-key", args: args{[]Object{&Map{}, &Int{}}}, wantErr: true,
wantedErr: ErrInvalidArgumentType{Name: "second", Expected: "string", Found: "int"}},
{name: "nil-map-no-key", args: args{[]Object{&Map{}}}, wantErr: true,
wantedErr: ErrWrongNumArguments},
{name: "map-missing-key",
args: args{
[]Object{
&Map{Value: map[string]Object{
"key": &String{Value: "value"},
}},
&String{Value: "key1"},
}},
want: UndefinedValue,
target: &Map{Value: map[string]Object{"key": &String{Value: "value"}}},
},
{name: "map-emptied",
args: args{
[]Object{
&Map{Value: map[string]Object{
"key": &String{Value: "value"},
}},
&String{Value: "key"},
}},
want: UndefinedValue,
target: &Map{Value: map[string]Object{}},
},
{name: "map-multi-keys",
args: args{
[]Object{
&Map{Value: map[string]Object{
"key1": &String{Value: "value1"},
"key2": &Int{Value: 10},
}},
&String{Value: "key1"},
}},
want: UndefinedValue,
target: &Map{Value: map[string]Object{"key2": &Int{Value: 10}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := builtinDelete(tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("builtinDelete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && !errors.Is(err, tt.wantedErr) {
if err.Error() != tt.wantedErr.Error() {
t.Errorf("builtinDelete() error = %v, wantedErr %v", err, tt.wantedErr)
return
}
}
if got != tt.want {
t.Errorf("builtinDelete() = %v, want %v", got, tt.want)
return
}
if !tt.wantErr && tt.target != nil {
switch v := tt.args.args[0].(type) {
case *Map, *Array:
if !reflect.DeepEqual(tt.target, tt.args.args[0]) {
t.Errorf("builtinDelete() objects are not equal got: %+v, want: %+v", tt.args.args[0], tt.target)
}
default:
t.Errorf("builtinDelete() unsuporrted arg[0] type %s", v.TypeName())
return
}
}
})
}
}
22 changes: 22 additions & 0 deletions docs/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ v := [1]
v = append(v, 2, 3) // v == [1, 2, 3]
```

## delete

Deletes the element with the specified key from the map type.
First argument must be a map type and second argument must be a string type.
(Like Go's `delete` builtin except keys are always string).
`delete` returns `undefined` value if successful and it mutates given map.

```golang
v := {key: "value"}
delete(v, "key") // v == {}
```

```golang
v := {key: "value"}
delete(v, "missing") // v == {"key": "value"}
```

```golang
delete({}) // runtime error, second argument is missing
delete({}, 1) // runtime error, second argument must be a string type
```

## type_name

Returns the type_name of an object.
Expand Down
34 changes: 34 additions & 0 deletions vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,40 @@ func TestBuiltinFunction(t *testing.T) {
expectError(t, `format("%s", "1234567890")`,
nil, "exceeding string size limit")
tengo.MaxStringLen = 2147483647

// delete
expectError(t, `delete()`, nil, tengo.ErrWrongNumArguments.Error())
expectError(t, `delete(1)`, nil, tengo.ErrWrongNumArguments.Error())
expectError(t, `delete(1, 2, 3)`, nil, tengo.ErrWrongNumArguments.Error())
expectError(t, `delete({}, "", 3)`, nil, tengo.ErrWrongNumArguments.Error())
expectError(t, `delete(1, 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(1.0, 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete("str", 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(bytes("str"), 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(error("err"), 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(true, 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(char('c'), 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(undefined, 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(time(1257894000), 1)`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(immutable({}), "key")`, nil, `invalid type for argument 'first'`)
expectError(t, `delete(immutable([]), "")`, nil, `invalid type for argument 'first'`)
expectError(t, `delete([], "")`, nil, `invalid type for argument 'first'`)
expectError(t, `delete({}, 1)`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, 1.0)`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, undefined)`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, [])`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, {})`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, error("err"))`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, bytes("str"))`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, char(35))`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, time(1257894000))`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, immutable({}))`, nil, `invalid type for argument 'second'`)
expectError(t, `delete({}, immutable([]))`, nil, `invalid type for argument 'second'`)

expectRun(t, `out = delete({}, "")`, nil, tengo.UndefinedValue)
expectRun(t, `out = {key1: 1}; delete(out, "key1")`, nil, MAP{})
expectRun(t, `out = {key1: 1, key2: "2"}; delete(out, "key1")`, nil, MAP{"key2": "2"})
expectRun(t, `out = [1, "2", {a: "b", c: 10}]; delete(out[2], "c")`, nil, ARR{1, "2", MAP{"a": "b"}})
}

func TestBytesN(t *testing.T) {
Expand Down

0 comments on commit ac53405

Please sign in to comment.