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

Bot: Add support for net/http style middlewares #147

Open
diamondburned opened this issue Sep 25, 2020 · 0 comments
Open

Bot: Add support for net/http style middlewares #147

diamondburned opened this issue Sep 25, 2020 · 0 comments

Comments

@diamondburned
Copy link
Owner

diamondburned commented Sep 25, 2020

Preamble

As of right now, the current middleware API only allows breaking the chain of execution by returning an error. This has many disadvantages, but they all boil down to the caller not having explicit control over the execution of whatever is next. This means that they cannot conditionally return based on the values of the next item in the chain of execution.

Proposal

Inspiration

Package net/http generally uses middlewares like such:

func middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Explicit flow control demo.
		if r.Header().Get("Action") == "Break me" {
			w.Write([]byte("Broken."))
			return
		}

		mockWriter := newMockWriter(w)
		next.ServeHTTP(mockWriter, r)

		// Explicit flow control even after
		if mockWriter.String() == "Error!" {
			w.WriteHeader(500)
		}

		mockWriter.WriteTo(w)
	})
}

This API is good, because although it seems almost too convenient most of the time, there is actually little magic involved in the implementation. It's just a fancy way of wrapping handlers in a chain. go-chi improves this even further by providing a helper router that wraps the chain of middlewares inside a slice:

r.Route("/", func(r chi.Router) {
	// stdlib
	r.Handle("/", middleware(serveHelloWorld))

	// go-chi
	r.Use(middleware)
	r.Get("/", serveHelloWorld)
})

New API

This proposal asks for an API similar to what net/http has, but with a touch of reflect magic that is internally handled but not exposed. Consider this API:

ctx.AddMiddleware(cmd.Method, func(next bot.Caller) bot.Caller {
	return bot.CallerFunc(func(m *gateway.MessageCreateEvent) (*api.SendMessageData, error) {
		if m.Content == "Break me" {
			return bot.TextReturn("Broken"), nil
		}

		r, err := next.Call(m)
		if err != nil {
			// Errors take precedence over *Return.
			return nil, err
		}

		if r != nil && r.Content == "Error!" {
			return nil, fmt.Errorf("%s", r.Content)
		}

		return r, nil
	})
})

This API adds several new types.

The bot.Caller interface is added to represent different types of callers. This includes the final function to call as well as other middlewares. The underlying type is purely an implementation detail, thus shouldn't be type-asserted, unless for debugging purposes.

For concreteness, the interface is exactly defined as such:

type Caller interface {
	// Call is a function that calls the underlying handler. The handler may
	// return nil values for all, one, the other, or neither.
	Call(ev interface{}) (*api.SendMessageData, error)
}

Having the interface's underlying type be an implementation detail is very nice, as it allows backwards compatibility with the previous middleware style simply by wrapping a section of the middleware slice inside a struct that implements bot.Caller.

The bot.CallerFunc function is added to wrap the given middleware function around a type that satisfies bot.Caller. It uses reflection to get the event type as well as the return signatures. The possible signatures are:

func(event *AnyType)
func(event *AnyType) error
func(event *AnyType) (string, error)
func(event *AnyType) (*discord.Embed, error)
func(event *AnyType) (*api.SendMessageData, error)

Similar to before, invalid function signatures will trigger a panic at runtime.

Below are concrete details for other aforementioned types. Note that the middleware function will still only have access to the value of the event, but not what the arguments parsed. This is because the actual argument parsing will be moved to the final call in the chain of execution and into *MethodContext.

package bot

// CallerFunc wraps around functions with the aforementioned possible function
// signatures. It panics if an unsupported function is given.
func CallerFunc(v interface{}) Call

// MethodContext is the same old struct with additional type information moved
// from the parent subcommand context.
type MethodContext struct{}

// Call calls the handler method with the given value. It does internal type
// checks. If v is a MessageCreateEvent, then the function will parse arguments
// from the value by itself. This parser is moved from ctx_call.go.
func (c *MethodContext) Call(v interface{}) (*api.SendMessageData, error)

Breaking changes

This proposal does not include any breaking changes. As mentioned above, the old middleware functions can simply be wrapped inside another type that implements bot.Caller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant