1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker

Former-commit-id: ed635ee95de7160cde11eaabc0c1dcb0e460a620
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-06-07 15:26:06 +03:00
parent 9fdcb4c7fb
commit ed45c77be5
328 changed files with 4262 additions and 41621 deletions

View File

@@ -0,0 +1,2 @@
PORT=8080
DSN=mongodb://localhost:27017

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,17 @@
# docker build -t myapp .
# docker run --rm -it -p 8080:8080 myapp: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/myapp .
ENTRYPOINT ["./myapp"]

View File

@@ -0,0 +1,64 @@
# Build RESTful API with the official MongoDB Go Driver and Iris
Article is coming soon, follow and stay tuned
- <https://medium.com/@kataras>
- <https://dev.to/kataras>
Read [the fully functional example](main.go).
## Run
### Docker
Install [Docker](https://www.docker.com/) and execute the command below
```sh
$ docker-compose up
```
### Manually
```sh
# .env file contents
PORT=8080
DSN=mongodb://localhost:27017
```
```sh
$ go run main.go
> 2019/01/28 05:17:59 Loading environment variables from file: .env
> 2019/01/28 05:17:59 ◽ Port=8080
> 2019/01/28 05:17:59 ◽ DSN=mongodb://localhost:27017
> Now listening on: http://localhost:8080
```
```sh
GET : http://localhost:8080/api/store/movies
POST : http://localhost:8080/api/store/movies
GET : http://localhost:8080/api/store/movies/{id}
PUT : http://localhost:8080/api/store/movies/{id}
DELETE : http://localhost:8080/api/store/movies/{id}
```
## Screens
### Add a Movie
![](0_create_movie.png)
### Update a Movie
![](1_update_movie.png)
### Get all Movies
![](2_get_all_movies.png)
### Get a Movie by its ID
![](3_get_movie.png)
### Delete a Movie by its ID
![](4_delete_movie.png)

View File

@@ -0,0 +1,101 @@
package storeapi
import (
"myapp/httputil"
"myapp/store"
"github.com/kataras/iris/v12"
)
type MovieHandler struct {
service store.MovieService
}
func NewMovieHandler(service store.MovieService) *MovieHandler {
return &MovieHandler{service: service}
}
func (h *MovieHandler) GetAll(ctx iris.Context) {
movies, err := h.service.GetAll(nil)
if err != nil {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve all movies")
return
}
if movies == nil {
// will return "null" if empty, with this "trick" we return "[]" json.
movies = make([]store.Movie, 0)
}
ctx.JSON(movies)
}
func (h *MovieHandler) Get(ctx iris.Context) {
id := ctx.Params().Get("id")
m, err := h.service.GetByID(nil, id)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
} else {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve movie [%s]", id)
}
return
}
ctx.JSON(m)
}
func (h *MovieHandler) Add(ctx iris.Context) {
m := new(store.Movie)
err := ctx.ReadJSON(m)
if err != nil {
httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload")
return
}
err = h.service.Create(nil, m)
if err != nil {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to create a movie")
return
}
ctx.StatusCode(iris.StatusCreated)
ctx.JSON(m)
}
func (h *MovieHandler) Update(ctx iris.Context) {
id := ctx.Params().Get("id")
var m store.Movie
err := ctx.ReadJSON(&m)
if err != nil {
httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload")
return
}
err = h.service.Update(nil, id, m)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
return
}
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to update movie [%s]", id)
return
}
}
func (h *MovieHandler) Delete(ctx iris.Context) {
id := ctx.Params().Get("id")
err := h.service.Delete(nil, id)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
return
}
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to delete movie [%s]", id)
return
}
}

View File

@@ -0,0 +1,18 @@
version: "3.1"
services:
app:
build: .
environment:
Port: 8080
DSN: db:27017
ports:
- 8080:8080
depends_on:
- db
db:
image: mongo
environment:
MONGO_INITDB_DATABASE: store
ports:
- 27017:27017

85
_examples/database/mongodb/env/env.go vendored Normal file
View File

@@ -0,0 +1,85 @@
package env
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
)
var (
// Port is the PORT environment variable or 8080 if missing.
// Used to open the tcp listener for our web server.
Port string
// DSN is the DSN environment variable or mongodb://localhost:27017 if missing.
// Used to connect to the mongodb.
DSN string
)
func parse() {
Port = getDefault("PORT", "8080")
DSN = getDefault("DSN", "mongodb://localhost:27017")
log.Printf("• Port=%s\n", Port)
log.Printf("• DSN=%s\n", DSN)
}
// Load loads environment variables that are being used across the whole app.
// Loading from file(s), i.e .env or dev.env
//
// Example of a 'dev.env':
// PORT=8080
// DSN=mongodb://localhost:27017
//
// After `Load` the callers can get an environment variable via `os.Getenv`.
func Load(envFileName string) {
if args := os.Args; len(args) > 1 && args[1] == "help" {
fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/master/_examples/database/mongodb/README.md")
os.Exit(-1)
}
// If more than one filename passed with comma separated then load from all
// of these, a env file can be a partial too.
envFiles := strings.Split(envFileName, ",")
for _, envFile := range envFiles {
if filepath.Ext(envFile) == "" {
envFile += ".env"
}
if fileExists(envFile) {
log.Printf("Loading environment variables from file: %s\n", envFile)
if err := godotenv.Load(envFile); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFile, err))
}
}
}
// envMap, _ := godotenv.Read(envFiles...)
// for k, v := range envMap {
// log.Printf("◽ %s=%s\n", k, v)
// }
parse()
}
func getDefault(key string, def string) string {
value := os.Getenv(key)
if value == "" {
os.Setenv(key, def)
value = def
}
return value
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

View File

@@ -0,0 +1,9 @@
module myapp
go 1.14
require (
github.com/joho/godotenv v1.3.0
github.com/kataras/iris/v12 v12.2.0
go.mongodb.org/mongo-driver v1.3.4
)

View File

@@ -0,0 +1,130 @@
package httputil
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/kataras/iris/v12"
)
var validStackFuncs = []func(string) bool{
func(file string) bool {
return strings.Contains(file, "/mongodb/api/")
},
}
// RuntimeCallerStack returns the app's `file:line` stacktrace
// to give more information about an error cause.
func RuntimeCallerStack() (s string) {
var pcs [10]uintptr
n := runtime.Callers(1, pcs[:])
frames := runtime.CallersFrames(pcs[:n])
for {
frame, more := frames.Next()
for _, fn := range validStackFuncs {
if fn(frame.File) {
s += fmt.Sprintf("\n\t\t\t%s:%d", frame.File, frame.Line)
}
}
if !more {
break
}
}
return s
}
// HTTPError describes an HTTP error.
type HTTPError struct {
error
Stack string `json:"-"` // the whole stacktrace.
CallerStack string `json:"-"` // the caller, file:lineNumber
When time.Time `json:"-"` // the time that the error occurred.
// ErrorCode int: maybe a collection of known error codes.
StatusCode int `json:"statusCode"`
// could be named as "reason" as well
// it's the message of the error.
Description string `json:"description"`
}
func newError(statusCode int, err error, format string, args ...interface{}) HTTPError {
if format == "" {
format = http.StatusText(statusCode)
}
desc := fmt.Sprintf(format, args...)
if err == nil {
err = errors.New(desc)
}
return HTTPError{
err,
string(debug.Stack()),
RuntimeCallerStack(),
time.Now(),
statusCode,
desc,
}
}
func (err HTTPError) writeHeaders(ctx iris.Context) {
ctx.StatusCode(err.StatusCode)
ctx.Header("X-Content-Type-Options", "nosniff")
}
// LogFailure will print out the failure to the "logger".
func LogFailure(logger io.Writer, ctx iris.Context, err HTTPError) {
timeFmt := err.When.Format("2006/01/02 15:04:05")
firstLine := fmt.Sprintf("%s %s: %s", timeFmt, http.StatusText(err.StatusCode), err.Error())
whitespace := strings.Repeat(" ", len(timeFmt)+1)
fmt.Fprintf(logger, "%s\n%sIP: %s\n%sURL: %s\n%sSource: %s\n",
firstLine, whitespace, ctx.RemoteAddr(), whitespace, ctx.FullRequestURI(), whitespace, err.CallerStack)
}
// Fail will send the status code, write the error's reason
// and return the HTTPError for further use, i.e logging, see `InternalServerError`.
func Fail(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
httpErr := newError(statusCode, err, format, args...)
httpErr.writeHeaders(ctx)
ctx.WriteString(httpErr.Description)
return httpErr
}
// FailJSON will send to the client the error data as JSON.
// Useful for APIs.
func FailJSON(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
httpErr := newError(statusCode, err, format, args...)
httpErr.writeHeaders(ctx)
ctx.JSON(httpErr)
return httpErr
}
// InternalServerError logs to the server's terminal
// and dispatches to the client the 500 Internal Server Error.
// Internal Server errors are critical, so we log them to the `os.Stderr`.
func InternalServerError(ctx iris.Context, err error, format string, args ...interface{}) {
LogFailure(os.Stderr, ctx, Fail(ctx, iris.StatusInternalServerError, err, format, args...))
}
// InternalServerErrorJSON acts exactly like `InternalServerError` but instead it sends the data as JSON.
// Useful for APIs.
func InternalServerErrorJSON(ctx iris.Context, err error, format string, args ...interface{}) {
LogFailure(os.Stderr, ctx, FailJSON(ctx, iris.StatusInternalServerError, err, format, args...))
}
// UnauthorizedJSON sends JSON format of StatusUnauthorized(401) HTTPError value.
func UnauthorizedJSON(ctx iris.Context, err error, format string, args ...interface{}) HTTPError {
return FailJSON(ctx, iris.StatusUnauthorized, err, format, args...)
}

View File

@@ -0,0 +1,83 @@
package main
// go get -u go.mongodb.org/mongo-driver
// go get -u github.com/joho/godotenv
import (
"context"
"flag"
"fmt"
"log"
"os"
// APIs
storeapi "myapp/api/store"
//
"myapp/env"
"myapp/store"
"github.com/kataras/iris/v12"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const version = "0.0.1"
func init() {
envFileName := ".env"
flagset := flag.CommandLine
flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables")
flagset.Parse(os.Args[1:])
env.Load(envFileName)
}
func main() {
clientOptions := options.Client().SetHosts([]string{env.DSN})
client, err := mongo.Connect(context.Background(), clientOptions)
if err != nil {
log.Fatal(err)
}
err = client.Ping(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(context.TODO())
db := client.Database("store")
var (
// Collections.
moviesCollection = db.Collection("movies")
// Services.
movieService = store.NewMovieService(moviesCollection)
)
app := iris.New()
app.Use(func(ctx iris.Context) {
ctx.Header("Server", "Iris MongoDB/"+version)
ctx.Next()
})
storeAPI := app.Party("/api/store")
{
movieHandler := storeapi.NewMovieHandler(movieService)
storeAPI.Get("/movies", movieHandler.GetAll)
storeAPI.Post("/movies", movieHandler.Add)
storeAPI.Get("/movies/{id}", movieHandler.Get)
storeAPI.Put("/movies/{id}", movieHandler.Update)
storeAPI.Delete("/movies/{id}", movieHandler.Delete)
}
// GET: http://localhost:8080/api/store/movies
// POST: http://localhost:8080/api/store/movies
// GET: http://localhost:8080/api/store/movies/{id}
// PUT: http://localhost:8080/api/store/movies/{id}
// DELETE: http://localhost:8080/api/store/movies/{id}
app.Listen(fmt.Sprintf(":%s", env.Port), iris.WithOptimizations)
}

View File

@@ -0,0 +1,180 @@
package store
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
// up to you:
// "go.mongodb.org/mongo-driver/mongo/options"
)
type Movie struct {
ID primitive.ObjectID `json:"_id" bson:"_id"` /* you need the bson:"_id" to be able to retrieve with ID filled */
Name string `json:"name"`
Cover string `json:"cover"`
Description string `json:"description"`
}
type MovieService interface {
GetAll(ctx context.Context) ([]Movie, error)
GetByID(ctx context.Context, id string) (Movie, error)
Create(ctx context.Context, m *Movie) error
Update(ctx context.Context, id string, m Movie) error
Delete(ctx context.Context, id string) error
}
type movieService struct {
C *mongo.Collection
}
var _ MovieService = (*movieService)(nil)
func NewMovieService(collection *mongo.Collection) MovieService {
// up to you:
// indexOpts := new(options.IndexOptions)
// indexOpts.SetName("movieIndex").
// SetUnique(true).
// SetBackground(true).
// SetSparse(true)
// collection.Indexes().CreateOne(context.Background(), mongo.IndexModel{
// Keys: []string{"_id", "name"},
// Options: indexOpts,
// })
return &movieService{C: collection}
}
func (s *movieService) GetAll(ctx context.Context) ([]Movie, error) {
// Note:
// The mongodb's go-driver's docs says that you can pass `nil` to "find all" but this gives NilDocument error,
// probably it's a bug or a documentation's mistake, you have to pass `bson.D{}` instead.
cur, err := s.C.Find(ctx, bson.D{})
if err != nil {
return nil, err
}
defer cur.Close(ctx)
var results []Movie
for cur.Next(ctx) {
if err = cur.Err(); err != nil {
return nil, err
}
// elem := bson.D{}
var elem Movie
err = cur.Decode(&elem)
if err != nil {
return nil, err
}
// results = append(results, Movie{ID: elem[0].Value.(primitive.ObjectID)})
results = append(results, elem)
}
return results, nil
}
func matchID(id string) (bson.D, error) {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
filter := bson.D{{Key: "_id", Value: objectID}}
return filter, nil
}
var ErrNotFound = errors.New("not found")
func (s *movieService) GetByID(ctx context.Context, id string) (Movie, error) {
var movie Movie
filter, err := matchID(id)
if err != nil {
return movie, err
}
err = s.C.FindOne(ctx, filter).Decode(&movie)
if err == mongo.ErrNoDocuments {
return movie, ErrNotFound
}
return movie, err
}
func (s *movieService) Create(ctx context.Context, m *Movie) error {
if m.ID.IsZero() {
m.ID = primitive.NewObjectID()
}
_, err := s.C.InsertOne(ctx, m)
if err != nil {
return err
}
// The following doesn't work if you have the `bson:"_id` on Movie.ID field,
// therefore we manually generate a new ID (look above).
// res, err := ...InsertOne
// objectID := res.InsertedID.(primitive.ObjectID)
// m.ID = objectID
return nil
}
func (s *movieService) Update(ctx context.Context, id string, m Movie) error {
filter, err := matchID(id)
if err != nil {
return err
}
// update := bson.D{
// {Key: "$set", Value: m},
// }
// ^ this will override all fields, you can do that, depending on your design. but let's check each field:
elem := bson.D{}
if m.Name != "" {
elem = append(elem, bson.E{Key: "name", Value: m.Name})
}
if m.Description != "" {
elem = append(elem, bson.E{Key: "description", Value: m.Description})
}
if m.Cover != "" {
elem = append(elem, bson.E{Key: "cover", Value: m.Cover})
}
update := bson.D{
{Key: "$set", Value: elem},
}
_, err = s.C.UpdateOne(ctx, filter, update)
if err != nil {
if err == mongo.ErrNoDocuments {
return ErrNotFound
}
return err
}
return nil
}
func (s *movieService) Delete(ctx context.Context, id string) error {
filter, err := matchID(id)
if err != nil {
return err
}
_, err = s.C.DeleteOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
return ErrNotFound
}
return err
}
return nil
}