add tutorial for the official mongodb go driver

Former-commit-id: 8353dd101c37c223bba404403f9f8fa2d042fede
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-01-28 05:36:44 +02:00
parent 680b5a0923
commit 4284739151
17 changed files with 635 additions and 5 deletions

View File

@ -17,6 +17,10 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you.
# Soon
Coming soon, stay tuned by reading the [PR](https://github.com/kataras/iris/pull/1175) progress.
# Fr, 11 January 2019 | v11.1.1 # Fr, 11 January 2019 | v11.1.1
Happy new year! This is a minor release, contains mostly bug fixes. Happy new year! This is a minor release, contains mostly bug fixes.

View File

@ -1020,7 +1020,7 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma
## Support ## Support
- [HISTORY](HISTORY.md#fr-11-january-2019--v1111) file is your best friend, it contains information about the latest features and changes - [HISTORY](HISTORY.md#soon) file is your best friend, it contains information about the latest features and changes
- Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues) - Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues)
- Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com) - Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com)
- Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) - Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)

View File

@ -1 +1 @@
11.1.1:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111 11.2.0:https://github.com/kataras/iris/blob/master/HISTORY.md#soon

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,58 @@
# 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).
```sh
$ go get -u github.com/mongodb/mongo-go-driver
$ go get -u github.com/joho/godotenv
```
```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 (
"github.com/kataras/iris/_examples/tutorial/mongodb/httputil"
"github.com/kataras/iris/_examples/tutorial/mongodb/store"
"github.com/kataras/iris"
)
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
}
}

71
_examples/tutorial/mongodb/env/env.go vendored Normal file
View File

@ -0,0 +1,71 @@
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")
}
// 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/tutorials/mongodb/README.md")
os.Exit(-1)
}
log.Printf("Loading environment variables from file: %s\n", envFileName)
// 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 i := range envFiles {
if filepath.Ext(envFiles[i]) == "" {
envFiles[i] += ".env"
}
}
if err := godotenv.Load(envFiles...); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFileName, 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
}

View File

@ -0,0 +1,130 @@
package httputil
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/kataras/iris"
)
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,81 @@
package main
// go get -u github.com/mongodb/mongo-go-driver
// go get -u github.com/joho/godotenv
import (
"context"
"flag"
"fmt"
"log"
"os"
// APIs
storeapi "github.com/kataras/iris/_examples/tutorial/mongodb/api/store"
//
"github.com/kataras/iris/_examples/tutorial/mongodb/env"
"github.com/kataras/iris/_examples/tutorial/mongodb/store"
"github.com/kataras/iris"
"github.com/mongodb/mongo-go-driver/mongo"
)
const version = "0.0.1"
func init() {
var envFileName = ".env"
flagset := flag.CommandLine
flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables")
flag.CommandLine.Parse(os.Args[1:])
env.Load(envFileName)
}
func main() {
client, err := mongo.Connect(context.Background(), env.DSN)
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.Run(iris.Addr(fmt.Sprintf(":%s", env.Port)), iris.WithOptimizations)
}

View File

@ -0,0 +1,180 @@
package store
import (
"context"
"errors"
"github.com/mongodb/mongo-go-driver/bson"
"github.com/mongodb/mongo-go-driver/bson/primitive"
"github.com/mongodb/mongo-go-driver/mongo"
// up to you:
// "github.com/mongodb/mongo-go-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
}

7
doc.go
View File

@ -27,7 +27,10 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/* /*
Package iris provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app. Package iris implements the highest realistic performance, easy to learn Go web framework.
Iris provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app.
Low-level handlers compatible with `net/http` and high-level fastest MVC implementation and handlers dependency injection.
Easy to learn for new gophers and advanced features for experienced, it goes as far as you dive into it!
Source code and other details for the project are available at GitHub: Source code and other details for the project are available at GitHub:
@ -35,7 +38,7 @@ Source code and other details for the project are available at GitHub:
Current Version Current Version
11.1.1 11.2.0
Installation Installation

View File

@ -33,7 +33,7 @@ import (
var ( var (
// Version is the current version number of the Iris Web Framework. // Version is the current version number of the Iris Web Framework.
Version = "11.1.1" Version = "11.2.0"
) )
// HTTP status codes as registered with IANA. // HTTP status codes as registered with IANA.