restruct

package module
v0.0.0-...-d27f41a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 31, 2023 License: MIT Imports: 17 Imported by: 0

README

Run Tests

restruct

RESTruct is a go rest framework based on structs. The goal of this project is to automate routing, request and response based on struct methods.



Install

go get github.com/altlimit/restruct

Router

Exported struct methods will be your handlers and will be routed like the following.

UpperCase turns to upper-case
With_Underscore to with/underscore
HasParam_0 to has-param/{0}
HasParam_0_AndMore_1 to has-param/{0}/and-more/{1}

There are multiple ways to process a request and a response, such as strongly typed parameters and returns or with *http.Request or http.ResponseWriter parameters. You can also use the context.Context parameter. Any other parameters will use the DefaultReader which you can override in your Handler.Reader.

type Calculator struct {
}

func (c *Calculator) Add(r *http.Request) interface{} {
    var req struct {
        A int64 `json:"a"`
        B int64 `json:"b"`
    }
    if err := restruct.Bind(r, &req, http.MethodPost); err != nil {
        return err
    }
    return req.A + req.B
}

func (c *Calculator) Subtract(a, b int64) int64 {
    return a - b
}

func (c *Calculator) Divide(a, b int64) (int64, error) {
    if b == 0 {
        return 0, errors.New("divide by 0")
    }
    return a / b, nil
}

func (c *Calculator) Multiply(r struct {
    A int64 `json:"a"`
    B int64 `json:"b"`
}) int64 {
    return r.A * r.B
}

func main() {
	restruct.Handle("/api/v1/", &Calculator{})
	http.ListenAndServe(":8080", nil)
}

We have registered the Calculator struct here as our service and we should now have available endpoints which you can send json request and response to.

// POST http://localhost:8080/api/v1/add
{
    "a": 10,
    "b": 20
}
// -> 20
// -> or any errors such as 400 {"error": "Bad Request"}

// POST http://localhost:8080/api/v1/subtract
// Since this is a non-request, response, context parameter
// it will be coming from json array request as a default behavior from DefaultReader
[
    20,
    10
]
// -> 10

// POST http://localhost:8080/api/v1/divide
// You can also have the ability to have a strongly typed handlers in your parameters and return types.
// Default behaviour from DefaultWriter is if multiple returns with last type is an error with value then it writes it.
[
    1,
    0
]
// -> 500 {"error":"Internal Server Error"}

// POST http://localhost:8080/api/v1/multiply
// With a single struct as a parameter, it will be similar to Add's implementation where it uses Bind internally to populate it. You can change your Bind with DefaultReader{Bind:...} to add your validation library.
{
    "a": 2,
    "b": 5
}
// -> 10

You can override default method named routes using Router interface. Implement Router in your service and return a slice Route.

func (c *Calculator) Routes() []Route {
    return []Route{
        Route{Handler: "Add", Path:"addition", Methods: []string{http.MethodPost}},
        Route{Handler: "Subtract", Path:"subtraction", Methods: []string{http.MethodPost}},
    }
}

Examples

Here are more ways to create handlers.

type Blob struct {
    Internal bool
}

func (b *Blob) Routes() []Route {
    return []Route{
        {Handler: "Download", Path: "blob/{path:.+}", methods: []string{http.MethodGet}}
    }
}

// Will be available at /blob/{path:.+} since we overwrite it in Routes
// you can also avoid using regex by naming your handler with Blob_0Path and access with "0Path" params.
func (b *Blob) Download(w http.ResponseWriter, r *http.Request) {
    path := restruct.Params(r)["path"]
    // handle your struct like normal
}

Here we use Router interface to add a regular expression. The path param on the download Route will accept anything even an additional nested paths / and it also has a standard handler definition.

To register the above service:

func main() {
	restruct.Handle("/api/v1/", &Blob{})
	http.ListenAndServe(":8080", nil)
}

You can create additional service with a different prefix by calling NewHandler on your struct then adding it with AddService.

h := restruct.NewHandler(&Blob{})
h.AddService("/internal/{tag}/", &Blob{Internal: true})
restruct.Handle("/api/v1/", h)

All your services will now be at /api/v1/internal/{tag}. You can also register the returned Handler in a third party router but make sure you call WithPrefix(...) on it if it's not a root route.

http.Handle("/", h)
// or if it's a not a root route
http.Handle("/api/v1/", h.WithPrefix("/api/v1/"))

You can have parameters with method using number and access them using restruct.Params(req) or restruct.Vars(ctx):

// Will be available at /upload/{0}
func (b *Blob) Upload_0(r *http.Request) interface{} {
    uploadType := restruct.Params(r)["0"]
    // handle your request normally
    fileID := ...
    return fileID
}

Refer to cmd/example for some advance usage.

Response Writer

The default ResponseWriter is DefaultWriter which uses json.Encoder().Encode to write outputs. This also handles errors and status codes. You can modify the output by implementing the ResponseWriter interface and set it in your Handler.Writer.

type TextWriter struct {}

func (tw *TextWriter) Write(w http.ResponseWriter, r *http.Request, types []reflect.Type, vals []reflect.Value) {
    // types - slice of return types
    // vals - slice of actual returned values
    // this writer we simply write anything returned as text
    var out []interface{}
    for _, val := range vals {
        out = append(out, val.Interface())
    }
    w.WriteHeader(http.StatusOK)
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte(fmt.Sprintf("%v", out)))
}

h := restruct.NewHandler(&Blob{})
h.Writer = &TextWriter{}

Request Reader

A handler can have any or no parameters, but the default parameters that doesn't go through request reader are: context.Context, *http.Request and http.ResponseWriter, these parameters will not be passed in RequestReader.Read interface.

// use form for urlencoded post
type login struct {
    Username string `json:"username" form:"username"`
    Password string `json:"password" from:"password"`
}

func (b *Blob) Login(l *login) interface{} {
    log.Println("Login", l.Username, l.Password)
    return "OK"
}

This uses the DefaultReader which by default can unmarshal single struct and use default bind(restruct.Bind), you can use your own Bind with DefaultReader{Bind:yourBinder} if you want to add validation libraries. The Bind reads the body with json.Encoder, or form values. If you have multiple parameters you will need to send a json array body.

[
    "FirstParam",
    2,
    {"third":"param"}
]

This is the default behaviour of DefaultReader. You can implement RequestReader interface which will allow you to control your own parameter parsing.

type CustomReader struct {}
func (cr *CustomReader) Read(r *http.Request, types []reflect.Type) (vals []reflect.Value, err error) {
    // types are the parameter types in order of your handler you must return equal number of vals to types.
    // You'll only get types that is not *http.Request, http.ResponseWriter, context.Context
    // You can return Error{} type here to return ResponseWriter errors/response and wrap your errors inside Error{Err:...}
    return
}

Middleware

Uses standard middleware and add by handler.Use(...) or you can add it under Route when using the Router interface.

func auth(next http.Handler) http.Handler {
    // you can use your h.Writer here if it's accessible somewhere
	wr := rs.DefaultWriter{}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") != "abc" {
			wr.WriteJSON(w, rs.Error{Status: http.StatusUnauthorized})
			return
		}
		next.ServeHTTP(w, r)
	})
}

h := restruct.NewHandler(&Blob{})
h.Use(auth)

Nested Structs

Nested structs are automatically routed. You can use route tag to customize or add route:"-" to skip exported structs.

type (
    V1 struct {
        Users User
        DB DB `route:"-"`
    }

    User struct {

    }
)

func (v *V1) Drop() {}
func (u *User)  SendEmail() {}

func main() {
    restruct.Handle("/api/v1/", &V1{})
    http.ListenAndServe(":8080", nil)
}

Will generate route: /api/v1/drop and /api/v1/users/send-email

Utilities

Available helper utilities for processing requests and response.

// Adding context values in middleware such as logged in userID
auth := r.Header.Get("Authorization") == "some-key-or-jwt"
if userID, ok := UserIDFromAuth(auth); ok {
    r = restruct.SetValue(r, "userID", userID)
}
// then access it from anywhere or a private method for getting your user record
if userID, ok := restruct.GetValue(r, "userID").(int64); ok {
    user, err := DB.GetUserByID(ctx, userID)
    // do something with user
}

// Bind helps read your json and form requests into a struct, you can add tag "query"
// to bind query strings at the same time. You can also add tag "form" to bind form posts from
// urlencoded or multipart. You can also use explicit functions BindQuery or BindForm.
var loginReq struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
if err := restruct.Bind(r, &loginReq, http.MethodPost); err != nil {
    return err
}

// Reading path parameters with Params /products/{0}
params := restruct.Params(r)
productID := params["0"]

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrReaderReturnLen = errors.New("reader args len does not match")
)

Functions

func Bind

func Bind(r *http.Request, out interface{}, methods ...string) error

Bind checks for valid methods and tries to bind query strings and body into struct

func BindForm

func BindForm(r *http.Request, out interface{}) error

BindForm puts all struct fields with tag:"form" from a form request

func BindJson

func BindJson(r *http.Request, out interface{}) error

BindJson puts all json tagged values into struct fields

func BindQuery

func BindQuery(r *http.Request, out interface{}) error

BindQuery puts all query string values into struct fields with tag:"query"

func GetVal

func GetVal(ctx context.Context, key string) interface{}

func GetVals

func GetVals(ctx context.Context) map[string]interface{}

func GetValue

func GetValue(r *http.Request, key string) interface{}

GetValue returns the stored value from context

func GetValues

func GetValues(r *http.Request) map[string]interface{}

GetValues returns a map of all values from context

func Handle

func Handle(pattern string, svc interface{})

Handle registers a struct or a *Handler for the given pattern in the http.DefaultServeMux.

func Params

func Params(r *http.Request) map[string]string

Params returns map of params from url path like /{param1} will be map[param1] = value

func Query

func Query(r *http.Request, name string) string

Query returns a query string value

func SetVal

func SetVal(ctx context.Context, key string, val interface{}) context.Context

func SetValue

func SetValue(r *http.Request, key string, val interface{}) *http.Request

SetValue stores a key value pair in context

func Vars

func Vars(ctx context.Context) map[string]string

Vars returns map of params from url from request context

Types

type DefaultReader

type DefaultReader struct {
	Bind func(*http.Request, interface{}, ...string) error
}

DefaultReader processes request with json.Encoder, urlencoded form and multipart for structs if it's just basic types it will be read from body as array such as [1, "hello", false] you can overwrite bind to apply validation library, etc

func (*DefaultReader) Read

func (dr *DefaultReader) Read(r *http.Request, types []reflect.Type) (vals []reflect.Value, err error)

type DefaultWriter

type DefaultWriter struct {
	// Optional ErrorHandler, called whenever unhandled errors occurs, defaults to logging errors
	ErrorHandler   func(error)
	Errors         map[error]Error
	EscapeJsonHtml bool
}

DefaultWriter uses json.Encoder for output and manages error handling. Adding Errors mapping can help with your existing error to a proper Error{}

func (*DefaultWriter) Write

func (dw *DefaultWriter) Write(w http.ResponseWriter, r *http.Request, types []reflect.Type, vals []reflect.Value)

Write implements the DefaultWriter ResponseWriter returning (int, any, error) will write status int, any response if error is nil returning (any, error) will write any response if error is nil with status 200 or 400, 500 depdening on your error returning (int, any, any, error) will write status int slice of [any, any] response if error is nil

func (*DefaultWriter) WriteJSON

func (dw *DefaultWriter) WriteJSON(w http.ResponseWriter, out interface{})

This writes application/json content type uses status code 200 on valid ones and 500 on uncaught, 400 on malformed json, etc. use Json{Status, Content} to specify a code

func (*DefaultWriter) WriteResponse

func (dw *DefaultWriter) WriteResponse(w http.ResponseWriter, resp *Response)

type Error

type Error struct {
	Status  int
	Message string
	Data    interface{}
	Err     error
}

func (Error) Error

func (e Error) Error() string

type Handler

type Handler struct {
	// Writer controls the output of your service, defaults to DefaultWriter
	Writer ResponseWriter
	// Reader controls the input of your service, defaults to DefaultReader
	Reader RequestReader
	// contains filtered or unexported fields
}

func NewHandler

func NewHandler(svc interface{}) *Handler

NewHandler creates a handler for a given struct.

func (*Handler) AddService

func (h *Handler) AddService(path string, svc interface{})

AddService adds a new service to specified route. You can put {param} in this route.

func (*Handler) NotFound

func (h *Handler) NotFound(handler interface{})

NotFound sets the notFound handler and calls it if no route matches

func (*Handler) Routes

func (h *Handler) Routes() (routes []string)

Routes returns a list of routes registered and it's definition

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP calls the method with the matched route.

func (*Handler) Use

func (h *Handler) Use(fns ...Middleware)

Use adds a middleware to your services.

func (*Handler) WithPrefix

func (h *Handler) WithPrefix(prefix string) *Handler

WithPrefix prefixes your service with given path. You can't use parameters here. This is useful if you want to register this handler with another third party router.

type Init

type Init interface {
	Init(*Handler)
}

Init interface to access and override handler configs

type Json

type Json struct {
	Status  int
	Content interface{}
}

Json response to specify a status code for default writer

type Middleware

type Middleware func(http.Handler) http.Handler

type Middlewares

type Middlewares interface {
	Middlewares() []Middleware
}

Middlewares interface for common middleware for a struct

type RequestReader

type RequestReader interface {
	Read(*http.Request, []reflect.Type) ([]reflect.Value, error)
}

RequestReader is called for input for your method if your parameter contains a things other than *http.Request, http.ResponseWriter, context.Context you'll get a slice of types and you must return values corresponding to those types

type Response

type Response struct {
	Status      int
	Headers     map[string]string
	ContentType string
	Content     []byte
}

Response is used by DefaultWriter for custom response

type ResponseWriter

type ResponseWriter interface {
	Write(http.ResponseWriter, *http.Request, []reflect.Type, []reflect.Value)
}

ResponseWriter is called on outputs of your methods. slice of reflect.Type & Value is the types and returned values

type Route

type Route struct {
	// Handler is the method name you want to use for this route
	Handler string
	// optional path, will use default behaviour if not present
	Path string
	// optional methods, will allow all if not present
	Methods []string
	// optional middlewares, run specific middleware for this route
	Middlewares []Middleware
}

Route for doing overrides with router interface and method restrictions.

type Router

type Router interface {
	Routes() []Route
}

Router can be used to override method name to specific path, implement Router interface in your service and return a slice of Route: [Route{Handler:"ProductEdit", Path: "product/{pid}"}]

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL