Skip to content
Merged
187 changes: 187 additions & 0 deletions configio/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package configio

import (
"io"
"net/http"
"net/url"
"time"

"github.com/benchttp/sdk/benchttp"
)

// A Builder is used to incrementally build a benchttp.Runner
// using Set and Write methods.
// The zero value is ready to use.
type Builder struct {
mutations []func(*benchttp.Runner)
}

// DecodeJSON decodes the input bytes as a JSON benchttp configuration
// and appends the resulting modifier to the builder.
// It returns any encountered during the decoding process or if the
// decoded configuration is invalid.
func (b *Builder) DecodeJSON(in []byte) error {
return b.decode(in, FormatJSON)
}

// DecodeYAML decodes the input bytes as a YAML benchttp configuration
// and appends the resulting modifier to the builder.
// It returns any encountered during the decoding process or if the
// decoded configuration is invalid.
func (b *Builder) DecodeYAML(in []byte) error {
return b.decode(in, FormatYAML)
}

func (b *Builder) decode(in []byte, format Format) error {
repr := representation{}
if err := decoderOf(format, in).decodeRepr(&repr); err != nil {
return err
}
// early check for invalid configuration
if err := repr.validate(); err != nil {
return err
}
b.append(func(dst *benchttp.Runner) {
// err is already checked via repr.validate(), so nil is expected.
if err := repr.parseAndMutate(dst); err != nil {
panicInternal("Builder.decode", "unexpected error: "+err.Error())
}
})
return nil
}

// Runner successively applies the Builder's mutations
// to a zero benchttp.Runner and returns it.
func (b *Builder) Runner() benchttp.Runner {
runner := benchttp.Runner{}
b.Mutate(&runner)
return runner
}

// Mutate successively applies the Builder's mutations
// to the benchttp.Runner value pointed to by dst.
func (b *Builder) Mutate(dst *benchttp.Runner) {
for _, mutate := range b.mutations {
mutate(dst)
}
}

// setters

// SetRequest adds a mutation that sets a runner's request to r.
func (b *Builder) SetRequest(r *http.Request) {
b.append(func(runner *benchttp.Runner) {
runner.Request = r
})
}

// SetRequestMethod adds a mutation that sets a runner's request method to v.
func (b *Builder) SetRequestMethod(v string) {
b.append(func(runner *benchttp.Runner) {
if runner.Request == nil {
runner.Request = &http.Request{}
}
runner.Request.Method = v
})
}

// SetRequestURL adds a mutation that sets a runner's request URL to v.
func (b *Builder) SetRequestURL(v *url.URL) {
b.append(func(runner *benchttp.Runner) {
if runner.Request == nil {
runner.Request = &http.Request{}
}
runner.Request.URL = v
})
}

// SetRequestHeader adds a mutation that sets a runner's request header to v.
func (b *Builder) SetRequestHeader(v http.Header) {
b.SetRequestHeaderFunc(func(_ http.Header) http.Header {
return v
})
}

// SetRequestHeaderFunc adds a mutation that sets a runner's request header
// to the result of calling f with its current request header.
func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) {
b.append(func(runner *benchttp.Runner) {
if runner.Request == nil {
runner.Request = &http.Request{}
}
runner.Request.Header = f(runner.Request.Header)
})
}

// SetRequestBody adds a mutation that sets a runner's request body to v.
func (b *Builder) SetRequestBody(v io.ReadCloser) {
b.append(func(runner *benchttp.Runner) {
if runner.Request == nil {
runner.Request = &http.Request{}
}
runner.Request.Body = v
})
}

// SetRequests adds a mutation that sets a runner's
// Requests field to v.
func (b *Builder) SetRequests(v int) {
b.append(func(runner *benchttp.Runner) {
runner.Requests = v
})
}

// SetConcurrency adds a mutation that sets a runner's
// Concurrency field to v.
func (b *Builder) SetConcurrency(v int) {
b.append(func(runner *benchttp.Runner) {
runner.Concurrency = v
})
}

// SetInterval adds a mutation that sets a runner's
// Interval field to v.
func (b *Builder) SetInterval(v time.Duration) {
b.append(func(runner *benchttp.Runner) {
runner.Interval = v
})
}

// SetRequestTimeout adds a mutation that sets a runner's
// RequestTimeout field to v.
func (b *Builder) SetRequestTimeout(v time.Duration) {
b.append(func(runner *benchttp.Runner) {
runner.RequestTimeout = v
})
}

// SetGlobalTimeout adds a mutation that sets a runner's
// GlobalTimeout field to v.
func (b *Builder) SetGlobalTimeout(v time.Duration) {
b.append(func(runner *benchttp.Runner) {
runner.GlobalTimeout = v
})
}

// SetTests adds a mutation that sets a runner's
// Tests field to v.
func (b *Builder) SetTests(v []benchttp.TestCase) {
b.append(func(runner *benchttp.Runner) {
runner.Tests = v
})
}

// SetTests adds a mutation that appends the given benchttp.TestCases
// to a runner's Tests field.
func (b *Builder) AddTests(v ...benchttp.TestCase) {
b.append(func(runner *benchttp.Runner) {
runner.Tests = append(runner.Tests, v...)
})
}

func (b *Builder) append(modifier func(runner *benchttp.Runner)) {
if modifier == nil {
panicInternal("Builder.append", "call with nil modifier")
}
b.mutations = append(b.mutations, modifier)
}
148 changes: 148 additions & 0 deletions configio/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package configio_test

import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

"github.com/benchttp/sdk/benchttp"
"github.com/benchttp/sdk/benchttptest"
"github.com/benchttp/sdk/configio"
)

func TestBuilder_WriteJSON(t *testing.T) {
in := []byte(`{"runner":{"requests": 5}}`)
dest := benchttp.Runner{Requests: 0, Concurrency: 2}
want := benchttp.Runner{Requests: 5, Concurrency: 2}

b := configio.Builder{}
if err := b.DecodeJSON(in); err != nil {
t.Fatal(err)
}
b.Mutate(&dest)

benchttptest.AssertEqualRunners(t, want, dest)
}

func TestBuilder_WriteYAML(t *testing.T) {
in := []byte(`runner: { requests: 5 }`)
dest := benchttp.Runner{Requests: 0, Concurrency: 2}
want := benchttp.Runner{Requests: 5, Concurrency: 2}

b := configio.Builder{}
if err := b.DecodeYAML(in); err != nil {
t.Fatal(err)
}
b.Mutate(&dest)

benchttptest.AssertEqualRunners(t, want, dest)
}

func TestBuilder_Set(t *testing.T) {
t.Run("basic fields", func(t *testing.T) {
want := benchttp.Runner{
Requests: 5,
Concurrency: 2,
Interval: 10 * time.Millisecond,
RequestTimeout: 1 * time.Second,
GlobalTimeout: 10 * time.Second,
}

b := configio.Builder{}
b.SetRequests(want.Requests)
b.SetConcurrency(-1)
b.SetConcurrency(want.Concurrency)
b.SetInterval(want.Interval)
b.SetRequestTimeout(want.RequestTimeout)
b.SetGlobalTimeout(want.GlobalTimeout)

benchttptest.AssertEqualRunners(t, want, b.Runner())
})

t.Run("request", func(t *testing.T) {
want := benchttp.Runner{
Request: httptest.NewRequest("GET", "https://example.com", nil),
}

b := configio.Builder{}
b.SetRequest(want.Request)

benchttptest.AssertEqualRunners(t, want, b.Runner())
})

t.Run("request fields", func(t *testing.T) {
want := benchttp.Runner{
Request: &http.Request{
Method: "PUT",
URL: mustParseRequestURI("https://example.com"),
Header: http.Header{
"API_KEY": []string{"abc"},
"Accept": []string{"text/html", "application/json"},
},
Body: readcloser("hello"),
},
}

b := configio.Builder{}
b.SetRequestMethod(want.Request.Method)
b.SetRequestURL(want.Request.URL)
b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}})
b.SetRequestHeaderFunc(func(prev http.Header) http.Header {
prev.Add("Accept", "text/html")
prev.Add("Accept", "application/json")
return prev
})
b.SetRequestBody(readcloser("hello"))

benchttptest.AssertEqualRunners(t, want, b.Runner())
})

t.Run("test cases", func(t *testing.T) {
want := benchttp.Runner{
Tests: []benchttp.TestCase{
{
Name: "maximum response time",
Field: "ResponseTimes.Max",
Predicate: "LT",
Target: 100 * time.Millisecond,
},
{
Name: "similar response times",
Field: "ResponseTimes.StdDev",
Predicate: "LTE",
Target: 20 * time.Millisecond,
},
{
Name: "100% availability",
Field: "RequestFailureCount",
Predicate: "EQ",
Target: 0,
},
},
}

b := configio.Builder{}
b.SetTests([]benchttp.TestCase{want.Tests[0]})
b.AddTests(want.Tests[1:]...)

benchttptest.AssertEqualRunners(t, want, b.Runner())
})
}

// helpers

func mustParseRequestURI(s string) *url.URL {
u, err := url.ParseRequestURI(s)
if err != nil {
panic("mustParseRequestURI: " + err.Error())
}
return u
}

func readcloser(s string) io.ReadCloser {
return io.NopCloser(strings.NewReader(s))
}
18 changes: 13 additions & 5 deletions configio/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@ import (
"github.com/benchttp/sdk/benchttp"
)

type Decoder interface {
Decode(dst *Representation) error
DecodeRunner(dst *benchttp.Runner) error
}

type Format string

const (
FormatJSON Format = "json"
FormatYAML Format = "yaml"
)

type Decoder interface {
Decode(dst *benchttp.Runner) error
}

// DecoderOf returns the appropriate Decoder for the given Format.
// It panics if the format is not a Format declared in configio.
func DecoderOf(format Format, in []byte) Decoder {
return decoderOf(format, in)
}

type decoder interface {
Decoder
decodeRepr(dst *representation) error
}

func decoderOf(format Format, in []byte) decoder {
r := bytes.NewReader(in)
switch format {
case FormatYAML:
Expand Down
Loading