Skip to content

Commit

Permalink
SYSENG-1357: generic client default options
Browse files Browse the repository at this point in the history
  • Loading branch information
Mario Schäfer committed Apr 24, 2024
1 parent 110c34a commit 167a9f9
Show file tree
Hide file tree
Showing 15 changed files with 375 additions and 37 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Some examples, more below in the actual changelog (newer entries are more likely
-->

### Added
* `WithRequestOptions` API option to configure default request options (#361, @anx-mschaefer)

### Changed
* (internal) add "error-return" to request option interfaces (#361, @anx-mschaefer)

## [0.6.4] - 2024-03-15

### Fixed
Expand Down
77 changes: 70 additions & 7 deletions pkg/api/api_implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
Expand All @@ -30,7 +31,8 @@ type defaultAPI struct {
client client.Client
logger *logr.Logger

clientOptions []client.Option
clientOptions []client.Option
requestOptions []types.Option
}

// NewAPIOption is the type for giving options to the NewAPI function.
Expand All @@ -43,6 +45,13 @@ func WithClientOptions(o ...client.Option) NewAPIOption {
}
}

// WithRequestOptions configures default options applied to requests
func WithRequestOptions(opts ...types.Option) NewAPIOption {
return func(a *defaultAPI) {
a.requestOptions = opts
}
}

// WithLogger configures the API to use the given logger. It is recommended to pass a named logger.
// If you don't pass an existing client, the logger you give here will given to the client (with
// added name "client").
Expand Down Expand Up @@ -79,8 +88,19 @@ func NewAPI(opts ...NewAPIOption) (API, error) {
// Get the identified object from the engine.
func (a defaultAPI) Get(ctx context.Context, o types.IdentifiedObject, opts ...types.GetOption) error {
options := types.GetOptions{}
var err error
for _, requestOpt := range a.requestOptions {
if getOpt, ok := requestOpt.(types.GetOption); ok {
err = errors.Join(err, getOpt.ApplyToGet(&options))
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
err = errors.Join(err, anyOpt.ApplyToAny(&options))
}
}
for _, opt := range opts {
opt.ApplyToGet(&options)
err = errors.Join(err, opt.ApplyToGet(&options))
}
if err != nil {
return fmt.Errorf("apply request options: %w", err)
}

return a.do(ctx, o, o, &options, types.OperationGet)
Expand All @@ -89,8 +109,19 @@ func (a defaultAPI) Get(ctx context.Context, o types.IdentifiedObject, opts ...t
// Create the given object on the engine.
func (a defaultAPI) Create(ctx context.Context, o types.Object, opts ...types.CreateOption) error {
options := types.CreateOptions{}
var err error
for _, requestOpt := range a.requestOptions {
if createOpt, ok := requestOpt.(types.CreateOption); ok {
err = errors.Join(err, createOpt.ApplyToCreate(&options))
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
err = errors.Join(err, anyOpt.ApplyToAny(&options))
}
}
for _, opt := range opts {
opt.ApplyToCreate(&options)
err = errors.Join(opt.ApplyToCreate(&options))
}
if err != nil {
return fmt.Errorf("apply request options: %w", err)
}

if err := a.do(ctx, o, o, &options, types.OperationCreate); err != nil {
Expand All @@ -115,8 +146,19 @@ func (a defaultAPI) handlePostCreateOptions(ctx context.Context, o types.Identif
// Update the object on the engine.
func (a defaultAPI) Update(ctx context.Context, o types.IdentifiedObject, opts ...types.UpdateOption) error {
options := types.UpdateOptions{}
var err error
for _, requestOpt := range a.requestOptions {
if updateOpt, ok := requestOpt.(types.UpdateOption); ok {
err = errors.Join(err, updateOpt.ApplyToUpdate(&options))
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
err = errors.Join(err, anyOpt.ApplyToAny(&options))
}
}
for _, opt := range opts {
opt.ApplyToUpdate(&options)
err = errors.Join(err, opt.ApplyToUpdate(&options))
}
if err != nil {
return fmt.Errorf("apply request options: %w", err)
}

return a.do(ctx, o, o, &options, types.OperationUpdate)
Expand All @@ -125,8 +167,19 @@ func (a defaultAPI) Update(ctx context.Context, o types.IdentifiedObject, opts .
// Destroy the identified object.
func (a defaultAPI) Destroy(ctx context.Context, o types.IdentifiedObject, opts ...types.DestroyOption) error {
options := types.DestroyOptions{}
var err error
for _, requestOpt := range a.requestOptions {
if destroyOpt, ok := requestOpt.(types.DestroyOption); ok {
err = errors.Join(err, destroyOpt.ApplyToDestroy(&options))
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
err = errors.Join(err, anyOpt.ApplyToAny(&options))
}
}
for _, opt := range opts {
opt.ApplyToDestroy(&options)
err = errors.Join(err, opt.ApplyToDestroy(&options))
}
if err != nil {
return fmt.Errorf("apply request options: %w", err)
}

return a.do(ctx, o, o, &options, types.OperationDestroy)
Expand All @@ -135,11 +188,21 @@ func (a defaultAPI) Destroy(ctx context.Context, o types.IdentifiedObject, opts
// List objects matching the info given in the object.
func (a defaultAPI) List(ctx context.Context, o types.FilterObject, opts ...types.ListOption) error {
options := types.ListOptions{}
var err error
for _, requestOpt := range a.requestOptions {
if listOpt, ok := requestOpt.(types.ListOption); ok {
err = errors.Join(err, listOpt.ApplyToList(&options))
} else if anyOpt, ok := requestOpt.(types.AnyOption); ok {
err = errors.Join(err, anyOpt.ApplyToAny(&options))
}
}
for _, opt := range opts {
opt.ApplyToList(&options)
err = errors.Join(err, opt.ApplyToList(&options))
}
if err != nil {
return fmt.Errorf("apply request options: %w", err)
}

var err error
ctx, err = a.contextPrepare(ctx, o, types.OperationList, &options)

if err != nil {
Expand Down
154 changes: 144 additions & 10 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,56 @@ var _ = Describe("getResponseType function", func() {

type apiTestAnyopOption string

func (o apiTestAnyopOption) ApplyToGet(opts *types.GetOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToGet(opts *types.GetOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToList(opts *types.ListOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToList(opts *types.ListOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToCreate(opts *types.CreateOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToCreate(opts *types.CreateOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToUpdate(opts *types.UpdateOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToUpdate(opts *types.UpdateOptions) error {
return opts.Set("api_test_option", o, false)
}

func (o apiTestAnyopOption) ApplyToDestroy(opts *types.DestroyOptions) {
_ = opts.Set("api_test_option", o, false)
func (o apiTestAnyopOption) ApplyToDestroy(opts *types.DestroyOptions) error {
return opts.Set("api_test_option", o, false)
}

type errorOption struct {
err error
}

func (o errorOption) ApplyToGet(opts *types.GetOptions) error {
return o.err
}

func (o errorOption) ApplyToList(opts *types.ListOptions) error {
return o.err
}

func (o errorOption) ApplyToCreate(opts *types.CreateOptions) error {
return o.err
}

func (o errorOption) ApplyToUpdate(opts *types.UpdateOptions) error {
return o.err
}

func (o errorOption) ApplyToDestroy(opts *types.DestroyOptions) error {
return o.err
}

type errorAnyOption struct {
err error
}

func (o errorAnyOption) ApplyToAny(opts types.Options) error {
return o.err
}

type apiTestObject struct {
Expand Down Expand Up @@ -1025,6 +1057,108 @@ var _ = Describe("using an API object", func() {
err = api.Destroy(ctx, &o, opt)
Expect(err).NotTo(HaveOccurred())
})

It("consumes the default options for all operations", func() {
opt := apiTestAnyopOption("hello world")
ctx := context.WithValue(context.TODO(), errAPITest, opt)

server.AppendHandlers(
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, []map[string]string{{"value": "option-check"}}),
ghttp.RespondWithJSONEncoded(200, map[string]string{"value": "option-check"}),
ghttp.RespondWithJSONEncoded(200, map[string]string{}),
)

api, err := NewAPI(
WithLogger(logger),
WithClientOptions(
client.BaseURL(server.URL()),
client.IgnoreMissingToken(),
),
WithRequestOptions(opt),
)
Expect(err).NotTo(HaveOccurred())

o := apiTestObject{"option-check"}

err = api.Create(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Get(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.List(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Update(ctx, &o)
Expect(err).NotTo(HaveOccurred())

err = api.Destroy(ctx, &o)
Expect(err).NotTo(HaveOccurred())
})

It("returns an error when applying configured options return errors", func() {
mockErr := errors.New("foo")
api, err := NewAPI(
WithLogger(logger),
WithClientOptions(
client.BaseURL(server.URL()),
client.IgnoreMissingToken(),
),
)
Expect(err).NotTo(HaveOccurred())

o := apiTestObject{"foo"}

Expect(api.Create(context.TODO(), &o, errorOption{mockErr})).Error().To(MatchError(mockErr))
Expect(api.Get(context.TODO(), &o, errorOption{mockErr})).Error().To(MatchError(mockErr))
Expect(api.List(context.TODO(), &o, errorOption{mockErr})).Error().To(MatchError(mockErr))
Expect(api.Update(context.TODO(), &o, errorOption{mockErr})).Error().To(MatchError(mockErr))
Expect(api.Destroy(context.TODO(), &o, errorOption{mockErr})).Error().To(MatchError(mockErr))
})

It("returns an error when applying configured default options return errors", func() {
mockErr := errors.New("foo")
api, err := NewAPI(
WithLogger(logger),
WithClientOptions(
client.BaseURL(server.URL()),
client.IgnoreMissingToken(),
),
WithRequestOptions(errorOption{mockErr}),
)
Expect(err).NotTo(HaveOccurred())

o := apiTestObject{"foo"}

Expect(api.Create(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Get(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.List(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Update(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Destroy(context.TODO(), &o)).Error().To(MatchError(mockErr))
})

It("returns an error when applying configured default AnyOption return errors", func() {
mockErr := errors.New("foo")
api, err := NewAPI(
WithLogger(logger),
WithClientOptions(
client.BaseURL(server.URL()),
client.IgnoreMissingToken(),
),
WithRequestOptions(errorAnyOption{mockErr}),
)
Expect(err).NotTo(HaveOccurred())

o := apiTestObject{"foo"}

Expect(api.Create(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Get(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.List(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Update(context.TODO(), &o)).Error().To(MatchError(mockErr))
Expect(api.Destroy(context.TODO(), &o)).Error().To(MatchError(mockErr))
})
})

const contextTestObjectBaseurl = "/v1/context_test_object"
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ var (

// ErrContextRequired is returned when a nil context was passed as argument.
ErrContextRequired = errors.New("no context given")

// ErrEnvironmentSegmentNotTypeString is returned when the environment segment is not of type string
ErrEnvironmentSegmentNotTypeString = errors.New("environment segment is not of type string")
)

// EngineError is the base type for all errors returned by the engine.
Expand Down
12 changes: 8 additions & 4 deletions pkg/api/internal/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ type PagedOption struct {
}

// ApplyToList applies the Paged option to all the ListOptions.
func (p PagedOption) ApplyToList(o *types.ListOptions) {
func (p PagedOption) ApplyToList(o *types.ListOptions) error {
o.Paged = true
o.Page = p.Page
o.EntriesPerPage = p.Limit
o.PageInfo = p.Info
return nil
}

// ObjectChannelOption configures the List operation to return the objects via the given channel.
Expand All @@ -30,23 +31,26 @@ type ObjectChannelOption struct {
}

// ApplyToList applies the AsObjectChannel option to all the ListOptions.
func (aoc ObjectChannelOption) ApplyToList(o *types.ListOptions) {
func (aoc ObjectChannelOption) ApplyToList(o *types.ListOptions) error {
o.ObjectChannel = aoc.Channel
return nil
}

// FullObjectsOption configures if the List operation shall make a Get operation for each object before
// returning it to the caller.
type FullObjectsOption bool

// ApplyToList applies the FullObjectsOption option to all the ListOptions.
func (foo FullObjectsOption) ApplyToList(o *types.ListOptions) {
func (foo FullObjectsOption) ApplyToList(o *types.ListOptions) error {
o.FullObjects = bool(foo)
return nil
}

// AutoTagOption configures the Create operation to automatically tag objects after creation
type AutoTagOption []string

// ApplyToCreate applies the AutoTagOption to the ListOptions
func (ato AutoTagOption) ApplyToCreate(o *types.CreateOptions) {
func (ato AutoTagOption) ApplyToCreate(o *types.CreateOptions) error {
o.AutoTags = ato
return nil
}
Loading

0 comments on commit 167a9f9

Please sign in to comment.