Skip to content

Commit

Permalink
Prototype Defaulter and Validator for River. (grafana#3998)
Browse files Browse the repository at this point in the history
* Defaulter and Validator interfaces for River.

Signed-off-by: erikbaranowski <39704712+erikbaranowski@users.noreply.github.com>

---------

Signed-off-by: erikbaranowski <39704712+erikbaranowski@users.noreply.github.com>
  • Loading branch information
erikbaranowski committed Jun 1, 2023
1 parent 67ccfa7 commit c383ac9
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 35 deletions.
32 changes: 13 additions & 19 deletions component/prometheus/scrape/scrape.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,21 @@ type Clustering struct {
Enabled bool `river:"enabled,attr"`
}

// DefaultArguments defines the default settings for a scrape job.
var DefaultArguments = Arguments{
MetricsPath: "/metrics",
Scheme: "http",
HonorLabels: false,
HonorTimestamps: true,
HTTPClientConfig: component_config.DefaultHTTPClientConfig,
ScrapeInterval: 1 * time.Minute, // From config.DefaultGlobalConfig
ScrapeTimeout: 10 * time.Second, // From config.DefaultGlobalConfig
}

// UnmarshalRiver implements river.Unmarshaler.
func (arg *Arguments) UnmarshalRiver(f func(interface{}) error) error {
*arg = DefaultArguments

type args Arguments
err := f((*args)(arg))
if err != nil {
return err
// SetToDefault implements river.Defaulter.
func (arg *Arguments) SetToDefault() {
*arg = Arguments{
MetricsPath: "/metrics",
Scheme: "http",
HonorLabels: false,
HonorTimestamps: true,
HTTPClientConfig: component_config.DefaultHTTPClientConfig,
ScrapeInterval: 1 * time.Minute, // From config.DefaultGlobalConfig
ScrapeTimeout: 10 * time.Second, // From config.DefaultGlobalConfig
}
}

// Validate implements river.Validator.
func (arg *Arguments) Validate() error {
// We must explicitly Validate because HTTPClientConfig is squashed and it won't run otherwise
return arg.HTTPClientConfig.Validate()
}
Expand Down
3 changes: 2 additions & 1 deletion component/prometheus/scrape/scrape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func TestForwardingToAppendable(t *testing.T) {

nilReceivers := []storage.Appendable{nil, nil}

args := DefaultArguments
var args Arguments
args.SetToDefault()
args.ForwardTo = nilReceivers

s, err := New(opts, args)
Expand Down
35 changes: 34 additions & 1 deletion pkg/river/internal/value/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,29 @@ import (
"github.com/grafana/agent/pkg/river/internal/reflectutil"
)

// The Defaulter interface allows a type to implement default functionality
// in River evaluation.
type Defaulter interface {
// SetToDefault is called when evaluating a block or body to set the value
// to its defaults.
SetToDefault()
}

// Unmarshaler is a custom type which can be used to hook into the decoder.
type Unmarshaler interface {
// UnmarshalRiver is called when decoding a value. f should be invoked to
// continue decoding with a value to decode into.
UnmarshalRiver(f func(v interface{}) error) error
}

// The Validator interface allows a type to implement validation functionality
// in River evaluation.
type Validator interface {
// Validate is called when evaluating a block or body to enforce the
// value is valid.
Validate() error
}

// Decode assigns a Value val to a Go pointer target. Pointers will be
// allocated as necessary when decoding.
//
Expand Down Expand Up @@ -70,7 +86,18 @@ type decoder struct {
makeCopy bool
}

func (d *decoder) decode(val Value, into reflect.Value) error {
func (d *decoder) decode(val Value, into reflect.Value) (err error) {
// If everything has decoded successfully, run Validate if implemented.
defer func() {
if err == nil {
if into.CanAddr() && into.Addr().Type().Implements(goRiverValidator) {
err = into.Addr().Interface().(Validator).Validate()
} else if into.Type().Implements(goRiverValidator) {
err = into.Interface().(Validator).Validate()
}
}
}()

// Store the raw value from val and try to address it so we can do underlying
// type match assignment.
rawValue := val.rv
Expand Down Expand Up @@ -126,6 +153,12 @@ func (d *decoder) decode(val Value, into reflect.Value) error {
return err
}

if into.CanAddr() && into.Addr().Type().Implements(goRiverDefaulter) {
into.Addr().Interface().(Defaulter).SetToDefault()
} else if into.Type().Implements(goRiverDefaulter) {
into.Interface().(Defaulter).SetToDefault()
}

targetType := RiverType(into.Type())

// Track a value to use for decoding. This value will be updated if
Expand Down
23 changes: 18 additions & 5 deletions pkg/river/internal/value/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@ func TestDecode_CustomTypes(t *testing.T) {
t.Run("object to Unmarshaler", func(t *testing.T) {
var actual customUnmarshaler
require.NoError(t, value.Decode(value.Object(nil), &actual))
require.True(t, actual.Called, "UnmarshalRiver was not invoked")
require.True(t, actual.UnmarshalCalled, "UnmarshalRiver was not invoked")
require.True(t, actual.DefaultCalled, "SetToDefault was not invoked")
require.True(t, actual.ValidateCalled, "Validate was not invoked")
})

t.Run("TextMarshaler to TextUnmarshaler", func(t *testing.T) {
Expand Down Expand Up @@ -285,14 +287,25 @@ func TestDecode_CustomTypes(t *testing.T) {
}

type customUnmarshaler struct {
Called bool `river:"called,attr,optional"`
UnmarshalCalled bool `river:"unmarshal_called,attr,optional"`
DefaultCalled bool `river:"default_called,attr,optional"`
ValidateCalled bool `river:"validate_called,attr,optional"`
}

func (cu *customUnmarshaler) UnmarshalRiver(f func(interface{}) error) error {
cu.Called = true
cu.UnmarshalCalled = true
return f((*customUnmarshalerTarget)(cu))
}

type customUnmarshalerTarget customUnmarshaler

func (s *customUnmarshalerTarget) SetToDefault() {
s.DefaultCalled = true
}

type s customUnmarshaler
return f((*s)(cu))
func (s *customUnmarshalerTarget) Validate() error {
s.ValidateCalled = true
return nil
}

type textEnumType bool
Expand Down
2 changes: 2 additions & 0 deletions pkg/river/internal/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ var (
goCapsule = reflect.TypeOf((*Capsule)(nil)).Elem()
goDuration = reflect.TypeOf((time.Duration)(0))
goDurationPtr = reflect.TypeOf((*time.Duration)(nil))
goRiverDefaulter = reflect.TypeOf((*Defaulter)(nil)).Elem()
goRiverDecoder = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
goRiverValidator = reflect.TypeOf((*Validator)(nil)).Elem()
goRawRiverFunc = reflect.TypeOf((RawFunction)(nil))
goRiverValue = reflect.TypeOf(Null)
)
Expand Down
20 changes: 20 additions & 0 deletions pkg/river/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import "github.com/grafana/agent/pkg/river/internal/value"
// The types below must be kept in sync with the internal package, and the
// checks below ensure they're compatible.
var (
_ value.Defaulter = (Defaulter)(nil)
_ value.Unmarshaler = (Unmarshaler)(nil)
_ value.Validator = (Validator)(nil)
_ value.Capsule = (Capsule)(nil)
_ value.ConvertibleFromCapsule = (ConvertibleFromCapsule)(nil)
_ value.ConvertibleIntoCapsule = (ConvertibleIntoCapsule)(nil)
Expand All @@ -26,6 +28,24 @@ type Unmarshaler interface {
UnmarshalRiver(f func(v interface{}) error) error
}

// The Defaulter interface allows a type to implement default functionality
// in River evaluation.
type Defaulter interface {
// SetToDefault is called when evaluating a block or body to set the value
// to its defaults. SetToDefault will not be called on types which are
// squashed into the parent struct using `river:",squash"`.
SetToDefault()
}

// The Validator interface allows a type to implement validation functionality
// in River evaluation.
type Validator interface {
// Validate is called when evaluating a block or body to enforce the
// value is valid. Validate will not be called on types which are
// squashed into the parent struct using `river:",squash"`.
Validate() error
}

// Capsule is an interface marker which tells River that a type should always
// be treated as a "capsule type" instead of the default type River would
// assign.
Expand Down
34 changes: 30 additions & 4 deletions pkg/river/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,51 @@ func (vm *Evaluator) Evaluate(scope *Scope, v interface{}) (err error) {
}

func (vm *Evaluator) evaluateBlockOrBody(scope *Scope, assoc map[value.Value]ast.Node, node ast.Node, rv reflect.Value) error {
// TODO(rfratto): the errors returned by this function are missing context to
// be able to print line numbers. We need to return decorated error types.

// Before decoding the block, we need to temporarily take the address of rv
// to handle the case of it implementing the unmarshaler interface.
if rv.CanAddr() {
rv = rv.Addr()
}

if err, unmarshaled := vm.evaluateUnmarshalRiver(scope, assoc, node, rv); unmarshaled || err != nil {
return err
}

if ru, ok := rv.Interface().(value.Defaulter); ok {
ru.SetToDefault()
}

if err := vm.evaluateDecode(scope, assoc, node, rv); err != nil {
return err
}

if ru, ok := rv.Interface().(value.Validator); ok {
if err := ru.Validate(); err != nil {
return err
}
}

return nil
}

func (vm *Evaluator) evaluateUnmarshalRiver(scope *Scope, assoc map[value.Value]ast.Node, node ast.Node, rv reflect.Value) (error, bool) {
if ru, ok := rv.Interface().(value.Unmarshaler); ok {
return ru.UnmarshalRiver(func(v interface{}) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer {
panic(fmt.Sprintf("river/vm: expected pointer, got %s", rv.Kind()))
}
return vm.evaluateBlockOrBody(scope, assoc, node, rv.Elem())
})
}), true
}

return nil, false
}

func (vm *Evaluator) evaluateDecode(scope *Scope, assoc map[value.Value]ast.Node, node ast.Node, rv reflect.Value) error {
// TODO(rfratto): the errors returned by this function are missing context to
// be able to print line numbers. We need to return decorated error types.

// Fully deference rv and allocate pointers as necessary.
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
Expand Down
24 changes: 19 additions & 5 deletions pkg/river/vm/vm_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,9 @@ func TestVM_Block_Unmarshaler(t *testing.T) {

var actual OuterBlock
require.NoError(t, eval.Evaluate(nil, &actual))
require.True(t, actual.Settings.Called, "UnmarshalRiver did not get invoked")
require.True(t, actual.Settings.UnmarshalCalled, "UnmarshalRiver did not get invoked")
require.True(t, actual.Settings.DefaultCalled, "SetToDefault did not get invoked")
require.True(t, actual.Settings.ValidateCalled, "Validate did not get invoked")
}

func TestVM_Block_UnmarshalToMap(t *testing.T) {
Expand Down Expand Up @@ -727,13 +729,25 @@ type Setting struct {
FieldA string `river:"field_a,attr"`
FieldB string `river:"field_b,attr"`

Called bool
UnmarshalCalled bool
DefaultCalled bool
ValidateCalled bool
}

func (s *Setting) UnmarshalRiver(f func(interface{}) error) error {
s.Called = true
type setting Setting
return f((*setting)(s))
s.UnmarshalCalled = true
return f((*settingUnmarshalTarget)(s))
}

type settingUnmarshalTarget Setting

func (s *settingUnmarshalTarget) SetToDefault() {
s.DefaultCalled = true
}

func (s *settingUnmarshalTarget) Validate() error {
s.ValidateCalled = true
return nil
}

func parseBlock(t *testing.T, input string) *ast.BlockStmt {
Expand Down

0 comments on commit c383ac9

Please sign in to comment.