Skip to content

Commit

Permalink
Add deactivation method to blobl environments
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeffail committed Sep 5, 2021
1 parent b24bb5d commit dba8e11
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 4 deletions.
16 changes: 16 additions & 0 deletions internal/bloblang/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ func (e *Environment) NewMapping(path, blobl string) (*mapping.Executor, error)
return exec, nil
}

// Deactivated returns a version of the environment where constructors are
// disabled for all functions and methods, allowing mappings to be parsed and
// validated but not executed.
//
// The underlying register of functions and methods is shared with the target
// environment, and therefore functions/methods registered to this set will also
// be added to the still activated environment. Use the Without methods (with
// empty args if applicable) in order to create a deep copy of the environment
// that is independent of the source.
func (e *Environment) Deactivated() *Environment {
return &Environment{
functions: e.functions.Deactivated(),
methods: e.methods.Deactivated(),
}
}

// RegisterMethod adds a new Bloblang method to the environment.
func (e *Environment) RegisterMethod(spec query.MethodSpec, ctor query.MethodCtor) error {
return e.methods.Add(spec, ctor)
Expand Down
26 changes: 25 additions & 1 deletion internal/bloblang/query/function_set.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package query

import (
"errors"
"fmt"
"regexp"
"sort"
Expand All @@ -9,6 +10,7 @@ import (
// FunctionSet contains an explicit set of functions to be available in a
// Bloblang query.
type FunctionSet struct {
disableCtors bool
constructors map[string]FunctionCtor
specs map[string]FunctionSpec
}
Expand Down Expand Up @@ -70,6 +72,9 @@ func (f *FunctionSet) Init(name string, args *ParsedParams) (Function, error) {
if !exists {
return nil, badFunctionErr(name)
}
if f.disableCtors {
return disabledFunction(name), nil
}
return wrapCtorWithDynamicArgs(name, args, ctor)
}

Expand All @@ -94,7 +99,7 @@ func (f *FunctionSet) Without(functions ...string) *FunctionSet {
specs[v.Name] = v
}
}
return &FunctionSet{constructors, specs}
return &FunctionSet{f.disableCtors, constructors, specs}
}

// OnlyPure creates a clone of the function set that can be mutated in
Expand All @@ -121,6 +126,19 @@ func (f *FunctionSet) NoMessage() *FunctionSet {
return f.Without(excludes...)
}

// Deactivated returns a version of the function set where constructors are
// disabled, allowing mappings to be parsed and validated but not executed.
//
// The underlying register of functions is shared with the target set, and
// therefore functions added to this set will also be added to the still
// activated set. Use the Without method (with empty args if applicable) in
// order to create a deep copy of the set that is independent of the source.
func (f *FunctionSet) Deactivated() *FunctionSet {
newSet := *f
newSet.disableCtors = true
return &newSet
}

//------------------------------------------------------------------------------

// AllFunctions is a set containing every single function declared by this
Expand Down Expand Up @@ -166,6 +184,12 @@ func FunctionDocs() []FunctionSpec {

//------------------------------------------------------------------------------

func disabledFunction(name string) Function {
return ClosureFunction("function "+name, func(ctx FunctionContext) (interface{}, error) {
return nil, errors.New("this function has been disabled")
}, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil })
}

func wrapCtorWithDynamicArgs(name string, args *ParsedParams, fn FunctionCtor) (Function, error) {
fns := args.dynamic()
if len(fns) == 0 {
Expand Down
34 changes: 34 additions & 0 deletions internal/bloblang/query/function_set_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package query

import (
"errors"
"sort"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func listFunctions(f *FunctionSet) []string {
Expand Down Expand Up @@ -44,6 +46,38 @@ func TestFunctionSetOnlyPure(t *testing.T) {
assert.NotContains(t, listFunctions(setTwo), "file")
}

func TestFunctionSetDeactivated(t *testing.T) {
setOne := AllFunctions.Without()
setTwo := setOne.Deactivated()

customErr := errors.New("custom error")

spec := NewFunctionSpec(FunctionCategoryGeneral, "meow", "").Param(ParamString("val1", ""))
require.NoError(t, setOne.Add(spec, func(args *ParsedParams) (Function, error) {
return ClosureFunction("", func(ctx FunctionContext) (interface{}, error) {
return nil, customErr
}, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil
}))

assert.Contains(t, listFunctions(setOne), "meow")
assert.Contains(t, listFunctions(setTwo), "meow")

goodArgs, err := spec.Params.PopulateNameless("hello")
require.NoError(t, err)

fnOne, err := setOne.Init("meow", goodArgs)
require.NoError(t, err)

fnTwo, err := setTwo.Init("meow", goodArgs)
require.NoError(t, err)

_, err = fnOne.Exec(FunctionContext{})
assert.Equal(t, customErr, err)

_, err = fnTwo.Exec(FunctionContext{})
assert.EqualError(t, err, "this function has been disabled")
}

func TestFunctionBadName(t *testing.T) {
testCases := map[string]string{
"!no": "function name '!no' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/",
Expand Down
26 changes: 25 additions & 1 deletion internal/bloblang/query/method_set.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package query

import (
"errors"
"fmt"
"sort"
)

// MethodSet contains an explicit set of methods to be available in a Bloblang
// query.
type MethodSet struct {
disableCtors bool
constructors map[string]MethodCtor
specs map[string]MethodSpec
}
Expand Down Expand Up @@ -66,6 +68,9 @@ func (m *MethodSet) Init(name string, target Function, args *ParsedParams) (Func
if !exists {
return nil, badMethodErr(name)
}
if m.disableCtors {
return disabledMethod(name), nil
}
return wrapMethodCtorWithDynamicArgs(name, target, args, ctor)
}

Expand All @@ -90,7 +95,20 @@ func (m *MethodSet) Without(methods ...string) *MethodSet {
specs[v.Name] = v
}
}
return &MethodSet{constructors, specs}
return &MethodSet{m.disableCtors, constructors, specs}
}

// Deactivated returns a version of the method set where constructors are
// disabled, allowing mappings to be parsed and validated but not executed.
//
// The underlying register of methods is shared with the target set, and
// therefore methods added to this set will also be added to the still activated
// set. Use the Without method (with empty args if applicable) in order to
// create a deep copy of the set that is independent of the source.
func (m *MethodSet) Deactivated() *MethodSet {
newSet := *m
newSet.disableCtors = true
return &newSet
}

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -129,6 +147,12 @@ func MethodDocs() []MethodSpec {

//------------------------------------------------------------------------------

func disabledMethod(name string) Function {
return ClosureFunction("method "+name, func(ctx FunctionContext) (interface{}, error) {
return nil, errors.New("this method has been disabled")
}, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil })
}

func wrapMethodCtorWithDynamicArgs(name string, target Function, args *ParsedParams, fn MethodCtor) (Function, error) {
fns := args.dynamic()
if len(fns) == 0 {
Expand Down
33 changes: 33 additions & 0 deletions internal/bloblang/query/method_set_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package query

import (
"errors"
"sort"
"testing"

Expand Down Expand Up @@ -42,6 +43,38 @@ func TestMethodSetWithout(t *testing.T) {
assert.NoError(t, err)
}

func TestMethodSetDeactivated(t *testing.T) {
setOne := AllMethods.Without()
setTwo := setOne.Deactivated()

customErr := errors.New("custom error")

spec := NewMethodSpec("meow", "").Param(ParamString("val1", ""))
require.NoError(t, setOne.Add(spec, func(target Function, args *ParsedParams) (Function, error) {
return ClosureFunction("", func(ctx FunctionContext) (interface{}, error) {
return nil, customErr
}, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil
}))

assert.Contains(t, listMethods(setOne), "meow")
assert.Contains(t, listMethods(setTwo), "meow")

goodArgs, err := spec.Params.PopulateNameless("hello")
require.NoError(t, err)

fnOne, err := setOne.Init("meow", NewLiteralFunction("", nil), goodArgs)
require.NoError(t, err)

fnTwo, err := setTwo.Init("meow", NewLiteralFunction("", nil), goodArgs)
require.NoError(t, err)

_, err = fnOne.Exec(FunctionContext{})
assert.Equal(t, customErr, err)

_, err = fnTwo.Exec(FunctionContext{})
assert.EqualError(t, err, "this method has been disabled")
}

func TestMethodBadName(t *testing.T) {
testCases := map[string]string{
"!no": "method name '!no' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/",
Expand Down
2 changes: 1 addition & 1 deletion internal/docs/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ func NewLintContext() LintContext {
return LintContext{
LabelsToLine: map[string]int{},
DocsProvider: globalProvider,
BloblangEnv: bloblang.GlobalEnvironment(),
BloblangEnv: bloblang.GlobalEnvironment().Deactivated(),
}
}

Expand Down
21 changes: 21 additions & 0 deletions public/bloblang/spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package bloblang

import (
"encoding/json"

"github.com/Jeffail/benthos/v3/internal/bloblang/query"
)

Expand Down Expand Up @@ -107,6 +109,25 @@ func (p *PluginSpec) Param(def ParamDefinition) *PluginSpec {
return p
}

// EncodeJSON attempts to parse a JSON object as a byte slice and uses it to
// populate the configuration spec. The schema of this method is undocumented
// and is not intended for general use.
//
// EXPERIMENTAL: This method is not intended for general use and could have its
// signature and/or behaviour changed outside of major version bumps.
func (p *PluginSpec) EncodeJSON(v []byte) error {
def := struct {
Description string `json:"description"`
Params query.Params `json:"params"`
}{}
if err := json.Unmarshal(v, &def); err != nil {
return err
}
p.description = def.Description
p.params = def.Params
return nil
}

//------------------------------------------------------------------------------

// ParsedParams is a reference to the arguments of a method or function
Expand Down
2 changes: 1 addition & 1 deletion public/service/stream_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func NewStreamBuilder() *StreamBuilder {
func (s *StreamBuilder) getLintContext() docs.LintContext {
ctx := docs.NewLintContext()
ctx.DocsProvider = s.env.internal
ctx.BloblangEnv = s.env.getBloblangParserEnv()
ctx.BloblangEnv = s.env.getBloblangParserEnv().Deactivated()
return ctx
}

Expand Down

0 comments on commit dba8e11

Please sign in to comment.