From cce5ff4ff7fe416121fa6e8c0d194fa12d8d5c94 Mon Sep 17 00:00:00 2001 From: konrad Date: Fri, 30 Nov 2018 23:45:22 +0100 Subject: [PATCH] Added docs --- Readme.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 Readme.md diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..5d6932f --- /dev/null +++ b/Readme.md @@ -0,0 +1,156 @@ +# Vikunja Web Handler + +Vikunja was built with a maximum flexibility in mind while developing. To achive this, I built a set of easy-to-use +functions and respective web handlers, all represented through interfaces. + +## CRUDable + +This interface defines methods to Create/Read/ReadAll/Update/Delete something. In order to use the common web +handler, the struct must implement this and the `Rights` interface. + +The interface is defined as followed: + +```go +type CRUDable interface { + Create(Auth) error + ReadOne() error + ReadAll(string, Auth, int) (interface{}, error) + Update() error + Delete() error +} +``` + +Each of these methods is called on an instance of a struct like so: + +```go +func (l *List) ReadOne() (err error) { + *l, err = GetListByID(l.ID) + return +} +``` + +In that case, it takes the `ID` saved in the struct instance, gets the full list object and fills the original object with it. +(See parambinder to understand where that `ID` is coming from). + +All functions should behave like this, if they create or update something, they should return the created/updated struct +instance. The only exception is `ReadAll()` which returns an interface. Usually this is an array, because, well you cannot +make an array of a set type (If you know a way to do this, don't hesitate to drop me a message). + +### Handler Config + +The handler has some options which you can (and need to) configure. + +#### Auth + +`Auth` is an interface with some methods to decouple the action of getting the current user from the web handler. +The function defined via `Auths` should return a struct which implements the `Auth` interface. + +To define the thing which gets the appropriate auth object, you need to call a middleware like so (After all auth middlewares were called): + +#### Logging + +You can provide your own instance of `logger.Logger` (using [this package](https://github.com/op/go-logging)) to the handler. +It will use this instance to log errors which are not better specified or things like users trying to do something they're +not allowed to do and so on. + +#### Full Example + +```go +e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + c.Set("AuthProvider", &web.Auths{ + AuthObject: func(echo.Context) (web.Auth, error) { + return models.GetCurrentUser(c) // Your functions + }, + }) + c.Set("LoggingProvider", &log.Log) + return next(c) + } +}) +``` + +### Pagination + +When using the `ReadAll`-method, the third parameter contains the requested page. Your function should return only the number of results +corresponding to that page. The number of items per page is definied in the config as `service.pagecount` (Get it with `viper.GetInt("service.pagecount")`). + +These can be calculated in combination with a helper function, `getLimitFromPageIndex(pageIndex)` which returns +SQL-needed `limit` (max-length) and `offset` parameters. You can feed this function directly into xorm's `Limit`-Function like so: + +```go +lists := []List{} +err := x.Limit(getLimitFromPageIndex(pageIndex)).Find(&lists) +``` + +### Search + +When using the `ReadAll`-method, the first parameter is a search term which should be used to search items of your struct. You define the critera. + +Users can then pass the `?s=something` parameter to the url to search. + +As the logic for "give me everything" and "give me everything where the name contains 'something'" is mostly the same, we made the decision to design +the function like this, in order to keep the places with mostly the same logic as few as possible. Also just adding `?s=query` to the url one already +knows and uses is a lot more convenient. + +## Rights + +This interface defines methods to check for rights on structs. They accept an `Auth`-element as parameter and return a `bool`. + +The interface is defined as followed: + +```go +type Rights interface { + IsAdmin(Auth) bool + CanWrite(Auth) bool + CanRead(Auth) bool + CanDelete(Auth) bool + CanUpdate(Auth) bool + CanCreate(Auth) bool +} +``` + +When using the standard web handler, all methods except `CanRead()` are called before their `CRUD` counterparts. `CanRead()` +is called after `ReadOne()` was invoked as this would otherwise mean getting an object from the db to check if the user has the +right to see it and then getting it again if thats the case. Calling the function afterwards means we only have to get the +object once. + +## Standard web handler + +You can define routes for the standard web handler like so: + +`models.List` needs to implement `web.CRUDable` and `web.Rights`. + +```go +listHandler := &crud.WebHandler{ + EmptyStruct: func() crud.CObject { + return &models.List{} + }, + } + a.GET("/lists", listHandler.ReadAllWeb) + a.GET("/lists/:list", listHandler.ReadOneWeb) + a.POST("/lists/:list", listHandler.UpdateWeb) + a.DELETE("/lists/:list", listHandler.DeleteWeb) + a.PUT("/namespaces/:namespace/lists", listHandler.CreateWeb) +``` + +The handler will take care of everything like parsing the request, checking rights, pretty-print errors and return appropriate responses. + +## Errors + +Error types with their messages and http-codes should be implemented by you somewhere in your application and then returned by +the appropriate function when an error occures. If the error type implements `HTTPError`, the server returns a user-friendly +error message when this error occours. This means it returns a good HTTP status code, a message, and an error code. The error +code should be unique across all error codes and can be used on the client to show a localized error message or do other stuff +based on the exact error the server returns. That way the client won't have to "guess" that the error message remains the same +over multiple versions of your application. + +An `HTTPError` is defined as follows: + +```go +type HTTPError struct { + HTTPCode int `json:"-"` // Can be any valid HTTP status code, I'd reccomend to use the constants of the http package. + Code int `json:"code"` // Must be a uniqe int identifier for this specific error. I'd reccomend defining a constant for this. + Message string `json:"message"` // A user-readable message what went wrong. +} +``` +