diff --git a/HISTORY.md b/HISTORY.md index 587e1562..340eb0d1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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. +# Soon + +Coming soon, stay tuned by reading the [PR](https://github.com/kataras/iris/pull/1175) progress. + # Fr, 11 January 2019 | v11.1.1 Happy new year! This is a minor release, contains mostly bug fixes. diff --git a/README.md b/README.md index c5f97e9a..dfb02623 100644 --- a/README.md +++ b/README.md @@ -1020,7 +1020,7 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma ## 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) - 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) diff --git a/VERSION b/VERSION index 27271c12..ccea1b39 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11.1.1:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111 \ No newline at end of file +11.2.0:https://github.com/kataras/iris/blob/master/HISTORY.md#soon \ No newline at end of file diff --git a/_examples/tutorial/mongodb/.env b/_examples/tutorial/mongodb/.env new file mode 100644 index 00000000..c41f59a0 --- /dev/null +++ b/_examples/tutorial/mongodb/.env @@ -0,0 +1,2 @@ +PORT=8080 +DSN=mongodb://localhost:27017 \ No newline at end of file diff --git a/_examples/tutorial/mongodb/0_create_movie.png b/_examples/tutorial/mongodb/0_create_movie.png new file mode 100644 index 00000000..17e8a71e Binary files /dev/null and b/_examples/tutorial/mongodb/0_create_movie.png differ diff --git a/_examples/tutorial/mongodb/1_update_movie.png b/_examples/tutorial/mongodb/1_update_movie.png new file mode 100644 index 00000000..d0482aa6 Binary files /dev/null and b/_examples/tutorial/mongodb/1_update_movie.png differ diff --git a/_examples/tutorial/mongodb/2_get_all_movies.png b/_examples/tutorial/mongodb/2_get_all_movies.png new file mode 100644 index 00000000..1360c0a8 Binary files /dev/null and b/_examples/tutorial/mongodb/2_get_all_movies.png differ diff --git a/_examples/tutorial/mongodb/3_get_movie.png b/_examples/tutorial/mongodb/3_get_movie.png new file mode 100644 index 00000000..71eca41e Binary files /dev/null and b/_examples/tutorial/mongodb/3_get_movie.png differ diff --git a/_examples/tutorial/mongodb/4_delete_movie.png b/_examples/tutorial/mongodb/4_delete_movie.png new file mode 100644 index 00000000..185b730a Binary files /dev/null and b/_examples/tutorial/mongodb/4_delete_movie.png differ diff --git a/_examples/tutorial/mongodb/README.md b/_examples/tutorial/mongodb/README.md new file mode 100644 index 00000000..644a93d0 --- /dev/null +++ b/_examples/tutorial/mongodb/README.md @@ -0,0 +1,58 @@ +# Build RESTful API with the official MongoDB Go Driver and Iris + +Article is coming soon, follow and stay tuned + +- +- + +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) + diff --git a/_examples/tutorial/mongodb/api/store/movie.go b/_examples/tutorial/mongodb/api/store/movie.go new file mode 100644 index 00000000..2004d06c --- /dev/null +++ b/_examples/tutorial/mongodb/api/store/movie.go @@ -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 + } +} diff --git a/_examples/tutorial/mongodb/env/env.go b/_examples/tutorial/mongodb/env/env.go new file mode 100644 index 00000000..24ead415 --- /dev/null +++ b/_examples/tutorial/mongodb/env/env.go @@ -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 +} diff --git a/_examples/tutorial/mongodb/httputil/error.go b/_examples/tutorial/mongodb/httputil/error.go new file mode 100644 index 00000000..5bf7837b --- /dev/null +++ b/_examples/tutorial/mongodb/httputil/error.go @@ -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...) +} diff --git a/_examples/tutorial/mongodb/main.go b/_examples/tutorial/mongodb/main.go new file mode 100644 index 00000000..56702ebd --- /dev/null +++ b/_examples/tutorial/mongodb/main.go @@ -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) +} diff --git a/_examples/tutorial/mongodb/store/movie.go b/_examples/tutorial/mongodb/store/movie.go new file mode 100644 index 00000000..3b995e47 --- /dev/null +++ b/_examples/tutorial/mongodb/store/movie.go @@ -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 +} diff --git a/doc.go b/doc.go index 8f473373..84b0b61f 100644 --- a/doc.go +++ b/doc.go @@ -27,7 +27,10 @@ // 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: @@ -35,7 +38,7 @@ Source code and other details for the project are available at GitHub: Current Version -11.1.1 +11.2.0 Installation diff --git a/iris.go b/iris.go index 3cc537b7..496c5eb1 100644 --- a/iris.go +++ b/iris.go @@ -33,7 +33,7 @@ import ( var ( // 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.