Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for interpolated URLs. #75

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions public/service/config_interpolated_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package service

import (
"fmt"
"strings"

"github.com/redpanda-data/benthos/v4/internal/docs"
)

// NewInterpolatedURLField defines a new config field that describes a
// dynamic URL that supports Bloblang interpolation functions. It is then
// possible to extract an *FieldInterpolatedURL from the resulting parsed config
// with the method FieldInterpolatedURL.
func NewInterpolatedURLField(name string) *ConfigField {
tf := docs.FieldURL(name, "").IsInterpolated()
return &ConfigField{field: tf}
}

// FieldInterpolatedURL accesses a field from a parsed config that was
// defined with NewInterpolatedURLField and returns either an
// *InterpolatedURL or an error if the string was invalid.
func (p *ParsedConfig) FieldInterpolatedURL(path ...string) (*InterpolatedURL, error) {
v, exists := p.i.Field(path...)
if !exists {
return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, "."))
}

str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("expected field '%v' to be a string, got %T", strings.Join(path, "."), v)
}

e, err := p.mgr.BloblEnvironment().NewField(str)
if err != nil {
return nil, fmt.Errorf("failed to parse interpolated field '%v': %v", strings.Join(path, "."), err)
}

return &InterpolatedURL{expr: e}, nil
}
57 changes: 57 additions & 0 deletions public/service/interpolated_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package service

import (
"net/url"

"github.com/redpanda-data/benthos/v4/internal/bloblang"
"github.com/redpanda-data/benthos/v4/internal/bloblang/field"
)

// InterpolatedURL resolves a URL containing dynamic interpolation
// functions for a given message.
type InterpolatedURL struct {
expr *field.Expression
}

// NewInterpolatedURL parses an interpolated URL expression.
func NewInterpolatedURL(expr string) (*InterpolatedURL, error) {
e, err := bloblang.GlobalEnvironment().NewField(expr)
if err != nil {
return nil, err
}
return &InterpolatedURL{expr: e}, nil
}

// Static returns the underlying contents of the interpolated URL only if it
// contains zero dynamic expressions, and is therefore static, otherwise an
// empty string is returned. A second boolean parameter is also returned
// indicating whether the URL was static, helping to distinguish between a
// static empty URL versus a non-static URL.
func (i *InterpolatedURL) Static() (*url.URL, bool) {
if i.expr.NumDynamicExpressions() > 0 {
return nil, false
}
s, _ := i.expr.String(0, nil)

u, err := url.Parse(s)
if err != nil {
return nil, false
}

return u, true
}

// TryURL resolves the interpolated field for a given message as a URL,
// returns an error if any interpolation functions fail.
func (i *InterpolatedURL) TryURL(m *Message) (*url.URL, error) {
s, err := i.expr.String(0, fauxOldMessage{m.part})
if err != nil {
return nil, err
}

u, err := url.Parse(s)
if err != nil {
return nil, err
}
return u, nil
}
94 changes: 94 additions & 0 deletions public/service/interpolated_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package service

import (
"net/url"
"testing"

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

func TestInterpolatedURL(t *testing.T) {
t.Parallel()

tests := []struct {
name string
expr string
msg *Message
expected *url.URL
}{
{
name: "content interpolation",
expr: `http://foo.com/${! content() }/bar`,
msg: NewMessage([]byte("hello world")),
expected: mustParseURL(`http://foo.com/hello world/bar`),
},
{
name: "no interpolation",
expr: `https://foo.bar`,
msg: NewMessage([]byte("hello world")),
expected: mustParseURL(`https://foo.bar`),
},
{
name: "metadata interpolation",
expr: `http://foo.com/${! meta("var1") }/bar`,
msg: func() *Message {
m := NewMessage([]byte("hello world"))
m.MetaSet("var1", "value1")
return m
}(),
expected: mustParseURL("http://foo.com/value1/bar"),
},
}

for _, test := range tests {
test := test

t.Run("api/"+test.name, func(t *testing.T) {
t.Parallel()

i, err := NewInterpolatedURL(test.expr)
require.NoError(t, err)

{
got, err := i.TryURL(test.msg)
require.NoError(t, err)

assert.Equal(t, test.expected, got)
}
})
}
}

func TestInterpolatedURLCtor(t *testing.T) {
t.Parallel()

i, err := NewInterpolatedURL(`http://foo.com/${! meta("var1") bar`)

assert.EqualError(t, err, "required: expected end of expression, got: bar")
assert.Nil(t, i)
}

func TestInterpolatedURLMethods(t *testing.T) {
t.Parallel()

i, err := NewInterpolatedURL(`http://foo.com/${! meta("var1") + 1 }/bar`)
require.NoError(t, err)

m := NewMessage([]byte("hello world"))
m.MetaSet("var1", "value1")

{
got, err := i.TryURL(m)
require.EqualError(t, err, "cannot add types string (from meta field var1) and number (from number literal)")
require.Empty(t, got)
}
}

func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}