diff --git a/File-server.md b/File-server.md deleted file mode 100644 index 13e5b5d..0000000 --- a/File-server.md +++ /dev/null @@ -1,108 +0,0 @@ -Serve static files from a specific directory (system physical or embedded to the application) is done by the `Party.HandleDir` method. - -`HandleDir` registers a handler that serves HTTP requests -with the contents of a file system (physical or embedded). - -* First parameter : the route path -* Second parameter : the system or the embedded directory that needs to be served -* Third parameter : not required, the directory options, set fields is optional. - -Returns the GET *Route. - -```go -HandleDir(requestPath, directory string, opts ...DirOptions) (getRoute *Route) -``` - -The `DirOptions` structure looks like this: - -```go -type DirOptions struct { - // Defaults to "/index.html", if request path is ending with **/*/$IndexName - // then it redirects to **/*(/) which another handler is handling it, - // that another handler, called index handler, is auto-registered by the framework - // if end developer wasn't managed to handle it manually/by hand. - IndexName string - // Should files served under gzip compression? - Gzip bool - - // List the files inside the current requested directory if `IndexName` not found. - ShowList bool - // If `ShowList` is true then this function will be used instead - // of the default one to show the list of files of a current requested directory(dir). - DirList func(ctx iris.Context, dirName string, dir http.File) error - - // When embedded. - Asset func(name string) ([]byte, error) - AssetInfo func(name string) (os.FileInfo, error) - AssetNames func() []string - - // Optional validator that loops through each found requested resource. - AssetValidator func(ctx iris.Context, name string) bool -} -``` - -Let's say that you have an `./assets` folder near to your executable and you want the files to be served through `http://localhost:8080/static/**/*` route. - -```go -app := iris.New() - -app.HandleDir("/static", "./assets") - -app.Listen(":8080") -``` - -Now, if you want to embed the static files to be lived inside the executable build in order to not depend on a system directory you can use a tool like [go-bindata](https://github.com/go-bindata/go-bindata) to convert the files into `[]byte` inside your program. Let's take a quick tutorial on this and how Iris helps to serve those data. - -Install go-bindata: - -```sh -go get -u github.com/go-bindata/go-bindata/... -``` - -Navigate to your program directory, that the `./assets` subdirectory exists and execute: - -```sh -$ go-bindata ./assets/... -``` - -The above creates a generated go file which contains three main functions: `Asset`, `AssetInfo` and `AssetNames`. Use them on the `DirOptions`: - -```go -// [app := iris.New...] - -app.HandleDir("/static", "./assets", iris.DirOptions { - Asset: Asset, - AssetInfo: AssetInfo, - AssetNames: AssetNames, - Gzip: false, -}) -``` - -Build your app: - -```sh -$ go build -``` - -The `HandleDir` supports all the standards, including content-range, for both physical and embedded directories. - -However, if you just need a handler to work with, without register a route, you can use the `iris.FileServer` package-level function instead. - -The `FileServer` function returns a Handler which serves files from a specific system, phyisical, directory or an embedded one. - -* First parameter: is the directory. -* Second parameter: optional parameter is any optional settings that the caller can use. - -```go -iris.FileServer(directory string, options ...DirOptions) -``` - -**Usage** - -```go -handler := iris.FileServer("./assets", iris.DirOptions { - ShowList: true, Gzip: true, IndexName: "index.html", -}) -``` - -Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server diff --git a/Home.md b/Home.md index d77a4b0..44bbaa0 100644 --- a/Home.md +++ b/Home.md @@ -7,5 +7,3 @@ **Iris** can be used as a web port for [gRPC](https://grpc.io/). REST API for gRPC services. The source code of Iris is hosted on [GitHub ](https://github.com/kataras/iris) and licensed under the terms of [BSD 3-clause License](https://opensource.org/licenses/BSD-3-Clause), like the [Go project](https://github.com/golang/go) itself. - -> Documentation refers to the [v12.1.8 stable release](https://github.com/kataras/iris/tree/v12.1.8). Want to learn about the upcoming release? Head over to the [HISTORY.md#next](https://github.com/kataras/iris/blob/master/HISTORY.md#next) instead. diff --git a/Publications.md b/Publications.md index 4688c43..fa12cdf 100644 --- a/Publications.md +++ b/Publications.md @@ -5,8 +5,8 @@ Prepare a cup of coffee or tea, whatever pleases you the most, and read some art * [How to write a Go API Part 1: A Webserver With Iris](https://bit.ly/32xmf4Q) * [How to write a Go API Part 2: Database Integration With GORM](https://bit.ly/34PvKxR) * [How to write a Go API Part 3: Testing With Dockertest](https://bit.ly/2NoZziF) -* [A URL Shortener Service using Go, Iris and Bolt \(Updated\)](https://bit.ly/2KeP6pE) -* [CRUD REST API in Iris \(a framework for golang\)](https://bit.ly/2X9EsXl) +* [A URL Shortener Service using Go, Iris and Bolt (Updated)](https://bit.ly/2KeP6pE) +* [CRUD REST API in Iris (a framework for golang)](https://bit.ly/2X9EsXl) * [Monitor APIs in realtime using Go and Iris](https://pusher.com/tutorials/monitor-api-go) * [A Todo MVC Application using Iris and Vue.js](https://bit.ly/2yjBvoZ) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://bit.ly/2Kfdsjf) diff --git a/_Sidebar.md b/_Sidebar.md index 49f8c43..fdc0dae 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -24,7 +24,6 @@ * [[Forms]] * [[Model Validation|Model-validation]] * [[Cache]] -* [[File Server|File-server]] * [[View]] * [[Cookies]] * [[Sessions]] @@ -37,6 +36,32 @@ * [[Sitemap]] * [[Localization]] * [[Testing]] +* πŸš€Responses + * [Text](responses/text.md) + * [HTML](responses/html.md) + * [Markdown](responses/markdown.md) + * [XML](responses/xml.md) + * [YAML](responses/yaml.md) + * [Binary](responses/binary.md) + * [JSON](responses/json.md) + * [JSONP](responses/jsonp.md) + * [Problem](responses/problem.md) + * [Protocol Buffers](responses/protobuf.md) + * [MessagePack](responses/messagepack.md) + * [Gzip](responses/gzip.md) + * [Content Negotitation](responses/content-negotiation.md) + * [Stream](responses/stream.md) + * [Server-Sent Events](responses/sse.md) + * [HTTP/2 Push](responses/http2_push.md) + * [Recorder](responses/recorder.md) + * [Outroduction](responses/outroduction.md) + * [➲ Examples](https://github.com/kataras/iris/tree/master/_examples/response-writer) +* πŸ“File Server + * [Introduction](file-server/introduction.md) + * [Listing](file-server/listing.md) + * [In-memory Cache](file-server/memory-cache.md) + * [Serve files from Context](file-server/context-file-server.md) + * [➲ Examples](https://github.com/kataras/iris/tree/master/_examples/file-server) * πŸ€“Resources * [[Starter Kits|Starter-kits]] * [[Publications]] diff --git a/_assets/http2push.png b/_assets/http2push.png new file mode 100644 index 0000000..219568c Binary files /dev/null and b/_assets/http2push.png differ diff --git a/file-server/context-file-server.md b/file-server/context-file-server.md new file mode 100644 index 0000000..cd3df27 --- /dev/null +++ b/file-server/context-file-server.md @@ -0,0 +1,96 @@ +# Serve Files from Context + +```go +// SendFile sends a file as an attachment, that is downloaded and +// saved locally from client. +// Note that compression can be registered through `ctx.CompressWriter(true)` +// or `app.Use(iris.Compression)`. +// Use `ServeFile` if a file should be served as a page asset instead. +SendFile(filename string, destinationName string) error +// SendFileWithRate same as `SendFile` but it can throttle the speed of reading +// and though writing the file to the client. +SendFileWithRate(src, destName string, limit float64, burst int) error +``` + +**Usage** + +Force-Send a file to the client: + +```go +func handler(ctx iris.Context) { + src := "./files/first.zip" + ctx.SendFile(src, "client.zip") +} +``` + +Limit download speed to ~50Kb/s with a burst of 100KB: + +```go +func handler(ctx iris.Context) { + src := "./files/big.zip" + // optionally, keep it empty to resolve the filename based on the "src". + dest := "" + + limit := 50.0 * iris.KB + burst := 100 * iris.KB + ctx.SendFileWithRate(src, dest, limit, burst) +} +``` + +```go +// ServeContent replies to the request using the content in the +// provided ReadSeeker. The main benefit of ServeContent over io.Copy +// is that it handles Range requests properly, sets the MIME type, and +// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, +// and If-Range requests. +// +// If the response's Content-Type header is not set, ServeContent +// first tries to deduce the type from name's file extension. +// +// The name is otherwise unused; in particular it can be empty and is +// never sent in the response. +// +// If modtime is not the zero time or Unix epoch, ServeContent +// includes it in a Last-Modified header in the response. If the +// request includes an If-Modified-Since header, ServeContent uses +// modtime to decide whether the content needs to be sent at all. +// +// The content's Seek method must work: ServeContent uses +// a seek to the end of the content to determine its size. +// +// If the caller has set w's ETag header formatted per RFC 7232, section 2.3, +// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. +// +// Note that *os.File implements the io.ReadSeeker interface. +// Note that compression can be registered through +// `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`. +ServeContent(content io.ReadSeeker, filename string, modtime time.Time) +// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading +// and though writing the "content" to the client. +ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) +// ServeFile replies to the request with the contents of the named +// file or directory. +// +// If the provided file or directory name is a relative path, it is +// interpreted relative to the current directory and may ascend to +// parent directories. If the provided name is constructed from user +// input, it should be sanitized before calling `ServeFile`. +// +// Use it when you want to serve assets like css and javascript files. +// If client should confirm and save the file use the `SendFile` instead. +// Note that compression can be registered through +// `ctx.CompressWriter(true)` +// or `app.Use(iris.Compression)`. +ServeFile(filename string) error +// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading +// and though writing the file to the client. +ServeFileWithRate(filename string, limit float64, burst int) error +``` + +**Usage** + +```go +func handler(ctx iris.Context) { + ctx.ServeFile("./public/main.js") +} +``` \ No newline at end of file diff --git a/file-server/introduction.md b/file-server/introduction.md new file mode 100644 index 0000000..0175aae --- /dev/null +++ b/file-server/introduction.md @@ -0,0 +1,243 @@ +Serve static files from a specific directory (system physical or embedded to the application) is done by the `Party.HandleDir` method. + +`HandleDir` registers a handler that serves HTTP requests with the contents of a file system (physical or embedded). + +* First parameter : the route path +* Second parameter : the file system that needs to be served +* Third parameter : optional, the directory options. + +```go +HandleDir(requestPath string, fs http.FileSystem, opts ...DirOptions) ( []*Route) +``` + +The `DirOptions` structure looks like this: + +```go +type DirOptions struct { + // Defaults to "/index.html", if request path is ending with **/*/$IndexName + // then it redirects to **/*(/) which another handler is handling it, + // that another handler, called index handler, is auto-registered by the framework + // if end developer does not managed to handle it by hand. + IndexName string + // PushTargets filenames (map's value) to + // be served without additional client's requests (HTTP/2 Push) + // when a specific request path (map's key WITHOUT prefix) + // is requested and it's not a directory (it's an `IndexFile`). + // + // Example: + // "/": { + // "favicon.ico", + // "js/main.js", + // "css/main.css", + // } + PushTargets map[string][]string + // PushTargetsRegexp like `PushTargets` but accepts regexp which + // is compared against all files under a directory (recursively). + // The `IndexName` should be set. + // + // Example: + // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$") + // See `iris.MatchCommonAssets` too. + PushTargetsRegexp map[string]*regexp.Regexp + + // Cache to enable in-memory cache and pre-compress files. + Cache DirCacheOptions + // When files should served under compression. + Compress bool + + // List the files inside the current requested directory if `IndexName` not found. + ShowList bool + // If `ShowList` is true then this function will be used instead + // of the default one to show the list of files of a current requested directory(dir). + // See `DirListRich` package-level function too. + DirList DirListFunc + + // Files downloaded and saved locally. + Attachments Attachments + + // Optional validator that loops through each requested resource. + AssetValidator func(ctx *context.Context, name string) bool +} +``` + +## Quick Start + +Let's say that you have an `./assets` folder near to your executable and you want the files to be served through `http://localhost:8080/static/**/*` route. + +```go +app := iris.New() + +app.HandleDir("/static", iris.Dir("./assets")) + +app.Listen(":8080") +``` + +Now, if you want to embed the static files to be lived inside the executable build in order to not depend on a system directory you can use a tool like [go-bindata](https://github.com/go-bindata/go-bindata) to convert the files into `[]byte` inside your program. Let's take a quick tutorial on this and how Iris helps to serve those data. + +Install go-bindata: + +```bash +$ go get -u github.com/go-bindata/go-bindata/v3/go-bindata +``` + +Navigate to your program directory, that the `./assets` subdirectory exists and execute: + +```bash +$ go-bindata -fs -prefix "assets" ./assets/... +``` + +The above creates a generated go file which contains a `AssetFile()` functions that returns a compatible `http.FileSystem` you can give to Iris to serve the files. + +```go +// [app := iris.New...] + +app.HandleDir("/static", AssetFile()) +``` + +Run your app: + +```bash +$ go run . +``` + +The `HandleDir` supports all the standards, including content-range, for both physical, embedded and cached directories. + +However, if you just need a handler to work with, without register a route, you can use the `iris.FileServer` package-level function instead. + +The `FileServer` function returns a Handler which serves files from a specific system directory, an embedded one or a memory-cached one. + +```go +iris.FileServer(fs http.FileSystem, options DirOptions) +``` + +**Usage** + +```go +handler := iris.FileServer(iris.Dir("./assets"), iris.DirOptions { + ShowList: true, Compress: true, IndexName: "index.html", +}) +``` + +## Example + +Let's create an application that users can upload one or more files and list them. + +Copy-paste the contents of **main.go**: + +```go +package main + +import ( + "crypto/md5" + "fmt" + "io" + "mime/multipart" + "os" + "strconv" + "strings" + "time" + + "github.com/kataras/iris/v12" +) + +func init() { + os.Mkdir("./uploads", 0700) +} + +func main() { + app := iris.New() + app.RegisterView(iris.HTML("./views", ".html")) + // Serve assets (e.g. javascript, css). + app.HandleDir("/public", iris.Dir("./public")) + + app.Get("/", index) + + app.Get("/upload", uploadView) + app.Post("/upload", upload) + + app.HandleDir("/files", iris.Dir("./uploads"), iris.DirOptions{ + Compress: true, + ShowList: true, + }) + + app.Listen(":8080") +} + +func index(ctx iris.Context) { + ctx.Redirect("/upload") +} + +func uploadView(ctx iris.Context) { + now := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(now, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + ctx.View("upload.html", token) +} + +const maxSize = 10 * iris.MB + +func upload(ctx iris.Context) { + ctx.SetMaxRequestBodySize(maxSize) + + _, err := ctx.UploadFormFiles("./uploads", beforeSave) + if err != nil { + ctx.StopWithError(iris.StatusPayloadTooRage, err) + return + } + + ctx.Redirect("/files") +} + +func beforeSave(ctx iris.Context, file *multipart.FileHeader) { + ip := ctx.RemoteAddr() + ip = strings.ReplaceAll(ip, ".", "_") + ip = strings.ReplaceAll(ip, ":", "_") + + file.Filename = ip + "-" + file.Filename +} +``` + +The **./views/upload.html** is a simple html form, looks like that: + +```html + + + + + + + Upload Files + + + +
+ + + + + +
+ + + + + +``` diff --git a/file-server/listing.md b/file-server/listing.md new file mode 100644 index 0000000..b4fa606 --- /dev/null +++ b/file-server/listing.md @@ -0,0 +1,366 @@ +By default Iris will not list files and sub-directories when client requests a path of a directory (e.g. `http://localhost:8080/files/folder`). To enable file listing you just set `DirOptions.ShowList` to `true`: + +```go +options := iris.DirOptions{ + // [...] + ShowList: true, +} + +app.HandleDir("/files", iris.Dir("./uploads"), options) +``` + +By default listing is rendering a simple page of `= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", + float64(b)/float64(div), "kMGTPE"[exp]) + }) + app.RegisterView(view) + + // Serve assets (e.g. javascript, css). + // app.HandleDir("/public", "./public") + + app.Get("/", index) + + app.Get("/upload", uploadView) + app.Post("/upload", upload) + + filesRouter := app.Party("/files") + { + filesRouter.HandleDir("/", iris.Dir(uploadDir), iris.DirOptions{ + Compress: true, + ShowList: true, + DirList: iris.DirListRich(iris.DirListRichOptions{ + // Optionally, use a custom template for listing: + // Tmpl: dirListRichTemplate, + TmplName: "dirlist.html", + }), + }) + + auth := basicauth.New(basicauth.Config{ + Users: map[string]string{ + "myusername": "mypassword", + }, + }) + + filesRouter.Delete("/{file:path}", auth, deleteFile) + } + + app.Listen(":8080") +} + +func index(ctx iris.Context) { + ctx.Redirect("/upload") +} + +func uploadView(ctx iris.Context) { + now := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(now, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + ctx.View("upload.html", token) +} + +func upload(ctx iris.Context) { + ctx.SetMaxRequestBodySize(maxSize) + + _, err := ctx.UploadFormFiles(uploadDir, beforeSave) + if err != nil { + ctx.StopWithError(iris.StatusPayloadTooRage, err) + return + } + + ctx.Redirect("/files") +} + +func beforeSave(ctx iris.Context, file *multipart.FileHeader) { + ip := ctx.RemoteAddr() + ip = strings.ReplaceAll(ip, ".", "_") + ip = strings.ReplaceAll(ip, ":", "_") + + file.Filename = ip + "-" + file.Filename +} + +func deleteFile(ctx iris.Context) { + // It does not contain the system path, + // as we are not exposing it to the user. + fileName := ctx.Params().Get("file") + + filePath := path.Join(uploadDir, fileName) + + if err := os.RemoveAll(filePath); err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } + + ctx.Redirect("/files") +} +``` + +The **views/upload.html** should look like that: + +```html + + + + + + + Upload Files + + + +
+ + + + + +
+ + + + + +``` + +And finally the **customized listing page**. Copy-paste the following code to the **views/dirlist.html** file: + +```html + + + + + + + {{.Title}} + + + + + + + + + + + + + + + {{ range $idx, $file := .Files }} + + + + {{ if $file.Info.IsDir }} + + {{ else }} + + {{ end }} + + + + {{ end }} + +
#NameSizeActions
{{ $idx }}{{ $file.Name }}Dir{{ formatBytes $file.Info.Size }}
+ + + + +``` diff --git a/file-server/memory-cache.md b/file-server/memory-cache.md new file mode 100644 index 0000000..2a0ffb0 --- /dev/null +++ b/file-server/memory-cache.md @@ -0,0 +1,3 @@ +# Memory Cache + +TODO \ No newline at end of file diff --git a/mvc/mvc-grpc.md b/mvc/mvc-grpc.md new file mode 100644 index 0000000..699acb7 --- /dev/null +++ b/mvc/mvc-grpc.md @@ -0,0 +1,138 @@ +# gRPC + +gRPC[\*](https://grpc.io/) is a modern open source high performance Remote Procedure Call framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services. + +Iris and gRPC integration lives inside the [mvc](https://github.com/kataras/iris/tree/master/mvc) package. + +Have you ever have difficulties converting your app or parts of it from HTTP to gGRPC or did you ever wish you had decent HTTP framework support as well for your gRPC services? Now, with Iris you have the best of two worlds. Without change a bit of your existing gRPC services code, you can register them as Iris HTTP routes through a Controller \(your service struct value\). + +![\_assets/grpc-compatible-question.png](../.gitbook/assets/grpc-compatible-question.png) + +> Learn more about our conversation at: [https://github.com/kataras/iris/issues/1449\#issuecomment-623260695](https://github.com/kataras/iris/issues/1449#issuecomment-623260695) + +## Step by step + +We will follow the [official helloworld gRPC example](https://github.com/grpc/grpc-go/tree/master/examples/helloworld). If you had already work with gRPC services you can skip 1-5. + +**1.** Let's write our proto schema for request and response. + +```text +syntax = "proto3"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} +``` + +**2.** Install the protoc Go plugin + +```bash +$ go get -u github.com/golang/protobuf/protoc-gen-go +``` + +**3.** Generate Go file from the helloworld.proto file above + +```bash +$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld +``` + +**4.** Implement a gRPC service as usually, with or without Iris + +```go +import ( + // [...] + pb "myapp/helloworld" + "context" +) +``` + +```go +type Greeter struct { } + +// SayHello implements helloworld.GreeterServer. +func (c *Greeter) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil +} +``` + +Iris automatically binds the standard "context" `context.Context` to `iris.Context.Request().Context()` and any other structure that is not mapping to a registered dependency as a payload \(depending on the request\), e.g XML, YAML, Query, Form, JSON, Protobuf. + +**5.** Register your service to the gRPC server + +```go +import ( + // [...] + pb "myapp/helloworld" + "google.golang.org/grpc" +) +``` + +```go +grpcServer := grpc.NewServer() + +myService := &Greeter{} +pb.RegisterGreeterServer(grpcServer, myService) +``` + +**6.** Register this `myService` to Iris + +The `mvc.New(party).Handle(ctrl, mvc.GRPC{...})` option allows to register gRPC services per-party \(without the requirement of a full wrapper\) and optionally strict access to gRPC-only clients. + +Register MVC application controller for gRPC services. You can bind as many mvc gRpc services in the same Party or app, as the `ServiceName` differs. + +```go +import ( + // [...] + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" +) +``` + +```go +app := iris.New() + +rootApp := mvc.New(app) +rootApp.Handle(myService, mvc.GRPC{ + Server: grpcServer, // Required. + ServiceName: "helloworld.Greeter", // Required. + Strict: false, +}) +``` + +**7.** Generate TLS Keys + +The Iris server **should ran under TLS** \(it's a gRPC requirement\). + +```bash +$ openssl genrsa -out server.key 2048 +$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +``` + +**8.** Listen and Serve + +```go +app.Run(iris.TLS(":443", "server.crt", "server.key")) +``` + +POST: `https://localhost:443/helloworld.Greeter/SayHello` with request data: `{"name": "John"}` xpected output: `{"message": "Hello John"}`. + +Both HTTP Client and gRPC client will be able to communicate with our Iris+gRPC service. + +### Exercise files + +Full Server, Clients and Testing Code can be found at: [https://github.com/kataras/iris/tree/master/\_examples/mvc/grpc-compatible](https://github.com/kataras/iris/tree/master/_examples/mvc/grpc-compatible). + diff --git a/mvc/mvc-quickstart.md b/mvc/mvc-quickstart.md new file mode 100644 index 0000000..d595744 --- /dev/null +++ b/mvc/mvc-quickstart.md @@ -0,0 +1,379 @@ +# Quick start + +The following guide is just a simple example of usage of some of the **Iris MVC** features. You are not limited to that data structure or code flow. + +Create a folder, let's assume its path is `app`. The structure should look like that: + +```text +β”‚ main.go +β”‚ go.mod +β”‚ go.sum +└───environment +β”‚ environment.go +└───model +β”‚ request.go +β”‚ response.go +└───database +β”‚ database.go +β”‚ mysql.go +β”‚ sqlite.go +└───service +β”‚ greet_service.go +└───controller +β”‚ greet_controller.go +``` + +Navigate to that `app` folder and execute the following command: + +```bash +$ go init app +$ go get -u github.com/kataras/iris/v12@master +# or @latest for the latest official release. +``` + +## Environment + +Let's start by defining the available environments that our web-application can behave on. + +We'll just work on two available environments, the "development" and the "production", as they define the two most common scenarios. The `ReadEnv` will read from the `Env` type of a system's environment variable (see `main.go` in the end of the page). + +Create a `environment/environment.go` file and put the following contents: + +```go +package environment + +import ( + "os" + "strings" +) + +const ( + PROD Env = "production" + DEV Env = "development" +) + +type Env string + +func (e Env) String() string { + return string(e) +} + +func ReadEnv(key string, def Env) Env { + v := Getenv(key, def.String()) + if v == "" { + return def + } + + env := Env(strings.ToLower(v)) + switch env { + case PROD, DEV: // allowed. + default: + panic("unexpected environment " + v) + } + + return env +} + +func Getenv(key string, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + + return def +} +``` + +## Database + +We will use two database management systems, the `MySQL` and the `SQLite`. The first one for "production" use and the other for "development". + +Create a `database/database.go` file and copy-paste the following: + +```go +package database + +import "app/environment" + +type DB interface { + Exec(q string) error +} + +func NewDB(env environment.Env) DB { + switch env { + case environment.PROD: + return &mysql{} + case environment.DEV: + return &sqlite{} + default: + panic("unknown environment") + } +} +``` + +Let's simulate our MySQL and SQLite `DB` instances. Create a `database/mysql.go` file which looks like the following one: + +```go +package database + +import "fmt" + +type mysql struct{} + +func (db *mysql) Exec(q string) error { + return fmt.Errorf("mysql: not implemented <%s>", q) +} +``` + +And a `database/sqlite.go` file. + +```go +package database + +type sqlite struct{} + +func (db *sqlite) Exec(q string) error { return nil } +``` + +The `DB` depends on the \`Environment. + +> A practical and operational database example, including Docker images, can be found at the following guide: [https://github.com/kataras/iris/tree/master/\_examples/database/mysql](https://github.com/kataras/iris/tree/master/_examples/database/mysql) + +## Service + +We'll need a service that will communicate with a database instance in behalf of our Controller\(s\). + +In our case we will only need a single service, the Greet Service. + +For the sake of the example, let's use two implementations of a greet service based on the `Environment`. The `GreetService` interface contains a single method of `Say(input string) (output string, err error)`. Create a `./service/greet_service.go` file and write the following code: + +```go +package service + +import ( + "fmt" + + "app/database" + "app/environment" +) + +// GreetService example service. +type GreetService interface { + Say(input string) (string, error) +} + +// NewGreetService returns a service backed with a "db" based on "env". +func NewGreetService(env environment.Env, db database.DB) GreetService { + service := &greeter{db: db, prefix: "Hello"} + + switch env { + case environment.PROD: + return service + case environment.DEV: + return &greeterWithLogging{service} + default: + panic("unknown environment") + } +} + +type greeter struct { + prefix string + db database.DB +} + +func (s *greeter) Say(input string) (string, error) { + if err := s.db.Exec("simulate a query..."); err != nil { + return "", err + } + + result := s.prefix + " " + input + return result, nil +} + +type greeterWithLogging struct { + *greeter +} + +func (s *greeterWithLogging) Say(input string) (string, error) { + result, err := s.greeter.Say(input) + fmt.Printf("result: %s\nerror: %v\n", result, err) + return result, err +} +``` + +The `greeter` will be used on "production" and the `greeterWithLogging` on "development". The `GreetService` depends on the `Environment` and the `DB`. + +## Models + +Continue by creating our HTTP request and response models. + +Create a `model/request.go` file and copy-paste the following code: + +```go +package model + +type Request struct { + Name string `url:"name"` +} +``` + +Same for the `model/response.go` file. + +```go +package model + +type Response struct { + Message string `json:"msg"` +} +``` + +The server will accept a URL Query Parameter of `name` \(e.g. `/greet?name=kataras`\) and will reply back with a JSON message. + +## Controller + +MVC Controllers are responsible for controlling the flow of the application execution. When you make a request \(means request a page\) to MVC Application, a controller is responsible for returning the response to that request. + +We will only need the `GreetController` for our mini web-application. Create a file at `controller/greet_controller.go` which looks like that: + +```go +package controller + +import ( + "app/model" + "app/service" +) + +type GreetController struct { + Service service.GreetService + // Ctx iris.Context +} + +func (c *GreetController) Get(req model.Request) (model.Response, error) { + message, err := c.Service.Say(req.Name) + if err != nil { + return model.Response{}, err + } + + return model.Response{Message: message}, nil +} +``` + +The `GreetController` depends on the `GreetService`. It serves the `GET: /greet` index path through its `Get` method. The `Get` method accepts a `model.Request` which contains a single field name of `Name` which will be extracted from the `URL Query Parameter 'name'` \(because it's a `GET` requst and its `url:"name"` struct field\). And it will respond with a `model.Response` (JSON) or an error. + +{% page-ref page="../dependency-injection/inputs.md" %} +{% page-ref page="../dependency-injection/outputs.md" %} + +## Wrap up + +```sh + +-------------------+ + | Env (DEV, PROD) | + +---------+---------+ + | | | + | | | + | | | + DEV | | | PROD +-------------------+---------------------+ | +----------------------+------------------- + | | | + | | | + +---+-----+ +----------------v------------------+ +----+----+ + | sqlite | | NewDB(Env) DB | | mysql | + +---+-----+ +----------------+---+--------------+ +----+----+ + | | | | + | | | | + | | | | + +--------------+-----+ +-------------------v---v-----------------+ +----+------+ + | greeterWithLogging | | NewGreetService(Env, DB) GreetService | | greeter | + +--------------+-----+ +---------------------------+-------------+ +----+------+ + | | | + | | | + | +-----------------------------------------+ | + | | GreetController | | | + | | | | | + | | - Service GreetService <-- | | + | | | | + | +-------------------+---------------------+ | + | | | + | | | + | | | + | +-----------+-----------+ | + | | HTTP Request | | + | +-----------------------+ | + | | /greet?name=kataras | | + | +-----------+-----------+ | + | | | ++------------------+--------+ +------------+------------+ +-------+------------------+ +| model.Response (JSON) | | Response (JSON, error) | | Bad Request | ++---------------------------+ +-------------------------+ +--------------------------+ +| { | | mysql: not implemented | +| "msg": "Hello kataras" | +--------------------------+ +| } | ++---------------------------+ +``` + +Now it's the time to wrap all the above into our `main.go` file. Copy-paste the following code: + +```go +package main + +import ( + "app/controller" + "app/database" + "app/environment" + "app/service" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" +) + +func main() { + app := iris.New() + app.Get("/ping", pong).Describe("healthcheck") + + mvc.Configure(app.Party("/greet"), setup) + + // http://localhost:8080/greet?name=kataras + app.Listen(":8080", iris.WithLogLevel("debug")) +} + +func pong(ctx iris.Context) { + ctx.WriteString("pong") +} + +func setup(app *mvc.Application) { + // Register Dependencies. + app.Register( + environment.DEV, // DEV, PROD + database.NewDB, // sqlite, mysql + service.NewGreetService, // greeterWithLogging, greeter + ) + + // Register Controllers. + app.Handle(new(controller.GreetController)) +} +``` + +The `mvc.Application.Register` method registers one more dependencies, dependencies can depend on previously registered dependencies too. Thats the reason we pass, first, the `Environment(DEV)`, then the `NewDB` which depends on that `Environment`, following by the `NewGreetService` function which depends on both the `Environment(DEV)` and the `DB`. + +The `mvc.Application.Handle` registers a new controller, which depends on the `GreetService`, for the targeted sub-router of `Party("/greet")`. + +## Run + +Install [Go](https://golang.org/dl) and run the application with: + +```bash +go run main.go +``` + +### Or Docker + +Download the [Dockerfile](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/Dockerfile) and [docker-compose.yml](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/docker-compose.yml) files to the `app` folder. + +Install [Docker](https://www.docker.com/) and execute the following command: + +```bash +$ docker-compose up +``` + +Visit [http://localhost:8080?name=kataras](http://localhost:8080?name=kataras). + +Optionally, replace the `main.go`'s `app.Register(environment.DEV` with `environment.PROD`, restart the application and refresh. You will see that a new database \(`sqlite`\) and another service of \(`greeterWithLogging`\) will be binded to the `GreetController`. With **a single change** you achieve to completety change the result. diff --git a/mvc/mvc-sessions.md b/mvc/mvc-sessions.md new file mode 100644 index 0000000..66028fc --- /dev/null +++ b/mvc/mvc-sessions.md @@ -0,0 +1,68 @@ +# Session Controller + +Create a `main.go` file and copy-paste the following code snippets: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" + "github.com/kataras/iris/v12/sessions" +) + +func main() { + app := iris.New() + + // Configure sessions manager as we used to. + sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) + app.Use(sess.Handler()) + + visitApp := mvc.New(app) + visitApp.Register(time.Now()) + visitApp.Handle(new(VisitController)) + + app.Listen(":8080") +} +``` + +## Controller + +- The `VisitController.Session` is automatically binded to the current `sessions.Session`. +- The `VisitController.StartTime` is statically set to the server's start time with `.Register(time.Now())` above. + +```go +type VisitController struct { + Session *sessions.Session + + StartTime time.Time +} + +func (c *VisitController) Get() string { + // it increments a "visits" value of integer by one, + // if the entry with key 'visits' doesn't exist + // it will create it for you. + visits := c.Session.Increment("visits", 1) + // write the current, updated visits. + since := time.Now().Sub(c.StartTime).Seconds() + return fmt.Sprintf("%d visit(s) from my current session in %0.1f seconds of server's up-time", + visits, since) +} +``` + +## Run + +Open a terminal session and execute: + +```sh +$ go run main.go +``` + +1. Prepare a client, e.g. your browser +2. navigate to http://localhost:8080 +3. refresh the page some times +4. close the browser +5. re-open the browser (if it wasn't in private mode) and re-play 2. diff --git a/mvc/mvc-websockets.md b/mvc/mvc-websockets.md new file mode 100644 index 0000000..bf32e3e --- /dev/null +++ b/mvc/mvc-websockets.md @@ -0,0 +1,83 @@ +# Websocket Controller + +Iris has a trivial way of registering websocket events via a Go structure. The websocket controller is part of the [MVC](mvc.md) features. + +Iris has its own `iris/mvc/Application.HandleWebsocket(v interface{}) *neffos.Struct` to register controllers in existing Iris MVC applications\(offering a fully featured dependency injection container for request values and static services\). + +```go +// HandleWebsocket handles a websocket specific controller. +// Its exported methods are the events. +// If a "Namespace" field or method exists then namespace is set, +// otherwise empty namespace will be used for this controller. +// +// Note that a websocket controller is registered and ran under +// a connection connected to a namespace +// and it cannot send HTTP responses on that state. +// However all static and dynamic dependencies behave as expected. +func (*mvc.Application) HandleWebsocket(controller interface{}) *neffos.Struct +``` + +Let's see a usage example, we want to bind the `OnNamespaceConnected`, `OnNamespaceDisconnect` built-in events and a custom `"OnChat"` event with our controller's methods. + +**1.** We create the controller by declaring a NSConn type field as `stateless` and write the methods we need. + +```go +type websocketController struct { + *neffos.NSConn `stateless:"true"` + Namespace string + + Logger MyLoggerInterface +} + +func (c *websocketController) OnNamespaceConnected(msg neffos.Message) error { + return nil +} + +func (c *websocketController) OnNamespaceDisconnect(msg neffos.Message) error { + return nil +} + +func (c *websocketController) OnChat(msg neffos.Message) error { + return nil +} +``` + +Iris is smart enough to catch the `Namespace string` struct field to use it to register the controller's methods as events for that namespace, alternatively you can create a controller method of `Namespace() string { return "default" }` or use the `HandleWebsocket`'s return value to `.SetNamespace("default")`, it's up to you. + +**2.** We inititalize our MVC application targets to a websocket endpoint, as we used to do with regular HTTP Controllers for HTTP routes. + +```go +import ( + // [...] + "github.com/kataras/iris/v12/mvc" +) +// [app := iris.New...] + +mvcApp := mvc.New(app.Party("/websocket_endpoint")) +``` + +**3.** We register our dependencies, if any. + +```go +mvcApp.Register( + &prefixedLogger{prefix: "DEV"}, +) +``` + +**4.** We register one or more websocket controllers, each websocket controller maps to one namespace \(just one is enough, as in most of the cases you don't need more, but that depends on your app's needs and requirements\). + +```go +mvcApp.HandleWebsocket(&websocketController{Namespace: "default"}) +``` + +**5.** Next, we continue by mapping the mvc application as a connection handler to a websocket server \(you may use more than one mvc applications per websocket server via `neffos.JoinConnHandlers(mvcApp1, mvcApp2)`\). + +```go +websocketServer := neffos.New(websocket.DefaultGorillaUpgrader, mvcApp) +``` + +**6.** And the last step is to register that server to our endpoint through a normal `.Get` method. + +```go +mvcApp.Router.Get("/", websocket.Handler(websocketServer)) +``` \ No newline at end of file diff --git a/mvc/mvc.md b/mvc/mvc.md new file mode 100644 index 0000000..1861794 --- /dev/null +++ b/mvc/mvc.md @@ -0,0 +1,293 @@ +# MVC + +![\_assets/web\_mvc\_diagram.png](../.gitbook/assets/web_mvc_diagram.png) + +Using Iris MVC for code reuse. + +By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same \(or similar\) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user. + +Iris has **first-class support for the MVC \(Model View Controller\) architectural pattern**, you'll not find these stuff anywhere else in the Go world. You will have to import the [iris/mvc](https://github.com/kataras/iris/tree/master/mvc) subpackage. + +```go +import "github.com/kataras/iris/v12/mvc" +``` + +Iris web framework supports Request data, Models, Persistence Data and Binding with the fastest possible execution. + +If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). + +**Note:** Read the [Dependency Injection](../dependency-injection/dependency-injection.md) section before continue. + +**Characteristics** + +All HTTP Methods are supported, for example if want to serve `GET` then the controller should have a function named `Get()`, for `POST` verb use the `Post()`, for a parameter use the `By` keyword, e.g. `PostUserBy(id uint64)` which translates to POST: [...]/User/{id}. + +Serve custom controller's struct's methods as handlers with custom paths\(even with regex parametermized path\) via the `BeforeActivation` custom event callback, per-controller. Example: + +```go +import ( + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" +) + +func main() { + app := iris.New() + mvc.Configure(app.Party("/root"), myMVC) + app.Listen(":8080") +} + +func myMVC(app *mvc.Application) { + // app.Register(...) + // app.Router.Use/UseGlobal/Done(...) + app.Handle(new(MyController)) +} + +type MyController struct {} + +func (m *MyController) BeforeActivation(b mvc.BeforeActivation) { + // b.Dependencies().Add/Remove + // b.Router().Use/UseGlobal/Done + // and any standard Router API call you already know + + // 1-> Method + // 2-> Path + // 3-> The controller's function name to be parsed as handler + // 4-> Any handlers that should run before the MyCustomHandler + b.Handle("GET", "/something/{id:int64}", "MyCustomHandler", anyMiddleware...) +} + +// GET: http://localhost:8080/root +func (m *MyController) Get() string { + return "Hey" +} + +// GET: http://localhost:8080/root/something/{id:int64} +func (m *MyController) MyCustomHandler(id int64) string { + return "MyCustomHandler says Hey" +} +``` + +Persistence data inside your Controller struct \(share data between requests\) by defining services to the Dependencies or have a `Singleton` controller scope. + +Share the dependencies between controllers or register them on a parent MVC Application, and ability to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, i.e `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`. + +Access to the `Context` as a controller's field\(no manual binding is neede\) i.e `Ctx iris.Context` or via a method's input argument, i.e `func(ctx iris.Context, otherArguments...)`. + +Models inside your Controller struct \(set-ed at the Method function and rendered by the View\). You can return models from a controller's method or set a field in the request lifecycle and return that field to another method, in the same request lifecycle. + +Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. `Controllers` can be registered to any `Party`, including Subdomains, the Party's begin and done handlers work as expected. + +Optional `BeginRequest(ctx)` function to perform any initialization before the method execution, useful to call middlewares or when many methods use the same collection of data. + +Optional `EndRequest(ctx)` function to perform any finalization after any method executed. + +Inheritance, recursively, e.g. our [mvc session-controller example](https://github.com/kataras/iris/tree/master/_examples/mvc/session-controller), has the `Session *sessions.Session` as struct field which is filled by the sessions manager. + +Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods with the `By` word, uppercase is a new sub path. Example: + +If `mvc.New(app.Party("/user")).Handle(new(user.Controller))` + +* `func(*Controller) Get()` - `GET:/user`. +* `func(*Controller) Post()` - `POST:/user`. +* `func(*Controller) GetLogin()` - `GET:/user/login` +* `func(*Controller) PostLogin()` - `POST:/user/login` +* `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` +* `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers` +* `func(*Controller) GetBy(id int64)` - `GET:/user/{param:int64}` +* `func(*Controller) PostBy(id int64)` - `POST:/user/{param:int64}` + +If `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` + +* `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + +If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` + +* `func(*Controller) GetByWildcard(path string)` - `GET:/assets/{param:path}` + + Supported types for method functions receivers: int, int64, bool and string. + +Optionally, response via output arguments, like we've shown at the [Dependency Injection](../dependency-injection/dependency-injection.md) chapter. E.g. + +```go +func(c *ExampleController) Get() string | + (string, string) | + (string, int) | + int | + (int, string) | + (string, error) | + error | + (int, error) | + (any, bool) | + (customStruct, error) | + customStruct | + (customStruct, int) | + (customStruct, string) | + mvc.Result or (mvc.Result, error) +``` + +Where `mvc.Result` is just a type alias of `hero.Result`: + +```go +type Result interface { + // Dispatch should sends the response to the context's response writer. + Dispatch(ctx iris.Context) +} +``` + +## Example + +This example is equivalent to a simple hello world application. + +It seems that additional code you have to write doesn't worth it but remember that, this example does not make use of iris mvc features like the Model, Persistence or the View engine neither the Session, it's very simple for learning purposes, probably you'll never use such as simple controller anywhere in your app. + +The cost we have on this example for using MVC on the "/hello" path which serves JSON is ~2MB per 20MB throughput on my personal laptop, it's tolerated for the majority of the applications but you can choose what suits you best with Iris, low-level handlers: performance or high-level controllers: easier to maintain and smaller codebase on large applications. + +**Read the comments carefully** + +```go +package main + +import ( + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" + + "github.com/kataras/iris/v12/middleware/logger" + "github.com/kataras/iris/v12/middleware/recover" +) + +func main() { + app := iris.New() + // Optionally, add two built'n handlers + // that can recover from any http-relative panics + // and log the requests to the terminal. + app.Use(recover.New()) + app.Use(logger.New()) + + // Serve a controller based on the root Router, "/". + mvc.New(app).Handle(new(ExampleController)) + + // http://localhost:8080 + // http://localhost:8080/ping + // http://localhost:8080/hello + // http://localhost:8080/custom_path + app.Listen(":8080") +} + +// ExampleController serves the "/", "/ping" and "/hello". +type ExampleController struct{} + +// Get serves +// Method: GET +// Resource: http://localhost:8080 +func (c *ExampleController) Get() mvc.Result { + return mvc.Response{ + ContentType: "text/html", + Text: "

Welcome

", + } +} + +// GetPing serves +// Method: GET +// Resource: http://localhost:8080/ping +func (c *ExampleController) GetPing() string { + return "pong" +} + +// GetHello serves +// Method: GET +// Resource: http://localhost:8080/hello +func (c *ExampleController) GetHello() interface{} { + return map[string]string{"message": "Hello Iris!"} +} + +// BeforeActivation called once, before the controller adapted to the main application +// and of course before the server ran. +// After version 9 you can also add custom routes for a specific controller's methods. +// Here you can register custom method's handlers +// use the standard router with `ca.Router` to +// do something that you can do without mvc as well, +// and add dependencies that will be binded to +// a controller's fields or method function's input arguments. +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { + anyMiddlewareHere := func(ctx iris.Context) { + ctx.Application().Logger().Warnf("Inside /custom_path") + ctx.Next() + } + + b.Handle( + "GET", + "/custom_path", + "CustomHandlerWithoutFollowingTheNamingGuide", + anyMiddlewareHere, + ) + + // or even add a global middleware based on this controller's router, + // which in this example is the root "/": + // b.Router().Use(myMiddleware) +} + +// CustomHandlerWithoutFollowingTheNamingGuide serves +// Method: GET +// Resource: http://localhost:8080/custom_path +func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string { + return "hello from the custom handler without following the naming guide" +} + +// GetUserBy serves +// Method: GET +// Resource: http://localhost:8080/user/{username:string} +// By is a reserved "keyword" to tell the framework that you're going to +// bind path parameters in the function's input arguments, and it also +// helps to have "Get" and "GetBy" in the same controller. +// +// func (c *ExampleController) GetUserBy(username string) mvc.Result { +// return mvc.View{ +// Name: "user/username.html", +// Data: username, +// } +// } + +/* Can use more than one, the factory will make sure +that the correct http methods are being registered for each route +for this controller, uncomment these if you want: + +func (c *ExampleController) Post() {} +func (c *ExampleController) Put() {} +func (c *ExampleController) Delete() {} +func (c *ExampleController) Connect() {} +func (c *ExampleController) Head() {} +func (c *ExampleController) Patch() {} +func (c *ExampleController) Options() {} +func (c *ExampleController) Trace() {} +*/ + +/* +func (c *ExampleController) All() {} +// OR +func (c *ExampleController) Any() {} + + + +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { + // 1 -> the HTTP Method + // 2 -> the route's path + // 3 -> this controller's method name that should be handler for that route. + b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) +} + +// After activation, all dependencies are set-ed - so read only access on them +// but still possible to add custom controller or simple standard handlers. +func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {} + +*/ +``` + +Every `exported` func prefixed with an HTTP Method\(`Get`, `Post`, `Put`, `Delete`...\) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. + +An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server \(including the TCP port\): `localhost:8080` and the target URI `/helloworld`. + +The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL. + +Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "[http://localhost:8080/helloworld/welcome/golang/32719](http://localhost:8080/helloworld/welcome/golang/32719)", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead. + +> The [\_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller\_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advandage of the Iris MVC Binder, Iris MVC Models and more... diff --git a/responses/binary.md b/responses/binary.md new file mode 100644 index 0000000..d8b555a --- /dev/null +++ b/responses/binary.md @@ -0,0 +1,15 @@ +**Content-Type: "application/octet-stream"** + +A MIME attachment with the content type `"application/octet-stream"` is a **binary** file. Typically, it will be an application or a document that must be opened in an application, such as a spreadsheet or word processor. If the attachment has a filename extension associated with it, you may be able to tell what kind of file it is. A .exe extension, for example, indicates it is a Windows or DOS program (executable), while a file ending in .doc is probably meant to be opened in Microsoft Word. + +No matter what kind of file it is, an application/octet-stream attachment **is rarely viewable** in web client. After downloading an attachment users must then open the attachment in the appropriate application to view its contents. + +The `Context.Binary(data)` is the method which sends binary responses to the client. It accepts `[]byte` value. + +```go +func handler(ctx iris.Context) { + ctx.Binary([]byte("Some binary data here.")) +} +``` + +You may also want to see the following methods for [[file serving |file-server/context-file-server]]. diff --git a/responses/content-negotiation.md b/responses/content-negotiation.md new file mode 100644 index 0000000..cf9e063 --- /dev/null +++ b/responses/content-negotiation.md @@ -0,0 +1,230 @@ +**Content-Type: _*_** + +## What is it + +**Sometimes a server application needs to serve different representations of a resource at the same URI**. Of course this can be done by hand, manually checking the `Accept` request header and push the requested form of the content. However, as your app manages more resources and different kind of representations this can be very painful, as you may need to check for `Accept-Charset`, `Accept-Encoding`, put some server-side priorities , handle the errors correctly and e.t.c. + +There are some web frameworks in Go already struggle to implement a feature like this but they don't do it correctly: + +* they don't handle accept-charset at all +* they don't handle accept-encoding at all +* they don't send error status code (406 not acceptable) as RFC proposes and more... + +But, fortunately for us, **Iris always follows the best practises and the Web standards**. + +Based on: + +* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) +* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) +* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset) +* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) + +Implemented on: + +* [https://github.com/kataras/iris/pull/1316/commits/8ee0de51c593fe0483fbea38117c3c88e065f2ef\#diff-15cce7299aae8810bcab9b0bf9a2fdb1](https://github.com/kataras/iris/pull/1316/commits/8ee0de51c593fe0483fbea38117c3c88e065f2ef#diff-15cce7299aae8810bcab9b0bf9a2fdb1) + +## Introduction + +The Context method which is responsible to render specific mime types based on the client's `Accept` header is the `Negotiate` one. + +Before `Negotiate` method fired, the handler MUST declare what mime types the server supports to render with safety. We can do that using the `Negotiation` priorities builder. + +## The `Negotitation` priorities builder + +The Context method which returns the builder is the `Negotiation` one. + +```go +Negotiation() *NegotiationBuilder +``` + +It returns: + +```go +type NegotiationBuilder struct { + Accept NegotiationAcceptBuilder + // [other unexproted fields] +} +``` + +The `Accept` struct **field** can be used to customize the **client's** Accept header manually, e.g. when the client does not contain an "application/json" mime type on its `Accept` header value. + +Read more about [NegotiationAcceptBuilder](https://godoc.org/github.com/kataras/iris/context#NegotiationAcceptBuilder). + +The `NegotitationBuilder` has the necessary **methods** to help you prioritize mime types, charsets and encoding. [Read the documentation](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder). + +In short, it contains the following methods: + +```go +// Declare custom mime and optionally content to render. +MIME(mime string, content interface{}) *NegotiationBuilder +// MIME method helpers: +Text(v ...string) *NegotiationBuilder +Markdown(v ...[]byte) *NegotiationBuilder +Binary(v ...[]byte) *NegotiationBuilder +JSON(v ...interface{}) *NegotiationBuilder +Problem(v ...interface{}) *NegotiationBuilder +JSONP(v ...interface{}) *NegotiationBuilder +XML(v ...interface{}) *NegotiationBuilder +YAML(v ...interface{}) *NegotiationBuilder +Protobuf(v ...interface{}) *NegotiationBuilder +MsgPack(v ...interface{}) *NegotiationBuilder +Any(v ...interface{}) *NegotiationBuilder +``` + +```go +// Charset +Charset(charset ...string) *NegotiationBuilder +``` + +```go +// Encoding (compression) +Encoding(encoding ...string) *NegotiationBuilder +EncodingGzip() *NegotiationBuilder +``` + +```go +// Clears all the above, resets the builder +// if necessary on another handler. +Clear() *NegotiationBuilder +``` + +The `Build` is called automatically on `Negotiate` method but +it it's exported for a custom implementation of negotiation by the end-developer if ever required. + +```go +// Build calculates the client's and server's +// mime type(s), charset(s) and encoding +// and returns the final content type, charset and +// encoding that server should render to the client. +Build() (contentType, charset, encoding string, content interface{}) +``` + +### Usage + +The Negotiation is a context method which sets and returns the negotiation builder, so it can be used inside a middleware too. + +To declare what mime types server can render and match versus the client's `Accept` header you can do that: + +```go +func main() { + users := app.Party("/users") + users.Use(setAcceptTypes) + // [...] +} + +func setAcceptTypes(ctx iris.Context) { + ctx.Negotiation().JSON().XML().HTML().EncodingGzip() + ctx.Next() +} +``` + +The above will tell the server that we can accept JSON, XML and HTML mime types, optionally encoding by Gzip if the client supports. So far nothing is rendered to the client, learn about the `Negotiate` method below. + +## The `Negotiate` method + +The `Context.Negotiate` method used for serving different representations of a resource at the same URI. It returns `context.ErrContentNotSupported` when not matched mime types. + +```go +Negotiate(v interface{}) (int, error) +``` + +The `Context.Negotiate` method accepts an `interface{}` which can be any Go value or a [ContentNegotiator](https://godoc.org/github.com/kataras/iris/context#ContentNegotiator) or a [ContentSelector](https://godoc.org/github.com/kataras/iris/context#ContentSelector) such as the [iris.N](https://godoc.org/github.com/kataras/iris/context#N) structure (see below). + +* The "v" can be a single [iris.N](https://godoc.org/github.com/kataras/iris/context#N) struct value. +* The "v" can be any value completes the [context.ContentSelector](https://godoc.org/github.com/kataras/iris/context#ContentSelector) interface. +* The "v" can be any value completes the [context.ContentNegotiator](https://godoc.org/github.com/kataras/iris/context#ContentNegotiator) interface. +* The "v" can be any value of struct (JSON, JSONP, XML, YAML, MsgPack, Protobuf) or + + `string(TEXT, HTML)` or `[]byte(Markdown, Binary)` or `[]byte` with any matched mime type. + +* If the "v" is nil, the `Context.Negotitation()` builder's content will be used instead, otherwise "v" overrides builder's content (server mime types are still retrieved by its registered, supported, mime list) +* Set mime type priorities by [Negotiation().MIME.Text.JSON.XML.HTML...]https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.JSON). +* Set charset priorities by [Negotiation().Charset(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.Charset). +* Set encoding algorithm priorities by [Negotiation().Encoding(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.Encoding). +* Modify the accepted by [Negotiation().Accept.Override().XML().JSON().Charset(...).Encoding(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationAcceptBuilder.Override). + +The `iris.N` is a struct which can be passed on the `Context.Negotiate` method. +It contains fields which should be filled based on the `Context.Negotiation()` +server side values. If no matched mime then its "Other" field will be sent, +which should be a string or []byte. +It completes the `ContentSelector` interface. + +```go +type N struct { + Text, HTML string + Markdown []byte + Binary []byte + + JSON interface{} + Problem Problem + JSONP interface{} + XML interface{} + YAML interface{} + Protobuf interface{} + MsgPack interface{} + + Other []byte // custom content types. +} +``` + +If the given `interface{}` value is not a type which implements one the above then the `Negotiate` method will render that based on the request's `Accept` header value matching the declared priorities. + +Note that if the given `v interface{}` is nil then it will uses the contents declared by the `Negotiation` builder itself. + +### Usage + +```go +func handler(ctx iris.Context) { + // data := [...] + ctx.Negotiation(). + JSON(data). + XML(data). + HTML("

Test Name

Age 26

"). + EncodingGzip(). + Charset("utf-8") + + err := ctx.Negotiate(nil) + // [handle err] +} +``` + +## Example + +When the client accepts **JSON** and **XML** and **HTML** responses from a specific server's endpoint and the server can render all of them: + +```go +type testdata struct { + ID uint64 `json:"id" xml:"ID"` + Name string `json:"name" xml:"Name"` + Age int `json:"age" xml:"Age"` +} + +func main() { + users := app.Party("/users") + users.Use(setAcceptTypes) + + users.Post("/{id:uint64}", handler) + // [...] +} + +func setAcceptTypes(ctx iris.Context) { + ctx.Negotiation().JSON().XML().HTML().EncodingGzip() + ctx.Next() +} + +func handler(ctx iris.Context) { + data := testdata{ + ID: ctx.Params().GetUint64Default("id", 0), + Name: "Test Name", + Age: 26, + } + + ctx.Negotiate(iris.N{ + JSON: data, + XML: data, + HTML: "

Test Name

Age 26

", + }) +} +``` + +That's all, read the examples for a comprehensive understanding in practise. diff --git a/responses/gzip.md b/responses/gzip.md new file mode 100644 index 0000000..870d7bc --- /dev/null +++ b/responses/gzip.md @@ -0,0 +1,43 @@ +**Content-Type: _*_** + +**Gzip** is a method of compressing files (making them smaller) for faster network transfers. It is also a file format. Compression allows your web server to provide smaller file sizes which load faster for your website users. Enabling **gzip compression** is a standard practice. https://en.wikipedia.org/wiki/Gzip + +To enable **compression** on **all** server's **responses** and **requests** use the `iris.Compression` middleware: + +```go +func main() { + app := iris.New() + app.Use(iris.Compression) + + app.Post("/data", handler) +} + +func handler(ctx iris.Context) { + response := iris.Map{"name", "kataras"} + ctx.JSON(response) +} +``` + +> If the client does not accept an encoding then it will write the contents as they are, without compression. + +When you want control over it you can use the `Context.CompressWriter(enable bool)` method before sent the response: + +```go +func handler(ctx iris.Context) { + ctx.CompressWriter(true) + + // [...] +} +``` + +There is also the `Context.ClientSupportsEncoding(s ...string) bool` method which reports if the client does accept and support gzip encoding: + +```go +func handler(ctx iris.Context) { + if ctx.ClientSupportsEncoding("gzip") { + // client expects and supports gzip replies. + } else { + // client does not support gzip. + } +} +``` diff --git a/responses/html.md b/responses/html.md new file mode 100644 index 0000000..6e7bb52 --- /dev/null +++ b/responses/html.md @@ -0,0 +1,18 @@ +**Content-Type: "text/html"** + +The `Context.HTML(format, ...args)` is the method which sends HTML responses to the client. It accepts the value (like fmt package works). + +```go +func handler(ctx iris.Context) { + response := "Hello, %s!" + ctx.HTML(response, "World") +} +``` + +**Result** + +```html +Hello, World! +``` + +> If you want to learn how to render template files or embedded views please head over to the [View](../view) chapter instead. diff --git a/responses/http2_push.md b/responses/http2_push.md new file mode 100644 index 0000000..2cefc15 --- /dev/null +++ b/responses/http2_push.md @@ -0,0 +1,88 @@ +**Content-Type: _*_** + +Server push lets the server preemptively "push" website assets +to the client without the user having explicitly asked for them. +When used with care, we can send what we know the user is going +to need for the page they’re requesting. + +The **target** must either be an absolute path (like "/path") or an absolute +URL that contains a valid host and the same scheme as the parent request. +If the target is a path, it will inherit the scheme and host of the +parent request. + +```go +Context.ResponseWriter().Push(target string, opts *http.PushOptions) error +``` + +> The `Push` method **returns** `iris.ErrPushNotSupported` if the client has disabled push or if push is not supported on the underlying connection. + +## Example + +The `Push` feature works only on **HTTP/2** servers. + +Create the project structure, e.g. + +```text +β”‚ main.go +└───public +β”‚ main.js +``` + +The `main.js` contains a simple alert function: + +```js +window.alert("javascript loaded"); +``` + +Execute the following command to generate sample server keys: + +```sh +$ openssl req -new -newkey rsa:4096 -x509 -sha256 \ +-days 365 -nodes -out mycert.crt -keyout mykey.key +``` + +Create the `main.go` file and copy-paste the code below: + +```go +package main + +import "github.com/kataras/iris/v12" + +func main() { + app := iris.New() + app.Get("/", pushHandler) + app.Get("/main.js", simpleAssetHandler) + + app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key")) +} + +func pushHandler(ctx iris.Context) { + target := "/main.js" + err := ctx.ResponseWriter().Push(target, nil) + if err != nil { + if err == iris.ErrPushNotSupported { + ctx.StopWithText(iris.StatusHTTPVersionNotSupported, + "HTTP/2 push not supported.") + } else { + ctx.StopWithError(iris.StatusInternalServerError, err) + } + return + } + + ctx.HTML(``, target) +} + +func simpleAssetHandler(ctx iris.Context) { + ctx.ServeFile("./public/main.js") +} +``` + +Run the server: + +```sh +$ go run main.go +``` + +Now, open your browser's developer tools and click the `Network` tab. Navigate to https://127.0.0.1/, the `main.js` _Initiator_ should be `Push / (index)` as shown below: + +![](../.assets/http2push.png) diff --git a/responses/json.md b/responses/json.md new file mode 100644 index 0000000..b2c0ff1 --- /dev/null +++ b/responses/json.md @@ -0,0 +1,69 @@ +**Content-Type: "application/json"** + +The `Context.JSON(v, ...opts)` is the method which sends JSON responses to the client. It accepts the value and optional settings for rendering. The `JSON` options structure looks like this: + +```go +// JSON contains the options for the JSON (Context's) Renderer. +type JSON struct { + // http-specific + StreamingJSON bool + // content-specific + UnescapeHTML bool + Indent string + Prefix string + ASCII bool + // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. + Secure bool + // proto.Message specific marshal options. + Proto ProtoMarshalOptions +} +``` + +> If `Indent` field is empty and the application runs without optimizations, the `Indent` field will be automatically set to `2 spaces`. + +So, if we want to write a JSON with indentation of four spaces and prefixed with `while(1)` we can do something like that: + +```go +func handler(ctx iris.Context) { + response := map[string]interface{}{"message": "ok"} + options := iris.JSON{Indent: " ", Secure:true} + ctx.JSON(response, options) +} +``` + +**Result** + +```json +{ + "message": "ok" +} +``` + +As with all RESTful rich responses, any marshallable (JSON at this case) Go type can be given. If we want to render a Go struct as JSON, the struct's fields we want to render should be **[exported](https://tour.golang.org/basics/3)**, and optionally tagged with the `json` struct tag. Look the exaple below: + +```go +type User struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + IgnoredField int `json:"-"` +} + +func handler(ctx iris.Context) { + response := User{ + Firstname: "makis", + Lastname: "maropoulos", + IgnoredField:42, + } + + ctx.JSON(response) +} +``` + +**Result** + +```json +{ + "firstname": "makis", + "lastname": "maropoulos" +} +``` diff --git a/responses/jsonp.md b/responses/jsonp.md new file mode 100644 index 0000000..415e127 --- /dev/null +++ b/responses/jsonp.md @@ -0,0 +1,82 @@ +**Content-Type: "text/javascript"** + +## What is JSONP, and why was it created? + +Say you're on domain `example.com`, and you want to make a request to domain `example.net`. To do so, you need to cross domain boundaries, a no-no in most of browserland. + +The one item that bypasses this limitation is `