Skip to content

Latest commit

 

History

History
255 lines (204 loc) · 7.8 KB

README.md

File metadata and controls

255 lines (204 loc) · 7.8 KB

goioc/web: Web Framework for Go, based on goioc/di

goioc

Go go.dev reference CodeFactor Go Report Card codecov Quality Gate Status DeepSource

How is this framework different from others?

  1. First of all, goioc/web is working using Dependency Injection and is based on goioc/di, which is the IoC Container.
  2. Secondly - and this is the most exciting part - web-endpoints in goioc/web can have (almost) arbitrary signature! No more func(w http.ResponseWriter, r *http.Request) handlers, if your endpoint receives a string and produces a binary stream, just declare it as is:
...
func (e *endpoint) Hello(name string) io.Reader {
	return bytes.NewBufferString("Hello, " + name + "!")
}
...

Cool, huh? 🤠 Of course, you can still directly use http.ResponseWriter and *http.Request, if you like.

Basic concepts

The main entity in goioc/web is the Endpoint, which is represented by the interface of the same name. Here's the example implementation:

type endpoint struct {
}

func (e endpoint) HandlerFuncName() string {
	return "Hello"
}

func (e *endpoint) Hello(name string) io.Reader {
	return bytes.NewBufferString("Hello, " + name + "!")
}

Endpoint interface has one method that returns the name of the method that will be used as an endpoint.

In order for goioc/web to pick up this endpoint, it should be registered in the DI Container:

_, _ = di.RegisterBean("endpoint", reflect.TypeOf((*endpoint)(nil)))

Then the container should be initialized (please, refer to the goioc/di documentation for more details):

_ = di.InitializeContainer()

Finally, the web-server can be started, either using the built-in function:

_ = web.ListenAndServe(":8080")

... or using returned Router

router, _ := web.CreateRouter()
_ = http.ListenAndServe(":8080", router)

Routing

So, how does the framework know where to bind this endpoint to? For the routing functionality goioc/web leverages gorilla/mux library. Don't worry: you don't have to cope with this library directly: goioc/web provides a set of convenient wrappers around it. The wrappers are implemented as tags in the endpoint-structure. Let's slightly update our previous example:

...
type endpoint struct {
	method interface{} `web.methods:"GET"`
	path   interface{} `web.path:"/hello"`
}
...

Now our endpoint is bound to a GET requests at the /hello path. Yes, it's that simple! 🙂

goioc/web tags

Tag Value Example
web.methods List of HTTP-methods. web.methods:"POST,PATCH"
web.path URL sub-path. Can contain path variables. web.path:"/articles/{category}/{id:[0-9]+}"
web.queries Key-value paris of the URL query part. web.queries:"foo,bar,id,{id:[0-9]+}"
web.headers Key-value paris of the request headers. web.headers:"Content-Type,application/octet-stream"
web.matcher ID of the bean of type *mux.MatcherFunc. web.matcher:"matcher"

In and Out types

As was mentioned above, with goioc/web you get a lot of freedom in terms of defining the signature of your endpoint's method. Just look at these examples:

...
func (e *endpoint) Error() (int, string) {
	return 505, "Something bad happened :("
}
...
...
func (e *endpoint) KeyValue(ctx context.Context) string {
	return ctx.Value(di.BeanKey("key")).(string)
}
...
...
func (e *endpoint) Hello(pathParams map[string]string) (http.Header, int) {
	return map[string][]string{
    		"Content-Type": {"application/octet-stream"},
    	}, []byte("Hello, " + pathParams["name"] + "!")
}
...

Supported argument types

  • http.ResponseWriter
  • *http.Request
  • context.Context
  • http.Header
  • io.Reader
  • io.ReadCloser
  • []byte
  • string
  • map[string]string
  • url.Values
  • struct implementing encoding.BinaryUnmarshaler or encoding.TextUnmarshaler
  • interface{} (GoiocSerializer bean is used to deserialize such arguments)

Supported return types

  • http.Header (response headers, must be first return argument, if used)
  • int (status code, must be first argument after response headers, if used)
  • io.Reader
  • io.ReadCloser
  • []byte
  • string
  • struct implementing encoding.BinaryMarshaler or encoding.TextMarshaler
  • interface{} (GoiocSerializer bean is used to serialize such returned object)

Templates

goioc/web supports templates!

todo.html

<h1>{{.PageTitle}}</h1>
<ul>
    {{range .Todos}}
        {{if .Done}}
            <li class="done">{{.Title}}</li>
        {{else}}
            <li>{{.Title}}</li>
        {{end}}
    {{end}}
</ul>

endpoint.go

type todo struct {
	Title string
	Done  bool
}
type todoPageData struct {
	PageTitle string
	Todos     []todo
}

type todoEndpoint struct {
	method interface{} `web.methods:"GET"`
	path   interface{} `web.path:"/todo"`
}

func (e todoEndpoint) HandlerFuncName() string {
	return "TodoList"
}

func (e *todoEndpoint) TodoList() (template.Template, interface{}) {
	tmpl := template.Must(template.ParseFiles("todo.html"))
	return *tmpl, todoPageData{
		PageTitle: "My TODO list",
		Todos: []todo{
			{Title: "Task 1", Done: false},
			{Title: "Task 2", Done: true},
			{Title: "Task 3", Done: true},
		},
	}
}

Note that in case of using templates, the next returned object after template.Template must be the actual structure that will be used to fill in the template 💡

Custom matchers

If functionality of web.methods, web.path, web.queries and web.headers is not enough for you, you can use custom matcher, based on Gorilla's mux.MatcherFunc:

...
_, _ = di.RegisterBeanFactory("matcher", di.Singleton, func(context.Context) (interface{}, error) {
		matcherFunc := mux.MatcherFunc(func(request *http.Request, match *mux.RouteMatch) bool {
			return strings.HasSuffix(request.URL.Path, "bar")
		})
		return &matcherFunc, nil
	})

...

type endpoint struct {
	method  interface{} `web.methods:"GET"`
	path    interface{} `web.path:"/endpoint/{key}/{*?}"`
	matcher interface{} `web.matcher:"matcher"`
}

func (e endpoint) HandlerFuncName() string {
	return "Match"
}

func (e *endpoint) Match() string {
	return "It's a match! :)"
}
...
$ curl localhost:8080/endpoint/foo/bar
It's a match! :)                

Middleware

Of course, custom middleware is also supported by the framework:

web.Use(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), di.BeanKey("key"), "value")))
	})
})