Skip to content

Commit

Permalink
Add databuilder (#826)
Browse files Browse the repository at this point in the history
* add DataBuilder

* add tests for DataBuilder

* add doc for DataBuilder

* change rb to db as the short version of "DataBuilder"
change DataBuilderKind const to "DataBuilder"

* change "subsequent request" tp "subsequent filters"

* change error log from "build request info" into "build data"

* change description from "build the result" into "building the data"

* change sample to be a simpler one
  • Loading branch information
caoshengdong committed Oct 14, 2022
1 parent b67c5ff commit c1cf5b9
Show file tree
Hide file tree
Showing 3 changed files with 351 additions and 0 deletions.
37 changes: 37 additions & 0 deletions doc/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
- [ResultBuilder](#resultbuilder)
- [Configuration](#configuration-17)
- [Results](#results-17)
- [DataBuilder](#databuilder)
- [Configuration](#configuration-18)
- [Results](#results-18)
- [Common Types](#common-types)
- [pathadaptor.Spec](#pathadaptorspec)
- [pathadaptor.RegexpReplace](#pathadaptorregexpreplace)
Expand Down Expand Up @@ -947,6 +950,40 @@ filters:
| ... |
| result9 |

## DataBuilder

DataBuilder is used to manipulate and store data. The data from the previous
filter can be transformed and stored in the context so that the data can be
used in subsequent filters.

The example below shows how to use DataBuilder to store the request body in
the context.

```yaml
- name: requestBodyDataBuilder
kind: DataBuilder
dataKey: requestBody
template: |
{{.requests.DEFAULT.JSONBody | jsonEscape}}
```

### Configuration

| Name | Type | Description | Required |
|-----------------|--------|-----------------------------------------------|----------|
| template | string | template to create data, please refer the [template](#template-of-builer-filters) for more information | Yes |
| dataKey | string | key to store data | Yes |
| leftDelim | string | left action delimiter of the template, default is `{{` | No |
| rightDelim | string | right action delimiter of the template, default is `}}` | No |


### Results

| Value | Description |
|-----------------|---------------------------------------------------|
| buildErr | Error happens when building the data |


## Common Types

### pathadaptor.Spec
Expand Down
124 changes: 124 additions & 0 deletions pkg/filters/builder/databuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2017, MegaEase
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package builder

import (
"fmt"

"github.com/megaease/easegress/pkg/context"
"github.com/megaease/easegress/pkg/filters"
"github.com/megaease/easegress/pkg/logger"
)

const (
// DataBuilderKind is the kind of DataBuilder.
DataBuilderKind = "DataBuilder"
)

var dataBuilderKind = &filters.Kind{
Name: DataBuilderKind,
Description: "DataBuilder builds and stores data",
Results: []string{resultBuildErr},
DefaultSpec: func() filters.Spec {
return &DataBuilderSpec{}
},
CreateInstance: func(spec filters.Spec) filters.Filter {
return &DataBuilder{spec: spec.(*DataBuilderSpec)}
},
}

func init() {
filters.Register(dataBuilderKind)
}

type (
// DataBuilder is filter DataBuilder.
DataBuilder struct {
spec *DataBuilderSpec
Builder
}

// DataBuilderSpec is DataBuilder Spec.
DataBuilderSpec struct {
filters.BaseSpec `json:",inline"`
Spec `json:",inline"`
DataKey string `json:"dataKey" jsonschema:"omitempty"`
}
)

// Validate validates the DataBuilder Spec.
func (spec *DataBuilderSpec) Validate() error {
if spec.DataKey == "" {
return fmt.Errorf("dataKey must be specified")
}

if spec.Template == "" {
return fmt.Errorf("template must be specified")
}

return spec.Spec.Validate()
}

// Name returns the name of the DataBuilder filter instance.
func (db *DataBuilder) Name() string {
return db.spec.Name()
}

// Kind returns the kind of DataBuilder.
func (db *DataBuilder) Kind() *filters.Kind {
return dataBuilderKind
}

// Spec returns the spec used by the DataBuilder
func (db *DataBuilder) Spec() filters.Spec {
return db.spec
}

// Init initializes DataBuilder.
func (db *DataBuilder) Init() {
db.reload()
}

// Inherit inherits previous generation of DataBuilder.
func (db *DataBuilder) Inherit(previousGeneration filters.Filter) {
db.Init()
}

func (db *DataBuilder) reload() {
db.Builder.reload(&db.spec.Spec)
}

// Handle builds request.
func (db *DataBuilder) Handle(ctx *context.Context) (result string) {
data, err := prepareBuilderData(ctx)

if err != nil {
logger.Warnf("prepareBuilderData failed: %v", err)
return resultBuildErr
}

var r interface{}
if err = db.build(data, &r); err != nil {
msgFmt := "DataBuilder(%s): failed to build data: %v"
logger.Warnf(msgFmt, db.Name(), err)
return resultBuildErr
}

ctx.SetData(db.spec.DataKey, r)
return ""
}
190 changes: 190 additions & 0 deletions pkg/filters/builder/databuilder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* Copyright (c) 2017, MegaEase
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package builder

import (
"testing"

"github.com/megaease/easegress/pkg/context"
"github.com/megaease/easegress/pkg/util/codectool"
"github.com/stretchr/testify/assert"
)

func getDataBuilder(spec *DataBuilderSpec) *DataBuilder {
db := &DataBuilder{spec: spec}
db.Init()
return db
}

func TestDataBuilder(t *testing.T) {
assert := assert.New(t)

// test empty data
yamlConfig := `
dataKey: testKey
template: |
{}
`
{
spec := &DataBuilderSpec{}
codectool.MustUnmarshal([]byte(yamlConfig), spec)

db := getDataBuilder(spec)
db.Init()
assert.NoError(db.spec.Validate())
defer db.Close()

assert.Equal(dataBuilderKind, db.Kind())
ctx := context.New(nil)

res := db.Handle(ctx)
assert.Empty(res)

assert.Equal(map[string]interface{}{}, ctx.GetData("testKey"))
}

// test sample data
yamlConfig = `
dataKey: testKey
template: |
{
"name": "Bob",
"age": 18,
"address": {
"city": "Beijing",
"street": "Changan street"
}
}
`
{
spec := &DataBuilderSpec{}
codectool.MustUnmarshal([]byte(yamlConfig), spec)
db := getDataBuilder(spec)
db.Init()
assert.NoError(db.spec.Validate())
defer db.Close()

assert.Equal(dataBuilderKind, db.Kind())
ctx := context.New(nil)

res := db.Handle(ctx)
assert.Empty(res)

data := ctx.GetData("testKey")
assert.IsType(map[string]interface{}{}, data)
assert.EqualValues(map[string]interface{}{"name": "Bob", "age": 18, "address": map[string]interface{}{"city": "Beijing", "street": "Changan street"}}, data)
}

// test array
yamlConfig = `
dataKey: testKey
template: |
[{
"name": "Bob",
"age": 18,
"address": {
"city": "Beijing",
"street": "Changan street"
}
},
{
"name": "Alice",
"age": 20,
"address": {
"city": "Shanghai",
"street": "Xuhui street"
}
}]
`
{
spec := &DataBuilderSpec{}
codectool.MustUnmarshal([]byte(yamlConfig), spec)
db := getDataBuilder(spec)

db.Init()
assert.NoError(db.spec.Validate())
defer db.Close()

assert.Equal(dataBuilderKind, db.Kind())
ctx := context.New(nil)

res := db.Handle(ctx)
assert.Empty(res)

data := ctx.GetData("testKey")
assert.IsType([]interface{}{}, data)

var expected = make([]interface{}, 2)
expected[0] = map[string]interface{}{"name": "Bob", "age": 18, "address": map[string]interface{}{"city": "Beijing", "street": "Changan street"}}
expected[1] = map[string]interface{}{"name": "Alice", "age": 20, "address": map[string]interface{}{"city": "Shanghai", "street": "Xuhui street"}}
assert.EqualValues(expected, data)
}

// test array
yamlConfig = `
dataKey: testKey
template: |
[{
"name": "Bob",
"age": 18,
"address": {
"city": "Beijing",
"street": "Changan street"
},
"friends": [
"Alice", "Tom", "Jack"
]
}]
`
{
spec := &DataBuilderSpec{}
codectool.MustUnmarshal([]byte(yamlConfig), spec)
db := getDataBuilder(spec)

db.Init()
assert.NoError(db.spec.Validate())
defer db.Close()

assert.Equal(dataBuilderKind, db.Kind())
ctx := context.New(nil)

res := db.Handle(ctx)
assert.Empty(res)

data := ctx.GetData("testKey")
assert.IsType([]interface{}{}, data)

var expected = make([]interface{}, 1)
expected[0] = map[string]interface{}{"name": "Bob", "age": 18, "address": map[string]interface{}{"city": "Beijing", "street": "Changan street"}, "friends": []interface{}{"Alice", "Tom", "Jack"}}
assert.EqualValues(expected, data)
}

// test dataKey is empty
yamlConfig = `
template: |
{}
`
{
spec := &DataBuilderSpec{}
codectool.MustUnmarshal([]byte(yamlConfig), spec)
db := getDataBuilder(spec)
db.Init()
assert.Error(db.spec.Validate())
defer db.Close()
}
}

0 comments on commit c1cf5b9

Please sign in to comment.