The so-called framework

Frameworks have always been a powerful tool in agile development, enabling developers to quickly get started and build applications, and sometimes even fail to write programs without frameworks. Growth doesn’t happen overnight. From writing programs to getting a sense of accomplishment, to mastering frameworks and building applications quickly, when you’re comfortable with these aspects, try adapting some frameworks or creating your own.

I used to think there were enough frameworks in the Python world, but it turned out to be nothing compared to Golang. The NET/HTTP library provided by Golang is already very good, and the implementation of the HTTP protocol is very good. Based on this, it is not difficult to recreate frameworks, so there are many frameworks in the ecosystem. Since the barrier to building a framework is lower, it also leads to frameworks of varying quality.

Several frameworks were examined by their Github activity, maintained teams, and usage in production environments. Gin was also found to be a lightweight framework to learn from.

Gin

Gin is a golang microframework with elegant packaging, API friendly, and clear source code annotations. Version 1.0 has been released. It is fast and flexible, fault-tolerant and convenient. For Golang, web frameworks are far less dependent than Python, Java, etc. Its own NET/HTTP is simple enough and performs very well. Frameworks are more like collections of common functions or tools. Not only can framework development save time for many of the usual encapsulation, but it also helps with the team’s coding style and specification.

Here is a brief introduction to the use of Gin.

First, you need to install, installation is relatively simple, use go get can:

go get gopkg.in/gin-gonic/gin.v1
Copy the code

Gin’s version is hosted on gopkg’s website. During the installation process, gokpg got stuck and I had to download the source code of the response from Github and copy it to the corresponding directory based on the Godep file in GIN.

We’ll talk more about golang’s package management and dependencies later.

Hello World

Implementing Hello World with Gin is very simple. Create a router and use its Run method:

import (
    "gopkg.in/gin-gonic/gin.v1"
    "net/http"
)

func main(){
    
    router := gin.Default()

    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })
    router.Run(":8000")
}
Copy the code

You can implement a Web service in a few lines of code. Create a routing handler using gin’s Default method. Routing rules and routing functions are then bound via HTTP methods. Unlike the net/ HTTP library routing functions, GIN encapsulates both request and response into the Context of gin.Context. Finally, the Run method that starts the route listens on the port. Small as a sparrow is, it has all the organs. Gin also supports common restful methods such as POST,PUT,DELETE, and OPTION, in addition to GET.

Restful routing

Gin is routed from the Httprouter library. Therefore, httprouter has the same functionality as GIN, but gin does not support routing regular expressions:

func main(){
    router := gin.Default()
    
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })
}
Copy the code

Colon: adds a parameter name to form the route parameter. Its value can be read using the c. params method. Of course the value is a string. Such as /user/rsj217, and /user/hello will match, but /user/ and /user/rsj217/ will not.

☁ ☁ ~ curl http://127.0.0.1:8000/user/rsj217 Hello rsj217% % ~ curl http://127.0.0.1:8000/user/rsj217/ 404 page not found ☁ % ~ curl http://127.0.0.1:8000/user/ 404 page not foundCopy the code

Gin also provides * processing parameters in addition to:, so * matches more rules.

func main(){
    router := gin.Default()
    
    router.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    })
}
Copy the code

The access effect is as follows

☁ ☁ ~ ~ curl http://127.0.0.1:8000/user/rsj217/ rsj217 is / % rsj217 curl http://127.0.0.1:8000/user/rsj217/ China is % / ChinaCopy the code

The Query String argument and the body argument

The services provided by the Web are usually the interaction between client and server. The client sends a request to the server. In addition to routing parameters, other parameters are no more than two types: query String and body parameter. Query String is used for routing. Subsequent concatenation of key1=value2&key2=value2. Of course the key-value is encoded by urlencode.

query string

For arguments, there are often cases where arguments do not exist. Gin also considers whether to provide default values and gives an elegant solution:

func main(){
    router := gin.Default()
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest")
        lastname := c.Query("lastname")

        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })
  router.Run()
}
Copy the code

Read the parameters using the c.de FaultQuery method, which provides a default value if the parameter does not exist. Use the Query method to read a normal argument and return an empty string if the argument does not exist:

☁ ~ curl http://127.0.0.1:8000/welcome Hello Guest % ☁ ~ curl http://127.0.0.1:8000/welcome\? Firstname \ % = Hello China ☁ ~ curl http://127.0.0.1:8000/welcome\? Firstname \ \ = China & lastname \ % = Chinese Hello China ☁ ~ curl http://127.0.0.1:8000/welcome\? Firstname = \ \ & lastname \ = Chinese Hello China % ☁ ~ curl http://127.0.0.1:8000/welcome\? Firstname \=%E4%B8%AD%E5%9B%BD Hello China %Copy the code

The reason for using Chinese is to illustrate urlencode. Note that the default Guest value is not used when firstName is an empty string. The null value is also a value. DefaultQuery provides the default value only when the key does not exist.

body

HTTP messages transfer data in slightly more complex formats than Query String, and there are four common formats. Examples are Application/JSON, Application/X-www-form-urlencoded, Application/XML and multipart/form-data. The latter one is mainly used for uploading pictures. The json format is pretty easy to understand, and the urlencode is actually pretty easy to understand, which is just putting the contents of the Query String inside the body, and you also need the urlencode. By default, C. ostFROM parses x-www-form-urlencoded or FROm-data parameters.

func main(){
    router := gin.Default()
    router.POST("/form_post", func(c *gin.Context) {
        message := c.PostForm("message")
        nick := c.DefaultPostForm("nick", "anonymous")

        c.JSON(http.StatusOK, gin.H{
            "status":  gin.H{
                "status_code": http.StatusOK,
                "status":      "ok",
            },
            "message": message,
            "nick":    nick,
        })
    })
}
Copy the code

Just as GET handles query arguments, the POST method provides cases for handling default arguments. Similarly, if the argument does not exist, you will get an empty string.

☁ ~ curl -x POST http://127.0.0.1:8000/form_post - H "content-type: application/X - WWW - form - urlencoded" - d "message=hello&nick=rsj217" | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 104 100 79 100 25 48555 15365 --:--:-- --:--:-- --:--:-- 79000 { "message": "hello", "nick": "rsj217", "status": { "status": "ok", "status_code": 200 } }Copy the code

We used c. string to return the response, which, as the name implies, returns a string. Content-type is plain or text. Calling c.json returns JSON data. Gin.H is a powerful tool that encapsulates how to generate JSON. Using Golang, you can write faceted JSON like a dynamic language, or for nested JSON implementations, nested gin.H.

Send data to the server, not post, but put as well. Querystring and body are also not separate; both can be sent at the same time:

func main(){
    router := gin.Default()
    
    router.PUT("/post", func(c *gin.Context) {
        id := c.Query("id")
        page := c.DefaultQuery("page", "0")
        name := c.PostForm("name")
        message := c.PostForm("message")
        fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
        c.JSON(http.StatusOK, gin.H{
            "status_code": http.StatusOK,
        })
    })
}
Copy the code

The above example shows sending data to the server using both the query string and the body argument.

File upload

Uploading a single file

The basic send data was introduced earlier, where multipart/form-data forwarding is used for file uploads. Gin file uploads are also very convenient, similar to the native NET/HTTP method, except that gin encapsulates the native request into C. Request.

func main(){ router := gin.Default() router.POST("/upload", func(c *gin.Context) { name := c.PostForm("name") fmt.Println(name) file, header, err := c.Request.FormFile("upload") if err ! = nil { c.String(http.StatusBadRequest, "Bad request") return } filename := header.Filename fmt.Println(file, err, filename) out, err := os.Create(filename) if err ! = nil { log.Fatal(err) } defer out.Close() _, err = io.Copy(out, file) if err ! = nil { log.Fatal(err) } c.String(http.StatusCreated, "upload successful") }) router.Run(":8000") }Copy the code

Parse the client file name property using c.equest. FormFile. If you do not pass the file, an error will be thrown, so you need to handle this error. One way is to return directly. The file data is then copied to the hard disk using OS operations.

To test the upload, use the following command. Note that the upload parameter specified for c.equest. FormFile must be an absolute path:

The curl -x POST http://127.0.0.1:8000/upload - F "upload = @ / Users/ghost/Desktop/PIC. JPG" - H "content-type: multipart/form-data"Copy the code

Uploading multiple files

Uploading a single file is easy. Don’t assume multiple files are too much trouble. By analogy, the so-called multiple files, nothing more than one more time through the file, and then copy the data store. Handler () {route () {route ();}

router.POST("/multi/upload", func(c *gin.Context) { err := c.Request.ParseMultipartForm(200000) if err ! = nil { log.Fatal(err) } formdata := c.Request.MultipartForm files := formdata.File["upload"] for i, _ := range files { / file, err := files[i].Open() defer file.Close() if err ! = nil { log.Fatal(err) } out, err := os.Create(files[i].Filename) defer out.Close() if err ! = nil { log.Fatal(err) } _, err = io.Copy(out, file) if err ! = nil { log.Fatal(err) } c.String(http.StatusCreated, "upload successful") } })Copy the code

This is similar to a single file upload, except that c.equest.multipartform is used to get the file handle, get the file data, and then iterate over the read and write.

Using curl to upload data

Curl -x POST http://127.0.0.1:8000/multi/upload - F "upload = @ / Users/ghost/Desktop/PIC. JPG" - F "upload=@/Users/ghost/Desktop/journey.png" -H "Content-Type: multipart/form-data"Copy the code

Upload form

Using curl uploads, users upload images via forms, ajax, and requests. Here’s how to upload a Web form.

We need to write a form page first, so we need to introduce the Gin How Render template. Earlier we saw C. Sterling and C. Johnson. Let’s take a look at the C.HTML method.

First you need to define a template folder. Then call c.HTML to render the template, and you can pass values to the template via gin.H. At this point, whether it’s String, JSON, HTML, and later XML and YAML, you can see that the Gin packaged interface is simple and easy to use.

Create a “templates” folder and create an HTML file upload.html inside it:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>upload</title> </head> <body> <h3>Single Upload</h3>  <form action="/upload", method="post" enctype="multipart/form-data"> <input type="text" value="hello gin" /> <input type="file" name="upload" />  <input type="submit" value="upload" /> </form> <h3>Multi Upload</h3> <form action="/multi/upload", method="post" enctype="multipart/form-data"> <input type="text" value="hello gin" /> <input type="file" name="upload" />  <input type="file" name="upload" /> <input type="submit" value="upload" /> </form> </body> </html>Copy the code

Upload is simple and takes no parameters. One for single file uploads and one for multiple file uploads.

    router.LoadHTMLGlob("templates/*")
    router.GET("/upload", func(c *gin.Context) {
        c.HTML(http.StatusOK, "upload.html", gin.H{})
    })

Copy the code

Define the template file path using LoadHTMLGlob.

Parameter binding

We have seen x-www-form-urlencoded parameter processing, and now more and more applications are used to JSON communication, that is, whether a response is returned or a request is submitted, The content-Type is in application/ JSON format. Some old Web form pages were x-www-form-urlencoded, which required our server to be able to handle multiple Content-Type parameters.

It’s easy to solve in the Python world, because dynamic languages don’t need to implement defined data models. So you can write a decorator to encapsulate two formats of data into a data model. Golang isn’t easy to handle, but gin has a very powerful model Bind feature.

type User struct { Username string `form:"username" json:"username" binding:"required"` Passwd string `form:"passwd" json:"passwd" bdinding:"required"` Age int `form:"age" json:"age"` } func main(){ router := gin.Default() router.POST("/login", func(c *gin.Context) { var user User var err error contentType := c.Request.Header.Get("Content-Type") switch contentType { case "application/json": err = c.BindJSON(&user) case "application/x-www-form-urlencoded": err = c.BindWith(&user, binding.Form) } if err ! = nil { fmt.Println(err) log.Fatal(err) } c.JSON(http.StatusOK, gin.H{ "user": user.Username, "passwd": user.Passwd, "age": user.Age, }) }) }Copy the code

Define a User model structure, and then apply the BindJSON and BindWith methods once for the client-side Content-Type.

☁ ~ curl -x POST http://127.0.0.1:8000/login - H "content-type: application/X - WWW - form - urlencoded" - d "username=rsj217&passwd=123&age=21" | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 79 100 46 100 33 41181 29543 --:--:-- --:--:-- --:--:-- 46000 { "age": 21, "passwd": "123", "username": "Rsj217} ☁ ~ curl - POST http://127.0.0.1:8000/login - H X" content-type: application/X - WWW - form - urlencoded "- d "username=rsj217&passwd=123&new=21" | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 78 100 45 100 33 37751 27684 --:--:-- --:--:-- --:--:-- 45000 { "age": 0, "passwd": "123", "username": "Rsj217} ☁ ~ curl - POST http://127.0.0.1:8000/login - H X" content-type: application/X - WWW - form - urlencoded "- d "username=rsj217&new=21" | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server No JSON object could be decodedCopy the code

As you can see, the binding tag fields (username and passwd) are set in the structure. If not passed, an error will be thrown. For non-banding fields (age) that are not passed by the client, the User structure will be filled with zero values. Parameters that do not exist in the User structure are automatically ignored.

If you change to JSON, the effect is similar:

☁ ~ curl -x POST http://127.0.0.1:8000/login - H "content-type: application/json" - d '{" username ":" rsj217 ", "passwd" : "123", "age": 21}' | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left  Speed 100 96 100 46 100 50 32670 35511 --:--:-- --:--:-- --:--:-- 50000 { "age": 21, "passwd": "123", "username": "Rsj217} ☁ ~ curl - POST http://127.0.0.1:8000/login - H X" content-type: application/json "- d '{" username" : "rsj217", "passwd": "123", "new": 21}' | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left  Speed 100 95 100 45 100 50 49559 55066 --:--:-- --:--:-- --:--:-- 50000 { "age": 0, "passwd": "123", "username": "Rsj217} ☁ ~ curl - POST http://127.0.0.1:8000/login - H X" content-type: application/json "- d '{" username" : "rsj217", "new": 21}' | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left  Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server No JSON object could be decoded ☁ ~ curl -x POST http://127.0.0.1:8000/login - H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": 123, "new": 21}' | python -m json.tool % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left  Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (52) Empty reply from server No JSON object could be decodedCopy the code

Json has data types. Therefore, {“passwd”: “123”} and {“passwd”: 123} are different data types. Otherwise, an error may occur.

Of course, gin also provides a more advanced method, C. Bind, which automatically deduces whether a bind form or json parameter is a content-type.

router.POST("/login", func(c *gin.Context) { var user User err := c.Bind(&user) if err ! = nil { fmt.Println(err) log.Fatal(err) } c.JSON(http.StatusOK, gin.H{ "username": user.Username, "passwd": user.Passwd, "age": user.Age, }) })Copy the code

Multiformat rendering

Since requests can use different Content-Types, so can responses. Typically the response will be HTML, Text, plain, JSON, XML, and so on. Gin provides a very elegant rendering method. So far, we have seen C.struing, C.json, c.HTML, and here is c.xml.

    router.GET("/render", func(c *gin.Context) {
        contentType := c.DefaultQuery("content_type", "json")
        if contentType == "json" {
            c.JSON(http.StatusOK, gin.H{
                "user":   "rsj217",
                "passwd": "123",
            })
        } else if contentType == "xml" {
            c.XML(http.StatusOK, gin.H{
                "user":   "rsj217",
                "passwd": "123",
            })
        }

    })
Copy the code

The results are as follows:

☁ ~ curl http://127.0.0.1:8000/render\? Content_type \ = json {" passwd ":" 123 ", "user" : "rsj217} ☁ ~ curl http://127.0.0.1:8000/render\? content_type\=xml <map><user>rsj217</user><passwd>123</passwd></map>%Copy the code

redirect

Gin’s request for redirection is fairly simple. Call the context Redirect method:

    router.GET("/redict/google", func(c *gin.Context) {
        c.Redirect(http.StatusMovedPermanently, "https://google.com")
    })
Copy the code

Packet routing

Those of you who are familiar with Flask will be familiar with blueprint groups. Flask provides blueprints for managing organizational grouping apis. Gin also provides the ability to make your code logic more modular, while grouping is also easy to define the scope of middleware use.

    v1 := router.Group("/v1")

    v1.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v1 login")
    })

    v2 := router.Group("/v2")

    v2.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v2 login")
    })
Copy the code

The access effect is as follows:

☁ ~ curl http://127.0.0.1:8000/v1/login v1 login login % % ☁ ~ curl http://127.0.0.1:8000/v2/login v2Copy the code

Middleware middleware

One feature of Golang’s NET/HTTP design is the ease with which middleware can be built. Gin provides similar middleware. Note that the middleware only works with registered routing functions. For packet routing, middleware is nested and the scope of the middleware can be limited. Middleware is divided into global middleware, single routing middleware and group middleware.

Global middleware

Start by defining a middleware function:

func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Println("before middleware")
        c.Set("request", "clinet_request")
        c.Next()
        fmt.Println("before middleware")
    }
}
Copy the code

This function is simple and simply adds an attribute to the C context and assigns a value. The following route handler can extract its value based on the middleware decoration. Note that despite the name global middleware, routes set prior to the process of registering middleware will not be affected by the registered middleware. Middleware decorates routing function rules only if they are registered with middleware code.

    router.Use(MiddleWare())
    {
        router.GET("/middleware", func(c *gin.Context) {
            request := c.MustGet("request").(string)
            req, _ := c.Get("request")
            c.JSON(http.StatusOK, gin.H{
                "middile_request": request,
                "request": req,
            })
        })
    }

Copy the code

Use router to decorate the MiddleWare and then read the request value in /middlerware. Note that routing functions above router.use (MiddleWare()) code will not be decorated by MiddleWare.

The inclusion of decorated routing functions in curly braces is a code specification, even if there are no included routing functions, any routing using the Router is decorated. To differentiate the scope of permission, you can register the middleware with the objects returned by the group.

☁ ~ curl http://127.0.0.1:8000/middleware {" middile_request ":" clinet_request ", "request" : "clinet_request}"Copy the code

Using MustGet to read c without registration will throw an error, and you can use Get instead.

The above registry decoration makes all the code written below default to using the router’s registered middleware.

Single routing middleware

Gin also provides registration for specified routing functions, of course.

    router.GET("/before", MiddleWare(), func(c *gin.Context) {
        request := c.MustGet("request").(string)
        c.JSON(http.StatusOK, gin.H{
            "middile_request": request,
        })
    })
Copy the code

Write the above code before router.use (Middleware()), and also see that /before is decorated with Middleware.

Group middleware

Group middleware is similar, just register the middleware function on the appropriate group route:

Authorized := router.group ("/", MyMiddelware()) //  authorized := router.Group("/") authorized.Use(MyMiddelware()) { authorized.POST("/login", loginEndpoint) }Copy the code

Groups can be nested because middleware can also be nested according to the group’s nesting rules.

Middleware practices

The greatest use of middleware is for logging, error handlers, and authentication of some interfaces. The following implementation of a simple authentication middleware.

    router.GET("/auth/signin", func(c *gin.Context) {
        cookie := &http.Cookie{
            Name:     "session_id",
            Value:    "123",
            Path:     "/",
            HttpOnly: true,
        }
        http.SetCookie(c.Writer, cookie)
        c.String(http.StatusOK, "Login successful")
    })

    router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"data": "home"})
    })
Copy the code

The login function will set a cookie with session_id. Note that you need to specify path as /, otherwise gin will automatically set the cookie path as /auth. The logic of /homne is simple. After registering with AuthMiddleWare, the logic of AuthMiddleWare will be executed before the logic of /home.

The code for AuthMiddleWare is as follows:

func AuthMiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        if cookie, err := c.Request.Cookie("session_id"); err == nil {
            value := cookie.Value
            fmt.Println(value)
            if value == "123" {
                c.Next()
                return
            }
        }
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Unauthorized",
        })
        c.Abort()
        return
    }
}
Copy the code

The cookie is read from the context’s request, and then proofread the cookie, terminating the request and returning directly if there is a problem, using the c.abort () method.

In [7] : resp = requests. Get (' http://127.0.0.1:8000/home ') In [8] : resp. The json () Out [8] : {u 'error' : U 'Unauthorized'} [9] : In the login = requests. Get (' http://127.0.0.1:8000/auth/signin ') In [10] : login. Cookies Out [10] : <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, Domain ='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]> In [11]: Resp = requests. Get (' http://127.0.0.1:8000/home 'cookies = login. Cookies) In [12], resp. The json () Out [12] : {u' data ': u'home'}Copy the code

Asynchronous coroutines

One of Golang’s powerful tools for high concurrency is coroutines. Gin can use coroutines to implement asynchronous tasks. Because asynchronous processes are involved, the requested context needs to be copied to the asynchronous context, which is read-only.

    router.GET("/sync", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        log.Println("Done! in path" + c.Request.URL.Path)
    })

    router.GET("/async", func(c *gin.Context) {
        cCp := c.Copy()
        go func() {
            time.Sleep(5 * time.Second)
            log.Println("Done! in path" + cCp.Request.URL.Path)
        }()
    })
Copy the code

At the time of the request, sleep5 seconds, synchronization logic can see that the service process is asleep. The asynchronous logic sees the response return, and the program is still in the background with the coroutine.

Customize the router

Gin can Run not only with the router of the framework itself, but also with the functionality of NET/HTTP itself:

func main() {
    router := gin.Default()
    http.ListenAndServe(":8080", router)
}
Copy the code

or

func main() {
    router := gin.Default()

    s := &http.Server{
        Addr:           ":8000",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}
Copy the code

Of course, there is an elegant way to restart and end the process. The process of managing Golang with supervisor will be explored later.

conclusion

Gin is a lightweight and powerful Golang Web framework. We have provided a brief overview of common development features. About service startup, processing of request parameters and rendering of response format, as well as for upload and middleware authentication examples are given. A better grasp comes from practice, and gin’s source notes are very detailed. Read the source code for more detailed functions and magic features.

Part of the code in the article