From 99222654548465e8b0f2bb873cb41a576611f971 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 22 Jun 2020 17:49:45 +0300 Subject: [PATCH] add a README tutorial on the mvc/overview example Former-commit-id: c563c4f1ffa98705829e14b189a6976c3a6aa898 --- _examples/mvc/overview/Dockerfile | 17 + _examples/mvc/overview/README.md | 377 ++++++++++++++++++ _examples/mvc/overview/docker-compose.yml | 16 + .../mvc/overview/environment/environment.go | 44 +- _examples/mvc/overview/go.mod | 2 + _examples/mvc/overview/main.go | 18 +- .../mvc/overview/service/greet_service.go | 3 +- 7 files changed, 469 insertions(+), 8 deletions(-) create mode 100644 _examples/mvc/overview/Dockerfile create mode 100644 _examples/mvc/overview/README.md create mode 100644 _examples/mvc/overview/docker-compose.yml diff --git a/_examples/mvc/overview/Dockerfile b/_examples/mvc/overview/Dockerfile new file mode 100644 index 00000000..16c9113d --- /dev/null +++ b/_examples/mvc/overview/Dockerfile @@ -0,0 +1,17 @@ +# docker build -t app . +# docker run --rm -it -p 8080:8080 app:latest +FROM golang:latest AS builder +RUN apt-get update +ENV GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 +WORKDIR /go/src/app +COPY go.mod . +RUN go mod download +COPY . . +RUN go install + +FROM scratch +COPY --from=builder /go/bin/app . +ENTRYPOINT ["./app"] \ No newline at end of file diff --git a/_examples/mvc/overview/README.md b/_examples/mvc/overview/README.md new file mode 100644 index 00000000..7c6cf79d --- /dev/null +++ b/_examples/mvc/overview/README.md @@ -0,0 +1,377 @@ +# 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: + +``` +│ 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: + +```sh +$ 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 + +## 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 expecting 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). + +## 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: + +```sh +go run main.go +``` + +### Docker + +Install [Docker](https://www.docker.com/) and execute the following command: + +```sh +$ docker-compose up +``` + +Visit http://localhost:8080?name=kataras. + +Now, 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/_examples/mvc/overview/docker-compose.yml b/_examples/mvc/overview/docker-compose.yml new file mode 100644 index 00000000..db906506 --- /dev/null +++ b/_examples/mvc/overview/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.1' + +services: + app: + build: . + ports: + - 8080:8080 + environment: + PORT: 8080 + ENVIRONMENT: development + restart: on-failure + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/ping"] + interval: 30s + timeout: 10s + retries: 5 diff --git a/_examples/mvc/overview/environment/environment.go b/_examples/mvc/overview/environment/environment.go index dd540525..519a7cc9 100644 --- a/_examples/mvc/overview/environment/environment.go +++ b/_examples/mvc/overview/environment/environment.go @@ -1,10 +1,50 @@ package environment -// Env is the environment type. -type Env string +import ( + "os" + "strings" +) // Available environments example. const ( PROD Env = "production" DEV Env = "development" ) + +// Env is the environment type. +type Env string + +// String just returns the string representation of the Env. +func (e Env) String() string { + return string(e) +} + +// ReadEnv returns the environment of the system environment variable of "key". +// Returns the "def" if not found. +// Reports a panic message if the environment variable found +// but the Env is unknown. +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 +} + +// Getenv returns the value of a system environment variable "key". +// Defaults to "def" if not found. +func Getenv(key string, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + + return def +} diff --git a/_examples/mvc/overview/go.mod b/_examples/mvc/overview/go.mod index 8621348f..6dc27d95 100644 --- a/_examples/mvc/overview/go.mod +++ b/_examples/mvc/overview/go.mod @@ -1,3 +1,5 @@ module app go 1.14 + +require github.com/kataras/iris/v12 v12.1.9-0.20200621141528-f6355c9716ab diff --git a/_examples/mvc/overview/main.go b/_examples/mvc/overview/main.go index 7e96ad84..03230030 100644 --- a/_examples/mvc/overview/main.go +++ b/_examples/mvc/overview/main.go @@ -12,10 +12,17 @@ import ( 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")) + addr := ":" + environment.Getenv("PORT", ":8080") + app.Listen(addr, iris.WithLogLevel("debug")) +} + +func pong(ctx iris.Context) { + ctx.WriteString("pong") } /* @@ -35,9 +42,9 @@ func main() { | | | | | | | | | | | | - +------------+-----+ +-------------------v---v-----------------+ +----+------+ - | greeterWithLog | | NewGreetService(Env, DB) GreetService | | greeter | - -------------+-----+ +---------------------------+-------------+ +----+------+ + +--------------+-----+ +-------------------v---v-----------------+ +----+------+ + | greeterWithLogging | | NewGreetService(Env, DB) GreetService | | greeter | + +--------------+-----+ +---------------------------+-------------+ +----+------+ | | | | | | | +-----------------------------------------+ | @@ -66,8 +73,9 @@ func main() { func setup(app *mvc.Application) { // Register Dependencies. // Tip: A dependency can depend on other dependencies too. + env := environment.ReadEnv("ENVIRONMENT", environment.DEV) app.Register( - environment.DEV, // DEV, PROD + env, // DEV, PROD database.NewDB, // sqlite, mysql service.NewGreetService, // greeterWithLogging, greeter ) diff --git a/_examples/mvc/overview/service/greet_service.go b/_examples/mvc/overview/service/greet_service.go index 200e8a23..c0d493d8 100644 --- a/_examples/mvc/overview/service/greet_service.go +++ b/_examples/mvc/overview/service/greet_service.go @@ -1,9 +1,10 @@ package service import ( + "fmt" + "app/database" "app/environment" - "fmt" ) // GreetService example service.