From 22eaa3bb21c71c889ffdc3de93c40c47fecdaa45 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Wed, 6 Mar 2024 14:51:25 +0000 Subject: [PATCH] Add root level if statements --- CHANGELOG.md | 5 +- internal/bloblang/mapping/executor.go | 101 +++------- internal/bloblang/mapping/executor_test.go | 182 ++++++++++++------ internal/bloblang/mapping/statement.go | 132 +++++++++++++ internal/bloblang/parser/mapping_parser.go | 54 +++--- .../bloblang/parser/root_expression_parser.go | 109 +++++++++++ .../parser/root_expression_parser_test.go | 149 ++++++++++++++ website/docs/guides/bloblang/about.md | 14 +- website/docs/guides/bloblang/walkthrough.md | 25 +++ website/package.json | 2 +- 10 files changed, 601 insertions(+), 172 deletions(-) create mode 100644 internal/bloblang/mapping/statement.go create mode 100644 internal/bloblang/parser/root_expression_parser.go create mode 100644 internal/bloblang/parser/root_expression_parser_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f918f62a6e..818adac323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,12 @@ All notable changes to this project will be documented in this file. ### Added -- Field `credit` added to the `amqp_1` input to specify the maximum number of unacknowledged messages the sender can transmit. +- Field `credit` added to the `amqp_1` input to specify the maximum number of unacknowledged messages the sender can transmit. +- Bloblang now supports root-level `if` statements. ### Changed -- The default value of the `amqp_1.credit` input has changed from `1` to `64` +- The default value of the `amqp_1.credit` input has changed from `1` to `64`. ## 4.25.1 - 2024-03-01 diff --git a/internal/bloblang/mapping/executor.go b/internal/bloblang/mapping/executor.go index e15b8d1690..f4e00ba6fd 100644 --- a/internal/bloblang/mapping/executor.go +++ b/internal/bloblang/mapping/executor.go @@ -10,8 +10,6 @@ import ( "github.com/benthosdev/benthos/v4/internal/value" ) -//------------------------------------------------------------------------------ - // Message is an interface type to be given to a query function, it allows the // function to resolve fields and metadata from a message. type Message interface { @@ -39,34 +37,13 @@ func LineAndColOf(input, clip []rune) (line, col int) { //------------------------------------------------------------------------------ -// Statement describes an isolated mapping statement, where the result of a -// query function is to be mapped according to an Assignment. -type Statement struct { - input []rune - assignment Assignment - query query.Function -} - -// NewStatement initialises a new mapping statement from an Assignment and -// query.Function. The input parameter is an optional slice pointing to the -// parsed expression that created the statement. -func NewStatement(input []rune, assignment Assignment, query query.Function) *Statement { - return &Statement{ - input: input, - assignment: assignment, - query: query, - } -} - -//------------------------------------------------------------------------------ - // Executor is a parsed bloblang mapping that can be executed on a Benthos // message. type Executor struct { annotation string input []rune maps map[string]query.Function - statements []*Statement + statements []Statement maxMapStacks int } @@ -77,7 +54,7 @@ const defaultMaxMapStacks = 5000 // and a list of assignments to be executed on each mapping. The input parameter // is an optional slice pointing to the parsed expression that created the // executor. -func NewExecutor(annotation string, input []rune, maps map[string]query.Function, statements ...*Statement) *Executor { +func NewExecutor(annotation string, input []rune, maps map[string]query.Function, statements ...Statement) *Executor { return &Executor{ annotation: annotation, input: input, @@ -179,18 +156,24 @@ func (e *Executor) mapPart(appendTo *message.Part, index int, reference Message) vars := map[string]any{} for _, stmt := range e.statements { - res, err := stmt.query.Exec(query.FunctionContext{ + err := stmt.Execute(query.FunctionContext{ Maps: e.maps, Vars: vars, Index: index, MsgBatch: reference, NewMeta: newPart, NewValue: &newValue, - }.WithValueFunc(lazyValue)) + }.WithValueFunc(lazyValue), + AssignmentContext{ + Vars: vars, + Meta: newPart, + Value: &newValue, + }, + ) if err != nil { var line int - if len(e.input) > 0 && len(stmt.input) > 0 { - line, _ = LineAndColOf(e.input, stmt.input) + if len(e.input) > 0 && len(stmt.Input()) > 0 { + line, _ = LineAndColOf(e.input, stmt.Input()) } var ctxErr query.ErrNoContext if parseErr != nil && errors.As(err, &ctxErr) { @@ -202,21 +185,6 @@ func (e *Executor) mapPart(appendTo *message.Part, index int, reference Message) } return nil, fmt.Errorf("failed assignment (line %v): %w", line, err) } - if _, isNothing := res.(value.Nothing); isNothing { - // Skip assignment entirely - continue - } - if err = stmt.assignment.Apply(res, AssignmentContext{ - Vars: vars, - Meta: newPart, - Value: &newValue, - }); err != nil { - var line int - if len(e.input) > 0 && len(stmt.input) > 0 { - line, _ = LineAndColOf(e.input, stmt.input) - } - return nil, fmt.Errorf("failed to assign result (line %v): %w", line, err) - } } switch newValue.(type) { @@ -247,7 +215,7 @@ func (e *Executor) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, var paths []query.TargetPath for _, stmt := range e.statements { - _, tmpPaths := stmt.query.QueryTargets(childCtx) + _, tmpPaths := stmt.QueryTargets(childCtx) paths = append(paths, tmpPaths...) } @@ -259,7 +227,7 @@ func (e *Executor) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, func (e *Executor) AssignmentTargets() []TargetPath { var paths []TargetPath for _, stmt := range e.statements { - paths = append(paths, stmt.assignment.Target()) + paths = append(paths, stmt.AssignmentTargets()...) } return paths } @@ -275,20 +243,12 @@ func (e *Executor) Exec(ctx query.FunctionContext) (any, error) { ctx.NewValue = &newObj for _, stmt := range e.statements { - res, err := stmt.query.Exec(ctx) - if err != nil { - return nil, formatExecErr(err, true, e.input, stmt.input) - } - if _, isNothing := res.(value.Nothing); isNothing { - // Skip assignment entirely - continue - } - if err = stmt.assignment.Apply(res, AssignmentContext{ + if err := stmt.Execute(ctx, AssignmentContext{ Vars: ctx.Vars, // Meta: meta, Prevented for now due to .from(int) Value: &newObj, }); err != nil { - return nil, formatExecErr(err, false, e.input, stmt.input) + return nil, formatExecErr(err, e.input, stmt.Input()) } } @@ -298,16 +258,8 @@ func (e *Executor) Exec(ctx query.FunctionContext) (any, error) { // ExecOnto a provided assignment context. func (e *Executor) ExecOnto(ctx query.FunctionContext, onto AssignmentContext) error { for _, stmt := range e.statements { - res, err := stmt.query.Exec(ctx) - if err != nil { - return formatExecErr(err, true, e.input, stmt.input) - } - if _, isNothing := res.(value.Nothing); isNothing { - // Skip assignment entirely - continue - } - if err = stmt.assignment.Apply(res, onto); err != nil { - return formatExecErr(err, false, e.input, stmt.input) + if err := stmt.Execute(ctx, onto); err != nil { + return formatExecErr(err, e.input, stmt.Input()) } } return nil @@ -336,9 +288,8 @@ func (e *Executor) ToString(ctx query.FunctionContext) (string, error) { //------------------------------------------------------------------------------ type failedAssignmentErr struct { - line int - onExec bool - err error + line int + err error } func (f *failedAssignmentErr) Unwrap() error { @@ -346,10 +297,7 @@ func (f *failedAssignmentErr) Unwrap() error { } func (f *failedAssignmentErr) Error() string { - if f.onExec { - return fmt.Sprintf("failed assignment (line %v): %v", f.line, f.err) - } - return fmt.Sprintf("failed to assign result (line %v): %v", f.line, f.err) + return fmt.Sprintf("failed assignment (line %v): %v", f.line, f.err) } type errStacks struct { @@ -361,7 +309,7 @@ func (e *errStacks) Error() string { return fmt.Sprintf("entering %v exceeded maximum allowed stacks of %v, this could be due to unbounded recursion", e.annotation, e.maxStacks) } -func formatExecErr(err error, onExec bool, input, stmtInput []rune) error { +func formatExecErr(err error, input, stmtInput []rune) error { var u *failedAssignmentErr if errors.As(err, &u) { return u @@ -378,8 +326,7 @@ func formatExecErr(err error, onExec bool, input, stmtInput []rune) error { } return &failedAssignmentErr{ - line: line, - onExec: onExec, - err: err, + line: line, + err: err, } } diff --git a/internal/bloblang/mapping/executor_test.go b/internal/bloblang/mapping/executor_test.go index dc06109163..2a774876c9 100644 --- a/internal/bloblang/mapping/executor_test.go +++ b/internal/bloblang/mapping/executor_test.go @@ -39,79 +39,79 @@ func TestAssignments(t *testing.T) { }{ "simple json map": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), + NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, output: &part{Content: `{"bar":"test2","foo":"test1"}`}, }, "map to root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "bar")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "bar")), ), input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, output: &part{Content: `bar`}, }, "append array at root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), - NewStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "foo")), - NewStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "bar")), - NewStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "baz")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), + NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "foo")), + NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "bar")), + NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "baz")), ), input: []part{{Content: `[]`}}, output: &part{Content: `["foo","bar","baz"]`}, }, "append array at root nested": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), - NewStatement(nil, NewJSONAssignment("-", "A"), query.NewLiteralFunction("", "foo")), - NewStatement(nil, NewJSONAssignment("-", "B"), query.NewLiteralFunction("", "bar")), - NewStatement(nil, NewJSONAssignment("-", "C"), query.NewLiteralFunction("", "baz")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), + NewSingleStatement(nil, NewJSONAssignment("-", "A"), query.NewLiteralFunction("", "foo")), + NewSingleStatement(nil, NewJSONAssignment("-", "B"), query.NewLiteralFunction("", "bar")), + NewSingleStatement(nil, NewJSONAssignment("-", "C"), query.NewLiteralFunction("", "baz")), ), input: []part{{Content: `{}`}}, output: &part{Content: `[{"A":"foo"},{"B":"bar"},{"C":"baz"}]`}, }, "delete root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, output: nil, }, "no mapping to root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Nothing(nil))), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Nothing(nil))), ), input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, output: &part{Content: `{"bar":"test1","zed":"gone"}`}, }, "variable error DNE": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("doesnt exist")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("doesnt exist")), ), input: []part{{Content: `{}`}}, err: errors.New("failed assignment (line 0): variable 'doesnt exist' undefined"), }, "variable assignment": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", "does exist")), - NewStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("foo")), + NewSingleStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", "does exist")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("foo")), ), input: []part{{Content: `{}`}}, output: &part{Content: `{"foo":"does exist"}`}, }, "meta query error": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), + NewSingleStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), ), input: []part{{Content: `{}`}}, output: &part{Content: `{"foo":null}`}, }, "meta assignment": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), ), input: []part{{Content: `{}`}}, output: &part{ @@ -123,7 +123,7 @@ func TestAssignments(t *testing.T) { }, "meta deletion": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(metaKey("and")), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewMetaAssignment(metaKey("and")), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{ Content: `{}`, @@ -141,14 +141,14 @@ func TestAssignments(t *testing.T) { }, "meta set all error wrong type": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "foo")), + NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "foo")), ), input: []part{{Content: `{}`}}, - err: errors.New("failed to assign result (line 0): setting root meta object requires object value, received: string"), + err: errors.New("failed assignment (line 0): setting root meta object requires object value, received: string"), }, "meta set all": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", map[string]any{ + NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", map[string]any{ "new1": "value1", "new2": "value2", })), @@ -170,7 +170,7 @@ func TestAssignments(t *testing.T) { }, "meta delete all": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{ Content: `{}`, @@ -183,8 +183,8 @@ func TestAssignments(t *testing.T) { }, "metadata assignment": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "new value")), - NewStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("meta", "foo")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "new value")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("meta", "foo")), ), input: []part{{ Content: `{}`, @@ -202,8 +202,8 @@ func TestAssignments(t *testing.T) { }, "root_metadata assignment": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), - NewStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("root_meta", "foo")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("root_meta", "foo")), ), input: []part{{Content: `{}`}}, output: &part{ @@ -216,22 +216,64 @@ func TestAssignments(t *testing.T) { }, "invalid json message": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{Content: `{@#$ not valid json`}}, err: errors.New("failed assignment (line 0): unable to reference message as structured (with 'this.bar'): parse as json: invalid character '@' looking for beginning of object key string"), }, "json parse empty message": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), + NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), ), input: []part{{Content: ``}}, err: errors.New("failed assignment (line 0): unable to reference message as structured (with 'this.bar'): message is empty"), }, + "root if statements": { + mapping: NewExecutor("", nil, nil, + NewRootLevelIfStatement(nil).Add( + query.NewLiteralFunction("", true), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "bar")), + ), + ), + input: []part{{Content: `{}`}}, + output: &part{Content: `{"foo":"bar"}`}, + }, + "root if statements if/else": { + mapping: NewExecutor("", nil, nil, + NewRootLevelIfStatement(nil).Add( + query.NewLiteralFunction("", false), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "a")), + ).Add( + query.NewLiteralFunction("", true), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "b")), + ).Add( + nil, + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "c")), + ), + ), + input: []part{{Content: `{}`}}, + output: &part{Content: `{"foo":"b"}`}, + }, + "root if statements else": { + mapping: NewExecutor("", nil, nil, + NewRootLevelIfStatement(nil).Add( + query.NewLiteralFunction("", false), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "a")), + ).Add( + query.NewLiteralFunction("", false), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "b")), + ).Add( + nil, + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "c")), + ), + ), + input: []part{{Content: `{}`}}, + output: &part{Content: `{"foo":"c"}`}, + }, } for name, test := range tests { @@ -298,9 +340,27 @@ func TestTargets(t *testing.T) { }{ { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("first")), - NewStatement(nil, NewMetaAssignment(metaKey("bar")), query.NewLiteralFunction("", "second")), - NewStatement(nil, NewVarAssignment("baz"), function("meta", "third")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("first")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), query.NewLiteralFunction("", "second")), + NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), + ), + queryTargets: []query.TargetPath{ + query.NewTargetPath(query.TargetValue, "first"), + query.NewTargetPath(query.TargetMetadata, "third"), + }, + assignmentTargets: []TargetPath{ + NewTargetPath(TargetValue, "foo"), + NewTargetPath(TargetMetadata, "bar"), + NewTargetPath(TargetVariable, "baz"), + }, + }, + { + mapping: NewExecutor("", nil, nil, + NewRootLevelIfStatement(nil).Add(query.NewLiteralFunction("", false), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("first")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), query.NewLiteralFunction("", "second")), + NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), + ), ), queryTargets: []query.TargetPath{ query.NewTargetPath(query.TargetValue, "first"), @@ -314,9 +374,9 @@ func TestTargets(t *testing.T) { }, { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("first")), - NewStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "second")), - NewStatement(nil, NewVarAssignment("baz"), function("meta", "third")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("first")), + NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "second")), + NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), ), queryTargets: []query.TargetPath{ query.NewTargetPath(query.TargetValue, "first"), @@ -363,19 +423,19 @@ func TestExec(t *testing.T) { }{ "cant set meta": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "bar")), + NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "bar")), ), - err: "failed to assign result (line 0): unable to assign metadata in the current context", + err: "failed assignment (line 0): unable to assign metadata in the current context", }, "cant use json": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), function("json", "bar")), + NewSingleStatement(nil, NewJSONAssignment("foo"), function("json", "bar")), ), err: "failed assignment (line 0): target message part does not exist", }, "simple root get and set": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("")), ), input: "foobar", output: "foobar", @@ -383,7 +443,7 @@ func TestExec(t *testing.T) { }, "nested get and set": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), ), input: map[string]any{"bar": "baz"}, output: map[string]any{"foo": "baz"}, @@ -391,7 +451,7 @@ func TestExec(t *testing.T) { }, "failed get": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), function("json", "bar.baz")), + NewSingleStatement(nil, NewJSONAssignment("foo"), function("json", "bar.baz")), ), input: map[string]any{"nope": "baz"}, err: "failed assignment (line 0): target message part does not exist", @@ -399,7 +459,7 @@ func TestExec(t *testing.T) { }, "null get and set": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("does.not.exist")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("does.not.exist")), ), input: `{"message":"hello world"}`, output: map[string]any{"foo": nil}, @@ -407,7 +467,7 @@ func TestExec(t *testing.T) { }, "null get and set root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("does.not.exist")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("does.not.exist")), ), input: `{"message":"hello world"}`, output: nil, @@ -415,19 +475,19 @@ func TestExec(t *testing.T) { }, "colliding set at root": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "hello world")), - NewStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "hello world")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), ), input: map[string]any{"bar": "baz"}, - err: "failed to assign result (line 0): unable to set target path foo as the value of the root was a non-object type (string)", + err: "failed assignment (line 0): unable to set target path foo as the value of the root was a non-object type (string)", }, "colliding set at path": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "hello world")), - NewStatement(nil, NewJSONAssignment("foo", "bar"), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "hello world")), + NewSingleStatement(nil, NewJSONAssignment("foo", "bar"), query.NewFieldFunction("bar")), ), input: map[string]any{"bar": "baz"}, - err: "failed to assign result (line 0): unable to set target path foo.bar as the value of foo was a non-object type (string)", + err: "failed assignment (line 0): unable to set target path foo.bar as the value of foo was a non-object type (string)", }, } @@ -488,43 +548,43 @@ func TestQueries(t *testing.T) { }{ "simple json query": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), ), input: []part{{Content: `{"bar":true}`}}, output: true, }, "simple json query 2": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), ), input: []part{{Content: `{"bar":false}`}}, output: false, }, "json query deleted message": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("delete", value.Delete(nil))), + NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("delete", value.Delete(nil))), ), input: []part{{Content: `{"bar":{"is":"an object"}}`}}, err: errors.New("query mapping resulted in deleted message, expected a boolean value"), }, "simple json query bad type": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), + NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), ), input: []part{{Content: `{"bar":{"is":"an object"}}`}}, err: errors.New("expected bool value, got object from mapping"), }, "var assignment": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", true)), - NewStatement(nil, NewJSONAssignment(), initFunc("var", "foo")), + NewSingleStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", true)), + NewSingleStatement(nil, NewJSONAssignment(), initFunc("var", "foo")), ), input: []part{{Content: `not valid json`}}, output: true, }, "meta query error": { mapping: NewExecutor("", nil, nil, - NewStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), + NewSingleStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), ), input: []part{{Content: `{}`}}, err: errors.New("expected bool value, got object from mapping"), diff --git a/internal/bloblang/mapping/statement.go b/internal/bloblang/mapping/statement.go new file mode 100644 index 0000000000..40e668c11a --- /dev/null +++ b/internal/bloblang/mapping/statement.go @@ -0,0 +1,132 @@ +package mapping + +import ( + "fmt" + + "github.com/benthosdev/benthos/v4/internal/bloblang/query" + "github.com/benthosdev/benthos/v4/internal/value" +) + +type Statement interface { + QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) + AssignmentTargets() []TargetPath + Input() []rune + Execute(fnContext query.FunctionContext, asContext AssignmentContext) error +} + +//------------------------------------------------------------------------------ + +type SingleStatement struct { + input []rune + assignment Assignment + query query.Function +} + +func NewSingleStatement(input []rune, assignment Assignment, query query.Function) *SingleStatement { + return &SingleStatement{ + input: input, + assignment: assignment, + query: query, + } +} + +func (s *SingleStatement) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) { + return s.query.QueryTargets(ctx) +} + +func (s *SingleStatement) AssignmentTargets() []TargetPath { + return []TargetPath{s.assignment.Target()} +} + +func (s *SingleStatement) Input() []rune { + return s.input +} + +func (s *SingleStatement) Execute(fnContext query.FunctionContext, asContext AssignmentContext) error { + res, err := s.query.Exec(fnContext) + if err != nil { + return err + } + if _, isNothing := res.(value.Nothing); isNothing { + // Skip assignment entirely + return nil + } + return s.assignment.Apply(res, asContext) +} + +//------------------------------------------------------------------------------ + +type rootLevelIfStatementPair struct { + query query.Function + statements []Statement +} + +type RootLevelIfStatement struct { + input []rune + pairs []rootLevelIfStatementPair +} + +func NewRootLevelIfStatement(input []rune) *RootLevelIfStatement { + return &RootLevelIfStatement{ + input: input, + } +} + +func (r *RootLevelIfStatement) Add(query query.Function, statements ...Statement) *RootLevelIfStatement { + r.pairs = append(r.pairs, rootLevelIfStatementPair{query: query, statements: statements}) + return r +} + +func (r *RootLevelIfStatement) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) { + var paths []query.TargetPath + for _, p := range r.pairs { + if p.query != nil { + _, tmp := p.query.QueryTargets(ctx) + paths = append(paths, tmp...) + } + for _, s := range p.statements { + _, tmp := s.QueryTargets(ctx) + paths = append(paths, tmp...) + } + } + return ctx, paths +} + +func (r *RootLevelIfStatement) AssignmentTargets() []TargetPath { + var paths []TargetPath + for _, p := range r.pairs { + for _, s := range p.statements { + paths = append(paths, s.AssignmentTargets()...) + } + } + return paths +} + +func (r *RootLevelIfStatement) Input() []rune { + return r.input +} + +func (r *RootLevelIfStatement) Execute(fnContext query.FunctionContext, asContext AssignmentContext) error { + for i, p := range r.pairs { + if p.query != nil { + queryVal, err := p.query.Exec(fnContext) + if err != nil { + return fmt.Errorf("failed to check if condition %v: %w", i+1, err) + } + queryRes, isBool := queryVal.(bool) + if !isBool { + return fmt.Errorf("%v resolved to a non-boolean value %v (%T)", p.query.Annotation(), queryVal, queryVal) + } + if !queryRes { + continue + } + } + for _, stmt := range p.statements { + if err := stmt.Execute(fnContext, asContext); err != nil { + return err + } + } + return nil + } + return nil +} diff --git a/internal/bloblang/parser/mapping_parser.go b/internal/bloblang/parser/mapping_parser.go index cb7682dbcc..20a4c6b188 100644 --- a/internal/bloblang/parser/mapping_parser.go +++ b/internal/bloblang/parser/mapping_parser.go @@ -42,30 +42,30 @@ func ParseMapping(pCtx Context, expr string) (*mapping.Executor, *Error) { //------------------------------------------------------------------------------ -func mappingStatement(pCtx Context, enableMeta bool, maps map[string]query.Function) Func[*mapping.Statement] { - toNilStatement := ZeroedFuncAs[string, *mapping.Statement] +func mappingStatement(pCtx Context, enableMeta bool, maps map[string]query.Function) Func[mapping.Statement] { + toNilStatement := ZeroedFuncAs[string, mapping.Statement] - if maps == nil { - return OneOf( + var enabledStatements []Func[mapping.Statement] + if maps != nil { + enabledStatements = []Func[mapping.Statement]{ toNilStatement(importParser(pCtx, maps)), - letStatementParser(pCtx), - metaStatementParser(pCtx, enableMeta), - plainMappingStatementParser(pCtx), - ) + toNilStatement(mapParser(pCtx, maps)), + } } - return OneOf( - toNilStatement(importParser(pCtx, maps)), - toNilStatement(mapParser(pCtx, maps)), + enabledStatements = append(enabledStatements, letStatementParser(pCtx), metaStatementParser(pCtx, enableMeta), plainMappingStatementParser(pCtx), + rootLevelIfExpressionParser(pCtx), ) + + return OneOf(enabledStatements...) } func parseExecutor(pCtx Context) Func[*mapping.Executor] { return func(input []rune) Result[*mapping.Executor] { maps := map[string]query.Function{} - statements := []*mapping.Statement{} + statements := []mapping.Statement{} statementPattern := mappingStatement(pCtx, true, maps) @@ -165,7 +165,7 @@ func singleRootMapping(pCtx Context) Func[*mapping.Executor] { return Fail[*mapping.Executor](NewError(testRes.Remaining, expStr), input) } - stmt := mapping.NewStatement(input, mapping.NewJSONAssignment(), fn) + stmt := mapping.NewSingleStatement(input, mapping.NewJSONAssignment(), fn) return Success(mapping.NewExecutor("", input, map[string]query.Function{}, stmt), nil) } } @@ -294,7 +294,7 @@ func mapParser(pCtx Context, maps map[string]query.Function) Func[string] { seqSlice := res.Payload ident := seqSlice[2].(string) - stmtSlice := seqSlice[4].([]*mapping.Statement) + stmtSlice := seqSlice[4].([]mapping.Statement) if _, exists := maps[ident]; exists { return Fail[string](NewFatalError(input, fmt.Errorf("map name collision: %v", ident)), input) @@ -305,7 +305,7 @@ func mapParser(pCtx Context, maps map[string]query.Function) Func[string] { } } -func letStatementParser(pCtx Context) Func[*mapping.Statement] { +func letStatementParser(pCtx Context) Func[mapping.Statement] { p := Sequence( FuncAsAny(Expect(Term("let"), "assignment")), FuncAsAny(SpacesAndTabs), @@ -325,12 +325,12 @@ func letStatementParser(pCtx Context) Func[*mapping.Statement] { FuncAsAny(queryParser(pCtx)), ) - return func(input []rune) Result[*mapping.Statement] { + return func(input []rune) Result[mapping.Statement] { res := p(input) if res.Err != nil { - return Fail[*mapping.Statement](res.Err, input) + return Fail[mapping.Statement](res.Err, input) } - return Success(mapping.NewStatement( + return Success[mapping.Statement](mapping.NewSingleStatement( input, mapping.NewVarAssignment(res.Payload[2].(string)), res.Payload[6].(query.Function), @@ -349,7 +349,7 @@ var nameLiteralParser = JoinStringPayloads( ), ) -func metaStatementParser(pCtx Context, enabled bool) Func[*mapping.Statement] { +func metaStatementParser(pCtx Context, enabled bool) Func[mapping.Statement] { p := Sequence( FuncAsAny(Expect(Term("meta"), "assignment")), FuncAsAny(SpacesAndTabs), @@ -364,20 +364,20 @@ func metaStatementParser(pCtx Context, enabled bool) Func[*mapping.Statement] { FuncAsAny(queryParser(pCtx)), ) - return func(input []rune) Result[*mapping.Statement] { + return func(input []rune) Result[mapping.Statement] { res := p(input) if res.Err != nil { - return Fail[*mapping.Statement](res.Err, input) + return Fail[mapping.Statement](res.Err, input) } if !enabled { - return Fail[*mapping.Statement]( + return Fail[mapping.Statement]( NewFatalError(input, errors.New("setting meta fields is not allowed within this block")), input, ) } resSlice := res.Payload - return Success(mapping.NewStatement( + return Success[mapping.Statement](mapping.NewSingleStatement( input, mapping.NewMetaAssignment(resSlice[2].(*string)), resSlice[6].(query.Function), @@ -447,7 +447,7 @@ func pathParser(input []rune) Result[[]string] { return Success(path, res.Remaining) } -func plainMappingStatementParser(pCtx Context) Func[*mapping.Statement] { +func plainMappingStatementParser(pCtx Context) Func[mapping.Statement] { p := Sequence( FuncAsAny(pathParser), FuncAsAny(SpacesAndTabs), @@ -456,10 +456,10 @@ func plainMappingStatementParser(pCtx Context) Func[*mapping.Statement] { FuncAsAny(queryParser(pCtx)), ) - return func(input []rune) Result[*mapping.Statement] { + return func(input []rune) Result[mapping.Statement] { res := p(input) if res.Err != nil { - return Fail[*mapping.Statement](res.Err, input) + return Fail[mapping.Statement](res.Err, input) } resSlice := res.Payload @@ -469,7 +469,7 @@ func plainMappingStatementParser(pCtx Context) Func[*mapping.Statement] { path = path[1:] } - return Success(mapping.NewStatement( + return Success[mapping.Statement](mapping.NewSingleStatement( input, mapping.NewJSONAssignment(path...), resSlice[4].(query.Function), diff --git a/internal/bloblang/parser/root_expression_parser.go b/internal/bloblang/parser/root_expression_parser.go new file mode 100644 index 0000000000..f521f2d0c5 --- /dev/null +++ b/internal/bloblang/parser/root_expression_parser.go @@ -0,0 +1,109 @@ +package parser + +import ( + "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" + "github.com/benthosdev/benthos/v4/internal/bloblang/query" +) + +func rootLevelIfExpressionParser(pCtx Context) Func[mapping.Statement] { + return func(input []rune) Result[mapping.Statement] { + ifParser := Sequence( + FuncAsAny(Expect(Term("if"), "assignment")), + FuncAsAny(SpacesAndTabs), + FuncAsAny(MustBe(queryParser(pCtx))), + FuncAsAny(DiscardedWhitespaceNewlineComments), + FuncAsAny(DelimitedPattern( + Sequence( + charSquigOpen, + DiscardedWhitespaceNewlineComments, + ), + mappingStatement(pCtx, true, nil), + Sequence( + Discard(SpacesAndTabs), + NewlineAllowComment, + DiscardedWhitespaceNewlineComments, + ), + Sequence( + DiscardedWhitespaceNewlineComments, + charSquigClose, + ), + )), + ) + + elseIfParser := Optional(Sequence( + FuncAsAny(DiscardedWhitespaceNewlineComments), + FuncAsAny(Term("else if")), + FuncAsAny(SpacesAndTabs), + FuncAsAny(MustBe(queryParser(pCtx))), + FuncAsAny(DiscardedWhitespaceNewlineComments), + FuncAsAny(DelimitedPattern( + Sequence( + charSquigOpen, + DiscardedWhitespaceNewlineComments, + ), + mappingStatement(pCtx, true, nil), + Sequence( + Discard(SpacesAndTabs), + NewlineAllowComment, + DiscardedWhitespaceNewlineComments, + ), + Sequence( + DiscardedWhitespaceNewlineComments, + charSquigClose, + ), + )), + )) + + elseParser := Optional(Sequence( + FuncAsAny(DiscardedWhitespaceNewlineComments), + FuncAsAny(Term("else")), + FuncAsAny(DiscardedWhitespaceNewlineComments), + FuncAsAny(DelimitedPattern( + Sequence( + charSquigOpen, + DiscardedWhitespaceNewlineComments, + ), + mappingStatement(pCtx, true, nil), + Sequence( + Discard(SpacesAndTabs), + NewlineAllowComment, + DiscardedWhitespaceNewlineComments, + ), + Sequence( + DiscardedWhitespaceNewlineComments, + charSquigClose, + ), + )), + )) + + res := ifParser(input) + if res.Err != nil { + return Fail[mapping.Statement](res.Err, input) + } + + seqSlice := res.Payload + stmt := mapping.NewRootLevelIfStatement(input) + stmt.Add(seqSlice[2].(query.Function), seqSlice[4].([]mapping.Statement)...) + + for { + res = elseIfParser(res.Remaining) + if res.Err != nil { + return Fail[mapping.Statement](res.Err, input) + } + if res.Payload == nil { + break + } + seqSlice = res.Payload + stmt.Add(seqSlice[3].(query.Function), seqSlice[5].([]mapping.Statement)...) + } + + res = elseParser(res.Remaining) + if res.Err != nil { + return Fail[mapping.Statement](res.Err, input) + } + if seqSlice = res.Payload; seqSlice != nil { + stmt.Add(nil, seqSlice[3].([]mapping.Statement)...) + } + return Success[mapping.Statement](stmt, res.Remaining) + } +} diff --git a/internal/bloblang/parser/root_expression_parser_test.go b/internal/bloblang/parser/root_expression_parser_test.go new file mode 100644 index 0000000000..be2fcdb250 --- /dev/null +++ b/internal/bloblang/parser/root_expression_parser_test.go @@ -0,0 +1,149 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/benthosdev/benthos/v4/internal/message" +) + +func TestRootExpressionMappings(t *testing.T) { + type part struct { + Content string + Meta map[string]any + } + + tests := map[string]struct { + index int + mapping string + io [][2]part + }{ + "root level if statement": { + mapping: ` +if this.foo > this.bar { + root.a = "foo was bigger than bar" + root.b = "yep, agreed" +} +root.c = "idk" +`, + io: [][2]part{ + { + {Content: `{"foo":5,"bar":3}`}, + {Content: `{"a":"foo was bigger than bar","b":"yep, agreed","c":"idk"}`}, + }, + { + {Content: `{"foo":2,"bar":3}`}, + {Content: `{"c":"idk"}`}, + }, + }, + }, + "root level if/else statement": { + mapping: ` +if this.foo > this.bar { + +root.a = "foo was bigger than bar" + +root.b = "yep, agreed" + +} else { root.c = "idk" } +`, + io: [][2]part{ + { + {Content: `{"foo":5,"bar":3}`}, + {Content: `{"a":"foo was bigger than bar","b":"yep, agreed"}`}, + }, + { + {Content: `{"foo":2,"bar":3}`}, + {Content: `{"c":"idk"}`}, + }, + }, + }, + "root level if/elseif/else statement": { + mapping: ` +if this.foo > this.bar { + root.a = "foo was bigger than bar" + root.b = "yep, agreed" +} else if this.foo == this.bar { + root.c = "idk" +} else { + root.d = "heh, nice" +} +`, + io: [][2]part{ + { + {Content: `{"foo":5,"bar":3}`}, + {Content: `{"a":"foo was bigger than bar","b":"yep, agreed"}`}, + }, + { + {Content: `{"foo":2,"bar":2}`}, + {Content: `{"c":"idk"}`}, + }, + { + {Content: `{"foo":2,"bar":3}`}, + {Content: `{"d":"heh, nice"}`}, + }, + }, + }, + "root level meta assignments": { + mapping: ` +root = "" +if this.foo > this.bar { + meta a = "foo was bigger than bar" + meta b = "yep, agreed" +} else if this.foo == this.bar { + meta c = "idk" +} else { + meta = {"d": "heh, nice"} +} +`, + io: [][2]part{ + { + {Content: `{"foo":5,"bar":3}`}, + {Meta: map[string]any{"a": "foo was bigger than bar", "b": "yep, agreed"}}, + }, + { + {Content: `{"foo":2,"bar":2}`}, + {Meta: map[string]any{"c": "idk"}}, + }, + { + {Content: `{"foo":2,"bar":3}`}, + {Meta: map[string]any{"d": "heh, nice"}}, + }, + }, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + for _, io := range test.io { + inPart := message.NewPart([]byte(io[0].Content)) + for k, v := range io[0].Meta { + inPart.MetaSetMut(k, v) + } + + if io[1].Meta == nil { + io[1].Meta = map[string]any{} + } + + exec, perr := ParseMapping(GlobalContext(), test.mapping) + require.Nil(t, perr) + + resPart, err := exec.MapPart(test.index, message.Batch{inPart}) + require.NoError(t, err) + + outPart := part{ + Content: string(resPart.AsBytes()), + Meta: map[string]any{}, + } + _ = resPart.MetaIterMut(func(k string, v any) error { + outPart.Meta[k] = v + return nil + }) + assert.Equal(t, io[1], outPart) + } + }) + } +} diff --git a/website/docs/guides/bloblang/about.md b/website/docs/guides/bloblang/about.md index fd78b09e3c..d5c90592dd 100644 --- a/website/docs/guides/bloblang/about.md +++ b/website/docs/guides/bloblang/about.md @@ -192,20 +192,26 @@ For more information about these operators and how they work check out [the arit ## Conditional Mapping -Use `if` expressions to perform maps conditionally: +Use `if` as either a statement or an expression in order to perform maps conditionally: ```coffee root = this + root.sorted_foo = if this.foo.type() == "array" { this.foo.sort() } -# In: {"foo":"foobar"} -# Out: {"foo":"foobar"} +if this.foo.type() == "string" { + root.upper_foo = this.foo.uppercase() + root.lower_foo = this.foo.lowercase() +} + +# In: {"foo":"FooBar"} +# Out: {"foo":"FooBar","lower_foo":"foobar","upper_foo":"FOOBAR"} # In: {"foo":["foo","bar"]} # Out: {"foo":["foo","bar"],"sorted_foo":["bar","foo"]} ``` -And add as many `if else` queries as you like, followed by an optional final fallback `else`: +And add as many `else if` queries as you like, followed by an optional final fallback `else`: ```coffee root.sound = if this.type == "cat" { diff --git a/website/docs/guides/bloblang/walkthrough.md b/website/docs/guides/bloblang/walkthrough.md index fdaca69ead..1db8a552bd 100644 --- a/website/docs/guides/bloblang/walkthrough.md +++ b/website/docs/guides/bloblang/walkthrough.md @@ -227,6 +227,31 @@ root.pet.treats = if this.pet.is_cute { This is possible because field deletions are expressed as assigned values created with the `deleted()` function. This is cool but also in poor taste, treats should be allocated based on need, not cuteness! +### If Statement + +The `if` keyword can also be used as a statement in order to conditionally apply a series of mapping assignments, the previous example can be rewritten as: + +```coffee +root = this +if this.pet.is_cute { + root.pet.treats = this.pet.treats + 10 +} else { + root.pet.treats = deleted() +} +``` + +Converting this mapping to use a statement has resulted in a more verbose mapping as we had to specify `root.pet.treats` multiple times as an assignment target. However, using `if` as a statement can be beneficial when multiple assignments rely on the same logic: + +```coffee +root = this +if this.pet.is_cute { + root.pet.treats = this.pet.treats + 10 + root.pet.toys = this.pet.toys + 10 +} +``` + +More treats *and* more toys! Lucky Spot! + ### Match Expression Another conditional expression is `match` which allows you to list many branches consisting of a condition and a query to execute separated with `=>`, where the first condition to pass is the one that is executed: diff --git a/website/package.json b/website/package.json index f7392f716d..c6d0a4ea4d 100755 --- a/website/package.json +++ b/website/package.json @@ -7,7 +7,7 @@ }, "scripts": { "prestart": "sh build_plugins.sh", - "start": "docusaurus start", + "start": "docusaurus start -h 0.0.0.0", "prebuild": "sh build_plugins.sh", "build": "docusaurus build", "swizzle": "docusaurus swizzle",