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

new simple _examples/README.md, wiki should live only inside kataras/iris/wiki and the provided e-book

Former-commit-id: 350eafb0f70f8433e394e103ff93fa332ee00a05
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-05-05 16:03:19 +03:00
parent f5e59c10e1
commit c10dd32ad7
28 changed files with 416 additions and 1166 deletions

View File

@@ -0,0 +1,18 @@
// file: datamodels/movie.go
package datamodels
// Movie is our sample data structure.
// Keep note that the tags for public-use (for our web app)
// should be kept in other file like "web/viewmodels/movie.go"
// which could wrap by embedding the datamodels.Movie or
// declare new fields instead butwe will use this datamodel
// as the only one Movie model in our application,
// for the shake of simplicty.
type Movie struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Year int `json:"year"`
Genre string `json:"genre"`
Poster string `json:"poster"`
}

View File

@@ -0,0 +1,44 @@
// file: datasource/movies.go
package datasource
import "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels"
// Movies is our imaginary data source.
var Movies = map[uint64]datamodels.Movie{
1: {
ID: 1,
Name: "Casablanca",
Year: 1942,
Genre: "Romance",
Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg",
},
2: {
ID: 2,
Name: "Gone with the Wind",
Year: 1939,
Genre: "Romance",
Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg",
},
3: {
ID: 3,
Name: "Citizen Kane",
Year: 1941,
Genre: "Mystery",
Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg",
},
4: {
ID: 4,
Name: "The Wizard of Oz",
Year: 1939,
Genre: "Fantasy",
Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg",
},
5: {
ID: 5,
Name: "North by Northwest",
Year: 1959,
Genre: "Thriller",
Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg",
},
}

View File

@@ -0,0 +1,55 @@
// file: main.go
package main
import (
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/datasource"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/repositories"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/services"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/web/middleware"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/web/routes"
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Logger().SetLevel("debug")
// Load the template files.
app.RegisterView(iris.HTML("./web/views", ".html"))
// Create our movie repository with some (memory) data from the datasource.
repo := repositories.NewMovieRepository(datasource.Movies)
app.Party("/hello").ConfigureContainer(func(r *iris.APIContainer) {
r.Get("/", routes.Hello)
r.Get("/{name}", routes.HelloName)
})
app.Party("/movies").ConfigureContainer(func(r *iris.APIContainer) {
// Create our movie service, we will bind it to the movie app's dependencies.
movieService := services.NewMovieService(repo)
r.RegisterDependency(movieService)
// Add the basic authentication(admin:password) middleware
// for the /movies based requests.
r.Use(middleware.BasicAuth)
r.Get("/", routes.Movies)
r.Get("/{id:uint64}", routes.MovieByID)
r.Put("/{id:uint64}", routes.UpdateMovieByID)
r.Delete("/{id:uint64}", routes.DeleteMovieByID)
})
// http://localhost:8080/hello
// http://localhost:8080/hello/iris
// http://localhost:8080/movies ("admin": "password")
// http://localhost:8080/movies/1
app.Listen(
// Start the web server at localhost:8080
"localhost:8080",
// enables faster json serialization and more:
iris.WithOptimizations,
)
}

View File

@@ -0,0 +1,176 @@
// file: repositories/movie_repository.go
package repositories
import (
"errors"
"sync"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels"
)
// Query represents the visitor and action queries.
type Query func(datamodels.Movie) bool
// MovieRepository handles the basic operations of a movie entity/model.
// It's an interface in order to be testable, i.e a memory movie repository or
// a connected to an sql database.
type MovieRepository interface {
Exec(query Query, action Query, limit int, mode int) (ok bool)
Select(query Query) (movie datamodels.Movie, found bool)
SelectMany(query Query, limit int) (results []datamodels.Movie)
InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error)
Delete(query Query, limit int) (deleted bool)
}
// NewMovieRepository returns a new movie memory-based repository,
// the one and only repository type in our example.
func NewMovieRepository(source map[uint64]datamodels.Movie) MovieRepository {
return &movieMemoryRepository{source: source}
}
// movieMemoryRepository is a "MovieRepository"
// which manages the movies using the memory data source (map).
type movieMemoryRepository struct {
source map[uint64]datamodels.Movie
mu sync.RWMutex
}
const (
// ReadOnlyMode will RLock(read) the data .
ReadOnlyMode = iota
// ReadWriteMode will Lock(read/write) the data.
ReadWriteMode
)
func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) {
loops := 0
if mode == ReadOnlyMode {
r.mu.RLock()
defer r.mu.RUnlock()
} else {
r.mu.Lock()
defer r.mu.Unlock()
}
for _, movie := range r.source {
ok = query(movie)
if ok {
if action(movie) {
loops++
if actionLimit >= loops {
break // break
}
}
}
}
return
}
// Select receives a query function
// which is fired for every single movie model inside
// our imaginary data source.
// When that function returns true then it stops the iteration.
//
// It returns the query's return last known "found" value
// and the last known movie model
// to help callers to reduce the LOC.
//
// It's actually a simple but very clever prototype function
// I'm using everywhere since I firstly think of it,
// hope you'll find it very useful as well.
func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) {
found = r.Exec(query, func(m datamodels.Movie) bool {
movie = m
return true
}, 1, ReadOnlyMode)
// set an empty datamodels.Movie if not found at all.
if !found {
movie = datamodels.Movie{}
}
return
}
// SelectMany same as Select but returns one or more datamodels.Movie as a slice.
// If limit <=0 then it returns everything.
func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) {
r.Exec(query, func(m datamodels.Movie) bool {
results = append(results, m)
return true
}, limit, ReadOnlyMode)
return
}
// InsertOrUpdate adds or updates a movie to the (memory) storage.
//
// Returns the new movie and an error if any.
func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) {
id := movie.ID
if id == 0 { // Create new action
var lastID uint64
// find the biggest ID in order to not have duplications
// in productions apps you can use a third-party
// library to generate a UUID as string.
r.mu.RLock()
for _, item := range r.source {
if item.ID > lastID {
lastID = item.ID
}
}
r.mu.RUnlock()
id = lastID + 1
movie.ID = id
// map-specific thing
r.mu.Lock()
r.source[id] = movie
r.mu.Unlock()
return movie, nil
}
// Update action based on the movie.ID,
// here we will allow updating the poster and genre if not empty.
// Alternatively we could do pure replace instead:
// r.source[id] = movie
// and comment the code below;
current, exists := r.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
if !exists { // ID is not a real one, return an error.
return datamodels.Movie{}, errors.New("failed to update a nonexistent movie")
}
// or comment these and r.source[id] = m for pure replace
if movie.Poster != "" {
current.Poster = movie.Poster
}
if movie.Genre != "" {
current.Genre = movie.Genre
}
// map-specific thing
r.mu.Lock()
r.source[id] = current
r.mu.Unlock()
return movie, nil
}
func (r *movieMemoryRepository) Delete(query Query, limit int) bool {
return r.Exec(query, func(m datamodels.Movie) bool {
delete(r.source, m.ID)
return true
}, limit, ReadWriteMode)
}

View File

@@ -0,0 +1,65 @@
// file: services/movie_service.go
package services
import (
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/repositories"
)
// MovieService handles some of the CRUID operations of the movie datamodel.
// It depends on a movie repository for its actions.
// It's here to decouple the data source from the higher level compoments.
// As a result a different repository type can be used with the same logic without any aditional changes.
// It's an interface and it's used as interface everywhere
// because we may need to change or try an experimental different domain logic at the future.
type MovieService interface {
GetAll() []datamodels.Movie
GetByID(id uint64) (datamodels.Movie, bool)
DeleteByID(id uint64) bool
UpdatePosterAndGenreByID(id uint64, poster string, genre string) (datamodels.Movie, error)
}
// NewMovieService returns the default movie service.
func NewMovieService(repo repositories.MovieRepository) MovieService {
return &movieService{
repo: repo,
}
}
type movieService struct {
repo repositories.MovieRepository
}
// GetAll returns all movies.
func (s *movieService) GetAll() []datamodels.Movie {
return s.repo.SelectMany(func(_ datamodels.Movie) bool {
return true
}, -1)
}
// GetByID returns a movie based on its id.
func (s *movieService) GetByID(id uint64) (datamodels.Movie, bool) {
return s.repo.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
}
// UpdatePosterAndGenreByID updates a movie's poster and genre.
func (s *movieService) UpdatePosterAndGenreByID(id uint64, poster string, genre string) (datamodels.Movie, error) {
// update the movie and return it.
return s.repo.InsertOrUpdate(datamodels.Movie{
ID: id,
Poster: poster,
Genre: genre,
})
}
// DeleteByID deletes a movie by its id.
//
// Returns true if deleted otherwise false.
func (s *movieService) DeleteByID(id uint64) bool {
return s.repo.Delete(func(m datamodels.Movie) bool {
return m.ID == id
}, 1)
}

View File

@@ -0,0 +1,12 @@
// file: web/middleware/basicauth.go
package middleware
import "github.com/kataras/iris/v12/middleware/basicauth"
// BasicAuth middleware sample.
var BasicAuth = basicauth.New(basicauth.Config{
Users: map[string]string{
"admin": "password",
},
})

View File

@@ -0,0 +1,50 @@
// file: web/routes/hello.go
package routes
import (
"errors"
"github.com/kataras/iris/v12/hero"
)
var helloView = hero.View{
Name: "hello/index.html",
Data: map[string]interface{}{
"Title": "Hello Page",
"MyMessage": "Welcome to my awesome website",
},
}
// Hello will return a predefined view with bind data.
//
// `hero.Result` is just an interface with a `Dispatch` function.
// `hero.Response` and `hero.View` are the builtin result type dispatchers
// you can even create custom response dispatchers by
// implementing the `github.com/kataras/iris/hero#Result` interface.
func Hello() hero.Result {
return helloView
}
// you can define a standard error in order to re-use anywhere in your app.
var errBadName = errors.New("bad name")
// you can just return it as error or even better
// wrap this error with an hero.Response to make it an hero.Result compatible type.
var badName = hero.Response{Err: errBadName, Code: 400}
// HelloName returns a "Hello {name}" response.
// Demos:
// curl -i http://localhost:8080/hello/iris
// curl -i http://localhost:8080/hello/anything
func HelloName(name string) hero.Result {
if name != "iris" {
return badName
}
// return hero.Response{Text: "Hello " + name} OR:
return hero.View{
Name: "hello/name.html",
Data: name,
}
}

View File

@@ -0,0 +1,59 @@
// file: web/routes/movie.go
package routes
import (
"errors"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels"
"github.com/kataras/iris/v12/_examples/dependency-injection/overview/services"
"github.com/kataras/iris/v12"
)
// Movies returns list of the movies.
// Demo:
// curl -i http://localhost:8080/movies
func Movies(service services.MovieService) (results []datamodels.Movie) {
return service.GetAll()
}
// MovieByID returns a movie.
// Demo:
// curl -i http://localhost:8080/movies/1
func MovieByID(service services.MovieService, id uint64) (movie datamodels.Movie, found bool) {
return service.GetByID(id) // it will throw 404 if not found.
}
// UpdateMovieByID updates a movie.
// Demo:
// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1
func UpdateMovieByID(ctx iris.Context, service services.MovieService, id uint64) (datamodels.Movie, error) {
// get the request data for poster and genre
file, info, err := ctx.FormFile("poster")
if err != nil {
return datamodels.Movie{}, errors.New("failed due form file 'poster' missing")
}
// we don't need the file so close it now.
file.Close()
// imagine that is the url of the uploaded file...
poster := info.Filename
genre := ctx.FormValue("genre")
return service.UpdatePosterAndGenreByID(id, poster, genre)
}
// DeleteMovieByID deletes a movie.
// Demo:
// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1
func DeleteMovieByID(service services.MovieService, id uint64) interface{} {
wasDel := service.DeleteByID(id)
if wasDel {
// return the deleted movie's ID
return iris.Map{"deleted": id}
}
// right here we can see that a method function can return any of those two types(map or int),
// we don't have to specify the return type to a specific type.
return iris.StatusBadRequest
}

View File

@@ -0,0 +1,12 @@
<!-- file: web/views/hello/index.html -->
<html>
<head>
<title>{{.Title}} - My App</title>
</head>
<body>
<p>{{.MyMessage}}</p>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!-- file: web/views/hello/name.html -->
<html>
<head>
<title>{{.}}' Portfolio - My App</title>
</head>
<body>
<h1>Hello {{.}}</h1>
</body>
</html>

View File

@@ -0,0 +1,34 @@
package main
import (
"time"
"github.com/kataras/iris/v12/_examples/dependency-injection/sessions/routes"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/sessions"
)
func main() {
app := iris.New()
sessionManager := sessions.New(sessions.Config{
Cookie: "site_session_id",
Expires: 60 * time.Minute,
AllowReclaim: true,
})
// Session is automatically binded through `sessions.Get(ctx)`
// if a *sessions.Session input argument is present on the handler's function,
// which `routes.Index` does.
app.Use(sessionManager.Handler())
// Method: GET
// Path: http://localhost:8080
app.ConfigureContainer(registerRoutes)
app.Listen(":8080")
}
func registerRoutes(api *iris.APIContainer) {
api.Get("/", routes.Index)
}

View File

@@ -0,0 +1,17 @@
package routes
import (
"fmt"
"github.com/kataras/iris/v12/sessions"
)
// Index will increment a simple int version based on the visits that this user/session did.
func Index(session *sessions.Session) 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 := session.Increment("visits", 1)
// write the current, updated visits.
return fmt.Sprintf("%d visit(s) from my current session", visits)
}

View File

@@ -0,0 +1,156 @@
package main
import (
"fmt"
"strings"
"github.com/kataras/iris/v12"
// External package to optionally filter JSON responses before sent,
// see `sendJSON` for more.
"github.com/jmespath/go-jmespath"
)
/*
$ go get github.com/jmespath/go-jmespath
*/
func main() {
app := newApp()
app.Logger().SetLevel("debug")
// http://localhost:8080/users?query=[?Name == 'John Doe'].Age
// <- client will receive the age of a user which his name is "John Doe".
// You can also test query=[0].Name to retrieve the first user's name.
// Or even query=[0:3].Age to print the first three ages.
// Learn more about jmespath and how to filter:
// http://jmespath.readthedocs.io/en/latest/ and
// https://github.com/jmespath/go-jmespath/tree/master/fuzz/testdata
//
// http://localhost:8080/users
// http://localhost:8080/users/William%20Woe
// http://localhost:8080/users/William%20Woe/age
app.Listen(":8080")
}
func newApp() *iris.Application {
app := iris.New()
// PartyFunc is the same as usersRouter := app.Party("/users")
// but it gives us an easy way to call router's registration functions,
// i.e functions from another package that can handle this group of routes.
app.PartyFunc("/users", registerUsersRoutes)
return app
}
/*
START OF USERS ROUTER
*/
func registerUsersRoutes(usersRouter iris.Party) {
// GET: /users
usersRouter.Get("/", getAllUsersHandler)
usersRouter.Party("/{name}").ConfigureContainer(registerUserRoutes)
}
type user struct {
Name string `json:"name"`
Age int `json:"age"`
}
var usersSample = []*user{
{"William Woe", 25},
{"Mary Moe", 15},
{"John Doe", 17},
}
func getAllUsersHandler(ctx iris.Context) {
err := sendJSON(ctx, usersSample)
if err != nil {
fail(ctx, iris.StatusInternalServerError, "unable to send a list of all users: %v", err)
return
}
}
/*
START OF USERS.USER SUB ROUTER
*/
func registerUserRoutes(userRouter *iris.APIContainer) {
userRouter.RegisterDependency(userDependency)
// GET: /users/{name:string}
userRouter.Get("/", getUserHandler)
// GET: /users/{name:string}/age
userRouter.Get("/age", getUserAgeHandler)
}
var userDependency = func(ctx iris.Context) *user {
name := strings.Title(ctx.Params().Get("name"))
for _, u := range usersSample {
if u.Name == name {
return u
}
}
// you may want or no to handle the error here, either way the main route handler
// is going to be executed, always. A dynamic dependency(per-request) is not a middleware, so things like `ctx.Next()` or `ctx.StopExecution()`
// do not apply here, look the `getUserHandler`'s first lines; we stop/exit the handler manually
// if the received user is nil but depending on your app's needs, it is possible to do other things too.
// A dynamic dependency like this can return more output values, i.e (*user, bool).
fail(ctx, iris.StatusNotFound, "user with name '%s' not found", name)
return nil
}
func getUserHandler(ctx iris.Context, u *user) {
sendJSON(ctx, u)
}
func getUserAgeHandler(u *user) string {
return fmt.Sprintf("%d", u.Age)
}
/* END OF USERS.USER SUB ROUTER */
/* END OF USERS ROUTER */
// common JSON response for manual HTTP errors, optionally.
type httpError struct {
Code int `json:"code"`
Reason string `json:"reason"`
}
func (h httpError) Error() string {
return fmt.Sprintf("Status Code: %d\nReason: %s", h.Code, h.Reason)
}
func fail(ctx iris.Context, statusCode int, format string, a ...interface{}) {
err := httpError{
Code: statusCode,
Reason: fmt.Sprintf(format, a...),
}
// log all the >= 500 internal errors.
if statusCode >= 500 {
ctx.Application().Logger().Error(err)
}
// no next handlers will run.
ctx.StopWithJSON(statusCode, err)
}
// JSON helper to give end-user the ability to put indention chars or filtering the response, you can do that, optionally.
// If you'd like to see that function inside the Iris' Context itself raise a [Feature Request] issue and link this example.
func sendJSON(ctx iris.Context, resp interface{}) (err error) {
indent := ctx.URLParamDefault("indent", " ")
// i.e [?Name == 'John Doe'].Age # to output the [age] of a user which his name is "John Doe".
if query := ctx.URLParam("query"); query != "" && query != "[]" {
resp, err = jmespath.Search(query, resp)
if err != nil {
return
}
}
_, err = ctx.JSON(resp, iris.JSON{Indent: indent, UnescapeHTML: true})
return err
}