1
0
mirror of https://github.com/ggicci/httpin.git synced 2024-11-30 08:56:52 +02:00
🍡 HTTP Input for Go - Decode an HTTP request into a custom struct 📢 the encoding feature will come soon, see branch feat/encoder https://ggicci.github.io/httpin/
Go to file
2021-08-28 12:04:23 -05:00
.github/workflows refactor: update ci scripts 2021-04-23 11:26:31 +08:00
internal refactor: remove some API 2021-07-11 18:53:13 +08:00
patch feat: remove complex64/128 types and more coverage tests 2021-05-10 14:54:10 +08:00
.gitignore feat: add source value to InvalidFieldError 2021-07-07 21:24:14 +08:00
body_test.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
body.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
chain_test.go feat: more coverage 2021-07-07 19:07:28 +08:00
chain.go feat: add core option and tests for the middleware 2021-05-08 18:18:57 +08:00
decoder_test.go refactor: remove some API 2021-07-11 18:53:13 +08:00
decoders.go refactor: remove some API 2021-07-11 18:53:13 +08:00
directives_test.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
directives.go GH-5 add a query directive 2021-08-28 11:53:25 -05:00
errors.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
extractor.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
form.go refactor: remove some API 2021-07-11 18:53:13 +08:00
go.mod feat: deal with gochi URLParam 2021-07-15 19:05:14 +08:00
go.sum feat: deal with gochi URLParam 2021-07-15 19:05:14 +08:00
gochi_test.go refactor: gochi tests 2021-07-16 16:48:32 +08:00
gochi.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
gorilla_test.go feat: integrate with gorilla mux package to extract path varialbes 2021-06-24 14:08:13 +08:00
gorilla.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
header.go refactor: remove some API 2021-07-11 18:53:13 +08:00
httpin_test.go GH-5 add a query directive 2021-08-28 11:53:25 -05:00
httpin.go feat: created "body" directive 2021-07-19 14:31:31 +08:00
LICENSE Initial commit 2021-04-13 10:15:37 +08:00
Makefile refactor: update ci scripts 2021-04-23 11:26:31 +08:00
options_test.go feat: add core option and tests for the middleware 2021-05-08 18:18:57 +08:00
options.go refactor: remove some API 2021-07-11 18:53:13 +08:00
query.go GH-5 add a query directive 2021-08-28 11:53:25 -05:00
README.md GH-5 update readme 2021-08-28 12:04:23 -05:00
required.go refactor: add internal package to reduce public API surface 2021-05-08 15:27:37 +08:00
resolver_test.go feat: implement form and header extractors in directives 2021-04-30 19:10:07 +08:00
resolver.go fix: skip resolving unexported fields in a struct 2021-08-15 16:40:02 +08:00

httpin

Go Workflow codecov Go Reference

HTTP Input for Go - Decode an HTTP request into a custom struct

Define the struct for your input and then fetch your data!

Quick View

BEFORE (use net/http) AFTER (use httpin)
func ListUsers(rw http.ResponseWriter, r *http.Request) {
	page, err := strconv.ParseInt(r.FormValue("page"), 10, 64)
	if err != nil {
		// Invalid parameter: page.
		return
	}
	perPage, err := strconv.ParseInt(r.FormValue("per_page"), 10, 64)
	if err != nil {
		// Invalid parameter: per_page.
		return
	}
	isMember, err := strconv.ParseBool(r.FormValue("is_member"))
	if err != nil {
		// Invalid parameter: is_member.
		return
	}

	// Do sth.
}
type ListUsersInput struct {
	Page     int  `in:"form=page"`
	PerPage  int  `in:"form=per_page"`
	IsMember bool `in:"form=is_member"`
}

func ListUsers(rw http.ResponseWriter, r *http.Request) {
	input := r.Context().Value(httpin.Input).(*ListUsersInput)
	// Do sth.
}

Features

  • Builtin directive form to decode a field from HTTP form values, i.e. http.Request.Form
  • Builtin directive query to decode a field from HTTP querystring parameters, i.e. http.Request.URL.Query()
  • Builtin directive header to decode a field from HTTP headers, i.e. http.Request.Header
  • Builtin decoders used by form, query, and header directives for basic types, e.g. bool, int, int64, float32, time.Time, ... full list
  • Decode a field by inspecting a set of keys from the same source, e.g. in:"form=per_page,page_size"
  • Decode a field from multiple sources, e.g. both form, querystring, and headers, in:"form=access_token;query=token;header=x-api-token"
  • Register custom type decoders by implementing httpin.Decoder interface
  • Compose an input struct by embedding struct fields
  • Builtin directive required to tag a field as required
  • Builtin directive body to parse HTTP request body as JSON or XML
  • Register custom directive executors to extend the ability of field resolving, see directive required as an example and think about implementing your own directives like trim, to_lowercase, base58_to_int, etc.
  • Easily integrating with popular Go web frameworks and packages

Example (Use http.Handler)

First, set up the middleware for your handlers (bind Input vs. Handler). We recommend using alice to chain your HTTP middleware functions.

type Authorization struct {
	// Decode from multiple sources, the former with higher priority
	Token string `in:"form=access_token;header=x-api-token;required"`
}

type Pagination struct {
	Page int `in:"form=page"`

	// Decode from multiple keys in the same source, the former with higher priority
	PerPage int `in:"form=per_page,page_size"`
}

type ListUsersInput struct {
	Gender   string `in:"form=gender"`
	AgeRange []int  `in:"form=age_range"`
	IsMember bool   `in:"form=is_member"`

	Pagination    // Embedded field works
	Authorization // Embedded field works
}

func init() {
	http.Handle("/users", alice.New(
		httpin.NewInput(ListUsersInput{}),
	).ThenFunc(ListUsers))
}

Second, fetch your input with only ONE LINE of code.

func ListUsers(rw http.ResponseWriter, r *http.Request) {
	input := r.Context().Value(httpin.Input).(*ListUsersInput)

	// Do sth.
}

Parse HTTP Request Body

There're two ways of parsing HTTP request body into your input struct.

Parse HTTP request body into the input struct (body -> input)

Embed one of our body decoder annotations into your input struct:

// POST /users with JSON body
type CreateUserInput struct {
	httpin.JSONBody        // annotation
	Username        string `json:"username"`
	Gender          int    `json:"gender"`
}
  • httpin.JSONBody: parse request body in JSON format
  • httpin.XMLBody: parse request body in XML format

Parse HTTP request body into a field of the input struct (body -> input.field)

Use body directive.

// PATCH /users/:uid with JSON body
type UpdateUserProfileInput struct {
	UserID int64 `in:"path=uid"` // get user id from path variable
	Patch  struct {
		Username string `json:"username"`
		Gender   int    `json:"gender"`
	} `in:"body=json"` // use body=xml to decode in XML format
}

Advanced

🔥 Extend httpin by adding custom directives

Know the concept of a Directive:

type Authorization struct {
	Token string `in:"form=access_token,token;header=x-api-token;required"`
	                  ^---------------------^ ^----------------^ ^------^
	                            d1                    d2            d3
}

There are three directives above, separated by semicolons (;):

  • d1: form=access_token,token
  • d2: header=x-api-token
  • d3: required

A directive consists of two parts separated by an equal sign (=). The left part is the name of an executor who was designed to run this directive. The right part is a list of arguments separated by commas (,) which will be passed to the corresponding executor at runtime.

For instance, form=access_token,token, here form is the name of the executor, and access_token,token is the argument list which will be parsed as []string{"access_token", "token"}.

There are several builtin directive executors, e.g. form, header, required, ... full list. And you can define your own by:

First, create a directive executor by implementing the httpin.DirectiveExecutor interface:

func toLower(ctx *DirectiveContext) error {
	if ctx.ValueType.Kind() != reflect.String {
		return errors.New("not a string")
	}

	currentValue := ctx.Value.Elem().String()
	newValue := strings.ToLower(currentValue)
	ctx.Value.Elem().SetString(newValue)
	return nil
}

// Adapt toLower to httpin.DirectiveExecutor.
var MyLowercaseDirectiveExecutor = httpin.DirectiveExecutorFunc(toLower)

Seconds, register it to httpin:

httpin.RegisterDirectiveExecutor("to_lowercase", MyLowercaseDirectiveExecutor, nil)

Finally, you can use your own directives in the struct tags:

type Authorization struct {
	Token string `in:"form=token;required;to_lowercase"`
}

The directives will run in the order as they defined in the struct tag.