├── .gitignore
├── views
├── index.html
├── layout
│ ├── sidebar.html
│ ├── navbar.html
│ └── main.html
├── discount
│ ├── show.html
│ ├── index.html
│ ├── new.html
│ └── edit.html
└── 404.html
├── testdata
└── db_fixtures
│ └── discount.yml
├── controllers
├── home.go
├── dto.go
├── init_test.go
├── utils.go
├── discount_api.go
├── discount.go
└── discount_api_test.go
├── config.yml
├── models
├── init_test.go
├── utils.go
├── discount_test.go
└── discount.go
├── Dockerfile
├── .travis.yml
├── factory
├── factory.go
└── errors.go
├── README.md
├── static
└── css
│ └── index.css
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | echosample
2 | *.db
3 | tmp
4 | /.idea/
5 |
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 | {{define "index"}}
2 | {{template "header"}}
3 | Hello
4 | {{template "footer"}}
5 | {{end}}
--------------------------------------------------------------------------------
/views/layout/sidebar.html:
--------------------------------------------------------------------------------
1 | {{define "layout/sidebar"}}
2 |
7 | {{end}}
--------------------------------------------------------------------------------
/testdata/db_fixtures/discount.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: discount name2
3 | start_at: RAW=datetime('2017-01-02', 'localtime')
4 | end_at: RAW=datetime('2017-02-02', 'localtime')
5 | created_at: RAW=datetime('now')
6 | updated_at: RAW=datetime('now')
--------------------------------------------------------------------------------
/controllers/home.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo"
7 | )
8 |
9 | type HomeController struct {
10 | }
11 |
12 | func (c HomeController) Init(g *echo.Group) {
13 | g.GET("", c.Get)
14 | }
15 | func (HomeController) Get(c echo.Context) error {
16 | return c.Render(http.StatusOK, "index", nil)
17 | }
18 |
--------------------------------------------------------------------------------
/views/layout/navbar.html:
--------------------------------------------------------------------------------
1 | {{define "layout/navbar"}}
2 |
14 | {{end}}
--------------------------------------------------------------------------------
/config.yml:
--------------------------------------------------------------------------------
1 | database:
2 | driver: sqlite3
3 | connection: echosample.db
4 | logger:
5 | # kafka:
6 | # brokers:
7 | # - steamer-01.srvs.cloudkafka.com:9093
8 | # - steamer-02.srvs.cloudkafka.com:9093
9 | # - steamer-03.srvs.cloudkafka.com:9093
10 | # topic: labs
11 | behaviorLog:
12 | kafka:
13 | # brokers:
14 | # - steamer-01.srvs.cloudkafka.com:9093
15 | # - steamer-02.srvs.cloudkafka.com:9093
16 | # - steamer-03.srvs.cloudkafka.com:9093
17 | # topic: labs
18 | debug: true
19 | service: echosample
20 | httpport: 8080
21 |
--------------------------------------------------------------------------------
/models/init_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "runtime"
6 |
7 | "github.com/go-xorm/xorm"
8 | _ "github.com/mattn/go-sqlite3"
9 | "github.com/pangpanglabs/goutils/echomiddleware"
10 | )
11 |
12 | var ctx context.Context
13 |
14 | func init() {
15 | runtime.GOMAXPROCS(1)
16 | xormEngine, err := xorm.NewEngine("sqlite3", ":memory:")
17 | if err != nil {
18 | panic(err)
19 | }
20 | xormEngine.ShowSQL(true)
21 | xormEngine.Sync(new(Discount))
22 | ctx = context.WithValue(context.Background(), echomiddleware.ContextDBName, xormEngine.NewSession())
23 | }
24 |
--------------------------------------------------------------------------------
/views/discount/show.html:
--------------------------------------------------------------------------------
1 | {{define "discount/show"}}
2 | {{template "header"}}
3 | {{with .Discount}}
4 |
5 | - Id
- {{.Id}}
6 | - Name
- {{.Name}}
7 | - Desc
- {{.Desc}}
8 | - StartAt
- {{.StartAt.Format "2006-01-02"}}
9 | - EndAt
- {{.EndAt.Format "2006-01-02"}}
10 | - ActionType
- {{.ActionType}}
11 | - DiscountAmount
- {{.DiscountAmount}}
12 | - Enable
- {{.Enable}}
13 | - CreatedAt
- {{.CreatedAt}}
14 | - EndAt
- {{.EndAt}}
15 |
16 |
17 | Edit | Back
18 |
19 | {{end}}
20 | {{template "footer"}}
21 | {{end}}
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:latest
2 |
3 | RUN go get github.com/labstack/echo \
4 | && go get github.com/go-xorm/xorm \
5 | && go get github.com/spf13/viper \
6 | && go get github.com/asaskevich/govalidator \
7 | && go get github.com/dgrijalva/jwt-go \
8 | && go get github.com/sirupsen/logrus \
9 | && go get github.com/pangpanglabs/goutils/... \
10 | && go get github.com/go-sql-driver/mysql \
11 | && go get github.com/mattn/go-sqlite3 \
12 | && go get github.com/opentracing/opentracing-go \
13 | && go get github.com/openzipkin/zipkin-go-opentracing \
14 | && go get github.com/pangpanglabs/echoswagger
15 |
16 | ADD . $GOPATH/src/github.com/pangpanglabs/echosample
17 |
18 | RUN go test github.com/pangpanglabs/echosample/...
19 |
20 | WORKDIR $GOPATH/src/github.com/pangpanglabs/echosample
21 |
22 | CMD go run main.go
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.9.x
5 | - 1.10.x
6 | - 1.11.x
7 | - master
8 | install:
9 | - go get github.com/labstack/echo
10 | - go get github.com/go-xorm/xorm
11 | - go get github.com/spf13/viper
12 | - go get github.com/asaskevich/govalidator
13 | - go get github.com/dgrijalva/jwt-go
14 | - go get github.com/sirupsen/logrus
15 | - go get github.com/pangpanglabs/goutils/...
16 | - go get github.com/go-sql-driver/mysql
17 | - go get github.com/mattn/go-sqlite3
18 | - go get github.com/opentracing/opentracing-go
19 | - go get github.com/openzipkin/zipkin-go-opentracing
20 | - go get github.com/pangpanglabs/echoswagger
21 | script: go test -coverprofile=coverage.txt -covermode=atomic github.com/pangpanglabs/echosample/...
22 | after_success:
23 | - bash <(curl -s https://codecov.io/bash)
24 |
--------------------------------------------------------------------------------
/factory/factory.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/go-xorm/xorm"
7 | "github.com/pangpanglabs/goutils/echomiddleware"
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | func DB(ctx context.Context) xorm.Interface {
12 | v := ctx.Value(echomiddleware.ContextDBName)
13 | if v == nil {
14 | panic("DB is not exist")
15 | }
16 | if db, ok := v.(*xorm.Session); ok {
17 | return db
18 | }
19 | if db, ok := v.(*xorm.Engine); ok {
20 | return db
21 | }
22 | panic("DB is not exist")
23 | }
24 |
25 | func Logger(ctx context.Context) *logrus.Entry {
26 | v := ctx.Value(echomiddleware.ContextLoggerName)
27 | if v == nil {
28 | return logrus.WithFields(logrus.Fields{})
29 | }
30 | if logger, ok := v.(*logrus.Entry); ok {
31 | return logger
32 | }
33 | return logrus.WithFields(logrus.Fields{})
34 | }
35 |
--------------------------------------------------------------------------------
/views/404.html:
--------------------------------------------------------------------------------
1 | {{define "404"}}
2 |
3 |
4 |
5 | Offer
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Oops!
18 |
19 | 404 Not Found
20 |
21 |
22 | Home
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{end}}
--------------------------------------------------------------------------------
/models/utils.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/go-xorm/xorm"
7 | )
8 |
9 | func setSortOrder(q xorm.Interface, sortby, order []string) error {
10 | if len(sortby) != 0 {
11 | if len(sortby) == len(order) {
12 | // 1) for each sort field, there is an associated order
13 | for i, v := range sortby {
14 | if order[i] == "desc" {
15 | q.Desc(v)
16 | } else if order[i] == "asc" {
17 | q.Asc(v)
18 | } else {
19 | return errors.New("Invalid order. Must be either [asc|desc]")
20 | }
21 | }
22 | } else if len(sortby) != len(order) && len(order) == 1 {
23 | // 2) there is exactly one order, all the sorted fields will be sorted by this order
24 | for _, v := range sortby {
25 | if order[0] == "desc" {
26 | q.Desc(v)
27 | } else if order[0] == "asc" {
28 | q.Asc(v)
29 | } else {
30 | return errors.New("Invalid order. Must be either [asc|desc]")
31 | }
32 | }
33 | } else if len(sortby) != len(order) && len(order) != 1 {
34 | return errors.New("'sortby', 'order' sizes mismatch or 'order' size is not 1")
35 | }
36 | } else {
37 | if len(order) != 0 {
38 | return errors.New("unused 'order' fields")
39 | }
40 | }
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/views/layout/main.html:
--------------------------------------------------------------------------------
1 | {{define "header"}}
2 |
3 |
4 |
5 | Offer
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
20 |
21 | {{end}}
22 |
23 | {{define "footer"}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{end}}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # echosample
2 |
3 | [](https://travis-ci.org/pangpanglabs/echosample)
4 | [](https://codecov.io/gh/pangpanglabs/echosample)
5 |
6 |
7 | ## Getting Started
8 |
9 | Get source
10 | ```
11 | $ go get github.com/pangpanglabs/echosample
12 | ```
13 |
14 | Test
15 | ```
16 | $ go test github.com/pangpanglabs/echosample/...
17 | ```
18 |
19 | Run
20 | ```
21 | $ cd $GOPATH/src/github.com/pangpanglabs/echosample
22 | $ go run main.go
23 | ```
24 |
25 | Visit http://127.0.0.1:8080/
26 |
27 | ## Tips
28 |
29 | ### Live reload utility
30 |
31 | Install
32 | ```
33 | $ go get github.com/codegangsta/gin
34 | ```
35 |
36 | Run
37 | ```
38 | $ gin -a 8080 -i --all r
39 | ```
40 |
41 | Visit http://127.0.0.1:3000/
42 |
43 |
44 | ## References
45 |
46 | - web framework: [echo framework](https://echo.labstack.com/)
47 | - orm tool: [xorm](http://xorm.io/)
48 | - logger : [logrus](https://github.com/sirupsen/logrus)
49 | - configuration tool: [viper](https://github.com/spf13/viper)
50 | - validator: [govalidator](github.com/asaskevich/govalidator)
51 | - utils: https://github.com/pangpanglabs/goutils
52 |
--------------------------------------------------------------------------------
/controllers/dto.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/pangpanglabs/echosample/models"
7 | )
8 |
9 | const (
10 | DefaultMaxResultCount = 30
11 | )
12 |
13 | type SearchInput struct {
14 | Sortby []string `query:"sortby"`
15 | Order []string `query:"order"`
16 | SkipCount int `query:"skipCount"`
17 | MaxResultCount int `query:"maxResultCount"`
18 | }
19 | type DiscountInput struct {
20 | Name string `json:"name" valid:"required"`
21 | Desc string `json:"desc"`
22 | StartAt string `json:"startAt" valid:"required"`
23 | EndAt string `json:"endAt" valid:"required"`
24 | ActionType string `json:"actionType" valid:"required"`
25 | DiscountAmount float64 `json:"discountAmount" valid:"required"`
26 | Enable bool `json:"enable"`
27 | }
28 |
29 | func (d *DiscountInput) ToModel() (*models.Discount, error) {
30 | startAt, err := time.Parse("2006-01-02", d.StartAt)
31 | if err != nil {
32 | return nil, err
33 | }
34 | endAt, err := time.Parse("2006-01-02", d.EndAt)
35 | if err != nil {
36 | return nil, err
37 | }
38 | return &models.Discount{
39 | Name: d.Name,
40 | Desc: d.Desc,
41 | StartAt: startAt,
42 | EndAt: endAt,
43 | ActionType: d.ActionType,
44 | DiscountAmount: d.DiscountAmount,
45 | Enable: d.Enable,
46 | }, nil
47 | }
48 |
--------------------------------------------------------------------------------
/controllers/init_test.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "gopkg.in/testfixtures.v2"
5 | "runtime"
6 |
7 | "github.com/asaskevich/govalidator"
8 | "github.com/go-xorm/xorm"
9 | "github.com/labstack/echo"
10 | _ "github.com/mattn/go-sqlite3"
11 |
12 | "github.com/pangpanglabs/echosample/models"
13 | "github.com/pangpanglabs/goutils/echomiddleware"
14 | )
15 |
16 | var (
17 | echoApp *echo.Echo
18 | handleWithFilter func(handlerFunc echo.HandlerFunc, c echo.Context) error
19 | )
20 |
21 | func init() {
22 | runtime.GOMAXPROCS(1)
23 | xormEngine, err := xorm.NewEngine("sqlite3", ":memory:")
24 | if err != nil {
25 | panic(err)
26 | }
27 | xormEngine.Sync(new(models.Discount))
28 |
29 | fixtures, err := testfixtures.NewFolder(xormEngine.DB().DB, &testfixtures.SQLite{}, "../testdata/db_fixtures")
30 | if err != nil {
31 | panic(err)
32 | }
33 | testfixtures.SkipDatabaseNameCheck(true)
34 |
35 | if err := fixtures.Load(); err != nil {
36 | panic(err)
37 | }
38 |
39 | echoApp = echo.New()
40 | echoApp.Validator = &Validator{}
41 |
42 | logger := echomiddleware.ContextLogger()
43 | db := echomiddleware.ContextDB("test", xormEngine, echomiddleware.KafkaConfig{})
44 |
45 | handleWithFilter = func(handlerFunc echo.HandlerFunc, c echo.Context) error {
46 | return logger(db(handlerFunc))(c)
47 | }
48 | }
49 |
50 | type Validator struct{}
51 |
52 | func (v *Validator) Validate(i interface{}) error {
53 | _, err := govalidator.ValidateStruct(i)
54 | return err
55 | }
56 |
--------------------------------------------------------------------------------
/models/discount_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/pangpanglabs/goutils/test"
9 | )
10 |
11 | func TestDiscountCreate(t *testing.T) {
12 | d1 := Discount{
13 | Name: "name1",
14 | Desc: "desc1",
15 | }
16 | affected, err := d1.Create(ctx)
17 | test.Ok(t, err)
18 | test.Equals(t, affected, int64(1))
19 | test.Equals(t, d1.Id, int64(1))
20 | test.Equals(t, d1.CreatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
21 | test.Equals(t, d1.UpdatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
22 |
23 | d2 := Discount{
24 | Name: "name2",
25 | Desc: "desc2",
26 | }
27 | affected, err = d2.Create(ctx)
28 | test.Ok(t, err)
29 | test.Equals(t, affected, int64(1))
30 | test.Equals(t, d2.Id, int64(2))
31 | test.Equals(t, d1.CreatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
32 | test.Equals(t, d1.UpdatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
33 | }
34 |
35 | func TestDiscountGetAndUpdate(t *testing.T) {
36 | d, err := Discount{}.GetById(ctx, 1)
37 | test.Ok(t, err)
38 | test.Equals(t, d.Id, int64(1))
39 | test.Equals(t, d.Name, "name1")
40 | test.Equals(t, d.CreatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
41 | test.Equals(t, d.UpdatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
42 |
43 | d.Name = "name1-2"
44 | err = d.Update(ctx)
45 | test.Ok(t, err)
46 | test.Equals(t, d.Name, "name1-2")
47 |
48 | }
49 |
50 | func TestDiscountGetAll(t *testing.T) {
51 | totalCount, items, err := Discount{}.GetAll(ctx, []string{"name"}, []string{"desc"}, 0, 10)
52 | test.Ok(t, err)
53 | test.Equals(t, totalCount, int64(2))
54 | test.Equals(t, items[0].Id, int64(2))
55 | test.Equals(t, items[1].Id, int64(1))
56 | }
57 |
58 | func TestXXX(t *testing.T) {
59 | at, err := time.Parse("2006-01-02", "2017-12-31")
60 | test.Ok(t, err)
61 | test.Equals(t, at.Year(), 2017)
62 | test.Assert(t, at.Month() == 12, "Month should be equals to 12")
63 | fmt.Println(at)
64 | }
65 |
--------------------------------------------------------------------------------
/views/discount/index.html:
--------------------------------------------------------------------------------
1 | {{define "discount/index"}}
2 | {{template "header"}}
3 |
4 | New Discount
5 |
6 |
7 |
8 |
9 | | Id |
10 | Name |
11 | StartAt |
12 | EndAt |
13 | ActionType |
14 | DiscountAmount |
15 | Enable |
16 | Action |
17 |
18 |
19 |
20 | {{range .Discounts}}
21 |
22 | | {{.Id}} |
23 | {{.Name}} |
24 | {{.StartAt.Format "2006-01-02"}} |
25 | {{.EndAt.Format "2006-01-02"}} |
26 | {{.ActionType}} |
27 | {{.DiscountAmount}} |
28 | {{.Enable}} |
29 |
30 | [Show]
31 | [Edit]
32 | |
33 |
34 | {{end}}
35 |
36 |
37 |
38 | {{$totalPageSize := add 1 (divide .MaxResultCount .TotalCount)}}
39 | {{$maxResultCount := .MaxResultCount}}
40 |
57 |
58 | {{with .flash.error}} error: {{.}} {{end}}
59 | {{with .flash.warning}} warning: {{.}} {{end}}
60 | {{with .flash.notice}} notice: {{.}} {{end}}
61 | {{template "footer"}}
62 | {{end}}
--------------------------------------------------------------------------------
/static/css/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Base structure
3 | */
4 |
5 | /* Move down content because we have a fixed navbar that is 50px tall */
6 | body {
7 | padding-top: 50px;
8 | }
9 |
10 |
11 | /*
12 | * Global add-ons
13 | */
14 |
15 | .sub-header {
16 | padding-bottom: 10px;
17 | border-bottom: 1px solid #eee;
18 | }
19 |
20 | /*
21 | * Top navigation
22 | * Hide default border to remove 1px line.
23 | */
24 | .navbar-fixed-top {
25 | border: 0;
26 | }
27 |
28 | /*
29 | * Sidebar
30 | */
31 |
32 | /* Hide for mobile, show later */
33 | .sidebar {
34 | display: none;
35 | }
36 | @media (min-width: 768px) {
37 | .sidebar {
38 | position: fixed;
39 | top: 51px;
40 | bottom: 0;
41 | left: 0;
42 | z-index: 1000;
43 | display: block;
44 | padding: 20px;
45 | overflow-x: hidden;
46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
47 | background-color: #f5f5f5;
48 | border-right: 1px solid #eee;
49 | }
50 | }
51 |
52 | /* Sidebar navigation */
53 | .nav-sidebar {
54 | margin-right: -21px; /* 20px padding + 1px border */
55 | margin-bottom: 20px;
56 | margin-left: -20px;
57 | }
58 | .nav-sidebar > li > a {
59 | padding-right: 20px;
60 | padding-left: 20px;
61 | }
62 | .nav-sidebar > .active > a,
63 | .nav-sidebar > .active > a:hover,
64 | .nav-sidebar > .active > a:focus {
65 | color: #fff;
66 | background-color: #428bca;
67 | }
68 |
69 |
70 | /*
71 | * Main content
72 | */
73 |
74 | .main {
75 | padding: 20px;
76 | }
77 | @media (min-width: 768px) {
78 | .main {
79 | padding-right: 40px;
80 | padding-left: 40px;
81 | }
82 | }
83 | .main .page-header {
84 | margin-top: 0;
85 | }
86 |
87 |
88 | /*
89 | * Placeholder dashboard ideas
90 | */
91 |
92 | .placeholders {
93 | margin-bottom: 30px;
94 | text-align: center;
95 | }
96 | .placeholders h4 {
97 | margin-bottom: 0;
98 | }
99 | .placeholder {
100 | margin-bottom: 20px;
101 | }
102 | .placeholder img {
103 | display: inline-block;
104 | border-radius: 50%;
105 | }
--------------------------------------------------------------------------------
/models/discount.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/pangpanglabs/echosample/factory"
8 | )
9 |
10 | type Discount struct {
11 | Id int64 `json:"id"`
12 | Name string `json:"name"`
13 | Desc string `json:"desc"`
14 | StartAt time.Time `json:"startAt"`
15 | EndAt time.Time `json:"endAt"`
16 | ActionType string `json:"actionType"`
17 | DiscountAmount float64 `json:"discountAmount"`
18 | Enable bool `json:"enable"`
19 | CreatedAt time.Time `json:"createdAt" xorm:"created"`
20 | UpdatedAt time.Time `json:"updatedAt" xorm:"updated"`
21 | }
22 |
23 | func (d *Discount) Create(ctx context.Context) (int64, error) {
24 | affected, err := factory.DB(ctx).Insert(d)
25 | if err != nil {
26 | return 0, factory.ErrorDB.New(err)
27 | }
28 | return affected, nil
29 | }
30 |
31 | func (Discount) GetById(ctx context.Context, id int64) (*Discount, error) {
32 | var v Discount
33 | if has, err := factory.DB(ctx).ID(id).Get(&v); err != nil {
34 | return nil, factory.ErrorDB.New(err)
35 | } else if !has {
36 | return nil, nil
37 | }
38 | return &v, nil
39 | }
40 |
41 | func (Discount) GetAll(ctx context.Context, sortby, order []string, offset, limit int) (int64, []Discount, error) {
42 | q := factory.DB(ctx)
43 | if err := setSortOrder(q, sortby, order); err != nil {
44 | factory.Logger(ctx).Error(err)
45 | }
46 |
47 | var items []Discount
48 | totalCount, err := q.Limit(limit, offset).FindAndCount(&items)
49 | if err != nil {
50 | return 0, nil, factory.ErrorDB.New(err)
51 | }
52 | return totalCount, items, nil
53 | }
54 |
55 | func (d *Discount) Update(ctx context.Context) error {
56 | if origin, err := d.GetById(ctx, d.Id); err != nil {
57 | return factory.ErrorDB.New(err)
58 | } else if origin == nil {
59 | return factory.ErrorDiscountNotExists.New(err, d.Id)
60 | }
61 |
62 | if _, err := factory.DB(ctx).ID(d.Id).Update(d); err != nil {
63 | return factory.ErrorDB.New(err)
64 | }
65 | return nil
66 | }
67 |
68 | func (Discount) Delete(ctx context.Context, id int64) error {
69 | if origin, err := (Discount{}).GetById(ctx, id); err != nil {
70 | return factory.ErrorDB.New(err)
71 | } else if origin == nil {
72 | return factory.ErrorDiscountNotExists.New(err, id)
73 | }
74 |
75 | if _, err := factory.DB(ctx).ID(id).Delete(&Discount{}); err != nil {
76 | return factory.ErrorDB.New(err)
77 | }
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/views/discount/new.html:
--------------------------------------------------------------------------------
1 | {{define "discount/new"}}
2 | {{template "header"}}
3 |
55 |
56 | Back
57 |
58 | {{with .flash.error}}{{.}}
{{end}}
59 | {{with .flash.warning}}{{.}}
{{end}}
60 | {{with .flash.notice}}{{.}}
{{end}}
61 | {{template "footer"}}
62 | {{end}}
--------------------------------------------------------------------------------
/controllers/utils.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "net/url"
7 | "strings"
8 |
9 | "github.com/go-xorm/xorm"
10 | "github.com/labstack/echo"
11 | "github.com/pangpanglabs/echosample/factory"
12 | "github.com/pangpanglabs/goutils/behaviorlog"
13 | )
14 |
15 | const (
16 | FlashName = "flash"
17 | FlashSeparator = ";"
18 | )
19 |
20 | type ApiResult struct {
21 | Result interface{} `json:"result"`
22 | Success bool `json:"success"`
23 | Error factory.Error `json:"error"`
24 | }
25 |
26 | type ArrayResult struct {
27 | Items interface{} `json:"items"`
28 | TotalCount int64 `json:"totalCount"`
29 | }
30 |
31 | func renderFail(c echo.Context, err error) error {
32 | if err == nil {
33 | err = factory.ErrorSystem.New(nil)
34 | }
35 | behaviorlog.FromCtx(c.Request().Context()).WithError(err)
36 | var apiError *factory.Error
37 | if ok := errors.As(err, &apiError); ok {
38 | return c.JSON(apiError.Status(), ApiResult{
39 | Success: false,
40 | Error: *apiError,
41 | })
42 | }
43 | return err
44 | }
45 |
46 | func renderSucc(c echo.Context, status int, result interface{}) error {
47 | req := c.Request()
48 | if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" {
49 | if session, ok := factory.DB(req.Context()).(*xorm.Session); ok {
50 | err := session.Commit()
51 | if err != nil {
52 | return renderFail(c, factory.ErrorDB.New(err))
53 | }
54 | }
55 | }
56 |
57 | return c.JSON(status, ApiResult{
58 | Success: true,
59 | Result: result,
60 | })
61 | }
62 |
63 | func setFlashMessage(c echo.Context, m map[string]string) {
64 | var flashValue string
65 | for key, value := range m {
66 | flashValue += "\x00" + key + "\x23" + FlashSeparator + "\x23" + value + "\x00"
67 | }
68 |
69 | c.SetCookie(&http.Cookie{
70 | Name: FlashName,
71 | Value: url.QueryEscape(flashValue),
72 | })
73 | }
74 | func getFlashMessage(c echo.Context) map[string]string {
75 | cookie, err := c.Cookie(FlashName)
76 | if err != nil {
77 | return nil
78 | }
79 |
80 | m := map[string]string{}
81 |
82 | v, _ := url.QueryUnescape(cookie.Value)
83 | vals := strings.Split(v, "\x00")
84 | for _, v := range vals {
85 | if len(v) > 0 {
86 | kv := strings.Split(v, "\x23"+FlashSeparator+"\x23")
87 | if len(kv) == 2 {
88 | m[kv[0]] = kv[1]
89 | }
90 | }
91 | }
92 | //read one time then delete it
93 | c.SetCookie(&http.Cookie{
94 | Name: FlashName,
95 | Value: "",
96 | MaxAge: -1,
97 | Path: "/",
98 | })
99 |
100 | return m
101 | }
102 |
--------------------------------------------------------------------------------
/factory/errors.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | const WrapErrorMessage = "echosample error"
9 |
10 | type Error struct {
11 | Code int `json:"code,omitempty"`
12 | Message string `json:"message,omitempty"`
13 | Details string `json:"details,omitempty"`
14 | err error
15 | status int
16 | }
17 |
18 | func (t ErrorTemplate) New(err error, v ...interface{}) *Error {
19 | e := Error{
20 | Code: t.Code,
21 | Message: fmt.Sprintf(t.Message, v...),
22 | err: err,
23 | }
24 | if err != nil {
25 | e.Details = fmt.Sprintf("%s: %s", WrapErrorMessage, err.Error())
26 | }
27 | return &e
28 | }
29 |
30 | func (e *Error) Error() string {
31 | if e == nil {
32 | return ""
33 | }
34 | return e.Details
35 | }
36 |
37 | func (e *Error) Unwrap() error {
38 | if e == nil {
39 | return nil
40 | }
41 | return e.err
42 | }
43 |
44 | func (e *Error) Status() int {
45 | if e == nil || e.status == 0 {
46 | return http.StatusInternalServerError
47 | }
48 | return e.status
49 | }
50 |
51 | type ErrorTemplate Error
52 |
53 | var (
54 | // System Error
55 | ErrorSystem = ErrorTemplate{Code: 10001, Message: "System Error"}
56 | ErrorServiceUnavailable = ErrorTemplate{Code: 10002, Message: "Service unavailable"}
57 | ErrorRemoteService = ErrorTemplate{Code: 10003, Message: "Remote service error"}
58 | ErrorIPLimit = ErrorTemplate{Code: 10004, Message: "IP limit"}
59 | ErrorPermissionDenied = ErrorTemplate{Code: 10005, Message: "Permission denied", status: http.StatusForbidden}
60 | ErrorIllegalRequest = ErrorTemplate{Code: 10006, Message: "Illegal request", status: http.StatusBadRequest}
61 | ErrorHTTPMethod = ErrorTemplate{Code: 10007, Message: "HTTP method is not suported for this request", status: http.StatusMethodNotAllowed}
62 | ErrorParameter = ErrorTemplate{Code: 10008, Message: "Parameter error", status: http.StatusBadRequest}
63 | ErrorMissParameter = ErrorTemplate{Code: 10009, Message: "Miss required parameter", status: http.StatusBadRequest}
64 | ErrorDB = ErrorTemplate{Code: 10010, Message: "DB error, please contact the administator"}
65 | ErrorTokenInvaild = ErrorTemplate{Code: 10011, Message: "Token invaild", status: http.StatusUnauthorized}
66 | ErrorMissToken = ErrorTemplate{Code: 10012, Message: "Miss token", status: http.StatusUnauthorized}
67 | ErrorVersion = ErrorTemplate{Code: 10013, Message: "API version %s invalid"}
68 | ErrorNotFound = ErrorTemplate{Code: 10014, Message: "Resource not found", status: http.StatusNotFound}
69 | // Business Error
70 | ErrorDiscountNotExists = ErrorTemplate{Code: 20001, Message: "Discount %d does not exists"}
71 | )
72 |
--------------------------------------------------------------------------------
/views/discount/edit.html:
--------------------------------------------------------------------------------
1 | {{define "discount/edit"}}
2 | {{template "header"}}
3 |
56 |
57 | Show | Back
58 |
59 | {{with .flash.error}}{{.}}
{{end}}
60 | {{with .flash.warning}}{{.}}
{{end}}
61 | {{with .flash.notice}}{{.}}
{{end}}
62 | {{template "footer"}}
63 | {{end}}
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 | "runtime"
9 |
10 | "github.com/asaskevich/govalidator"
11 | _ "github.com/go-sql-driver/mysql"
12 | "github.com/go-xorm/xorm"
13 | "github.com/labstack/echo"
14 | "github.com/labstack/echo/middleware"
15 | _ "github.com/mattn/go-sqlite3"
16 | "github.com/pangpanglabs/echoswagger"
17 | configutil "github.com/pangpanglabs/goutils/config"
18 | "github.com/pangpanglabs/goutils/echomiddleware"
19 | "github.com/pangpanglabs/goutils/echotpl"
20 |
21 | "github.com/pangpanglabs/echosample/controllers"
22 | "github.com/pangpanglabs/echosample/models"
23 | )
24 |
25 | func main() {
26 | appEnv := flag.String("app-env", os.Getenv("APP_ENV"), "app env")
27 | flag.Parse()
28 |
29 | var c Config
30 | if err := configutil.Read(*appEnv, &c); err != nil {
31 | panic(err)
32 | }
33 | fmt.Println(c)
34 | db, err := initDB(c.Database.Driver, c.Database.Connection)
35 | if err != nil {
36 | panic(err)
37 | }
38 | defer db.Close()
39 |
40 | e := echo.New()
41 |
42 | r := echoswagger.New(e, "/doc", &echoswagger.Info{
43 | Title: "Echo Sample",
44 | Description: "This is API doc for Echo Sample",
45 | Version: "1.0",
46 | })
47 |
48 | controllers.HomeController{}.Init(e.Group("/"))
49 | controllers.DiscountController{}.Init(e.Group("/discounts"))
50 | controllers.DiscountApiController{}.Init(r.Group("Discount", "/api/discounts"))
51 |
52 | e.Static("/static", "static")
53 | e.Pre(middleware.RemoveTrailingSlash())
54 | e.Pre(echomiddleware.ContextBase())
55 | e.Use(middleware.Recover())
56 | e.Use(middleware.CORS())
57 | // e.Use(middleware.Logger())
58 | e.Use(echomiddleware.ContextLogger())
59 | e.Use(echomiddleware.ContextDB(c.Service, db, echomiddleware.KafkaConfig(c.Database.Logger.Kafka)))
60 | e.Use(echomiddleware.BehaviorLogger(c.Service, echomiddleware.KafkaConfig(c.BehaviorLog.Kafka)))
61 |
62 | e.Renderer = echotpl.New()
63 | e.Validator = &Validator{}
64 | e.Debug = c.Debug
65 |
66 | if err := e.Start(":" + c.HttpPort); err != nil {
67 | log.Println(err)
68 | }
69 |
70 | }
71 |
72 | func initDB(driver, connection string) (*xorm.Engine, error) {
73 | db, err := xorm.NewEngine(driver, connection)
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | if driver == "sqlite3" {
79 | runtime.GOMAXPROCS(1)
80 | }
81 |
82 | db.Sync(new(models.Discount))
83 | return db, nil
84 | }
85 |
86 | type Config struct {
87 | Database struct {
88 | Driver string
89 | Connection string
90 | Logger struct {
91 | Kafka echomiddleware.KafkaConfig
92 | }
93 | }
94 | BehaviorLog struct {
95 | Kafka echomiddleware.KafkaConfig
96 | }
97 | Trace struct {
98 | Zipkin echomiddleware.ZipkinConfig
99 | }
100 |
101 | Debug bool
102 | Service string
103 | HttpPort string
104 | }
105 |
106 | type Validator struct{}
107 |
108 | func (v *Validator) Validate(i interface{}) error {
109 | _, err := govalidator.ValidateStruct(i)
110 | return err
111 | }
112 |
--------------------------------------------------------------------------------
/controllers/discount_api.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 |
7 | "github.com/pangpanglabs/goutils/behaviorlog"
8 |
9 | "github.com/pangpanglabs/echosample/factory"
10 | "github.com/pangpanglabs/echosample/models"
11 |
12 | "github.com/labstack/echo"
13 | "github.com/pangpanglabs/echoswagger"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | type DiscountApiController struct {
18 | }
19 |
20 | func (c DiscountApiController) Init(g echoswagger.ApiGroup) {
21 | g.GET("", c.GetAll).AddParamQueryNested(SearchInput{})
22 | g.POST("", c.Create).AddParamBody(DiscountInput{}, "body", "", true)
23 | g.GET("/:id", c.GetOne).AddParamPath(0, "id", "")
24 | g.PUT("/:id", c.Update).AddParamPath(0, "id", "").AddParamBody(DiscountInput{}, "body", "", true)
25 | }
26 |
27 | func (DiscountApiController) GetAll(c echo.Context) error {
28 | var v SearchInput
29 | if err := c.Bind(&v); err != nil {
30 | return renderFail(c, factory.ErrorParameter.New(err))
31 | }
32 | if v.MaxResultCount == 0 {
33 | v.MaxResultCount = DefaultMaxResultCount
34 | }
35 |
36 | // behavior log
37 | behaviorlog.FromCtx(c.Request().Context()).WithBizAttr("maxResultCount", v.MaxResultCount).Log("SearchDiscount")
38 |
39 | // console log
40 | factory.Logger(c.Request().Context()).WithFields(logrus.Fields{
41 | "sortby": v.Sortby,
42 | "order": v.Order,
43 | "maxResultCount": v.MaxResultCount,
44 | "skipCount": v.SkipCount,
45 | }).Info("SearchStart")
46 |
47 | totalCount, items, err := models.Discount{}.GetAll(c.Request().Context(), v.Sortby, v.Order, v.SkipCount, v.MaxResultCount)
48 | if err != nil {
49 | return renderFail(c, err)
50 | }
51 |
52 | // behavior log
53 | behaviorlog.FromCtx(c.Request().Context()).
54 | WithCallURLInfo(http.MethodGet, "https://play.google.com/books", nil, 200).
55 | WithBizAttrs(map[string]interface{}{
56 | "totalCount": totalCount,
57 | "itemCount": len(items),
58 | }).
59 | Log("SearchComplete")
60 |
61 | return renderSucc(c, http.StatusOK, ArrayResult{
62 | TotalCount: totalCount,
63 | Items: items,
64 | })
65 | }
66 |
67 | func (DiscountApiController) Create(c echo.Context) error {
68 | var v DiscountInput
69 | if err := c.Bind(&v); err != nil {
70 | return renderFail(c, factory.ErrorParameter.New(err))
71 | }
72 | if err := c.Validate(&v); err != nil {
73 | return renderFail(c, factory.ErrorParameter.New(err))
74 | }
75 | discount, err := v.ToModel()
76 | if err != nil {
77 | return renderFail(c, factory.ErrorParameter.New(err))
78 | }
79 | if _, err := discount.Create(c.Request().Context()); err != nil {
80 | return renderFail(c, err)
81 | }
82 | return renderSucc(c, http.StatusOK, discount)
83 | }
84 |
85 | func (DiscountApiController) GetOne(c echo.Context) error {
86 | id, err := strconv.ParseInt(c.Param("id"), 10, 64)
87 | if err != nil {
88 | return renderFail(c, factory.ErrorParameter.New(err))
89 | }
90 | v, err := models.Discount{}.GetById(c.Request().Context(), id)
91 | if err != nil {
92 | return renderFail(c, err)
93 | }
94 | if v == nil {
95 | return renderFail(c, factory.ErrorNotFound.New(nil))
96 | }
97 | return renderSucc(c, http.StatusOK, v)
98 | }
99 |
100 | func (DiscountApiController) Update(c echo.Context) error {
101 | var v DiscountInput
102 | if err := c.Bind(&v); err != nil {
103 | return renderFail(c, factory.ErrorParameter.New(err))
104 | }
105 | if err := c.Validate(&v); err != nil {
106 | return renderFail(c, factory.ErrorParameter.New(err))
107 | }
108 | discount, err := v.ToModel()
109 | if err != nil {
110 | return renderFail(c, factory.ErrorParameter.New(err))
111 | }
112 |
113 | id, err := strconv.ParseInt(c.Param("id"), 10, 64)
114 | if err != nil {
115 | return renderFail(c, factory.ErrorParameter.New(err))
116 | }
117 | discount.Id = id
118 | if err := discount.Update(c.Request().Context()); err != nil {
119 | return renderFail(c, err)
120 | }
121 | return renderSucc(c, http.StatusOK, v)
122 | }
123 |
--------------------------------------------------------------------------------
/controllers/discount.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/labstack/echo"
9 | "github.com/sirupsen/logrus"
10 |
11 | "github.com/pangpanglabs/echosample/factory"
12 | "github.com/pangpanglabs/echosample/models"
13 | )
14 |
15 | type DiscountController struct {
16 | }
17 |
18 | func (c DiscountController) Init(g *echo.Group) {
19 | g.GET("", c.GetAll)
20 | g.GET("/new", c.New)
21 | g.POST("", c.Create)
22 | g.GET("/:id", c.GetOne)
23 | g.GET("/:id/edit", c.Edit)
24 | g.POST("/:id", c.Update)
25 | }
26 |
27 | func (DiscountController) GetAll(c echo.Context) error {
28 | var v SearchInput
29 | if err := c.Bind(&v); err != nil {
30 | setFlashMessage(c, map[string]string{"warning": err.Error()})
31 | }
32 | if v.MaxResultCount == 0 {
33 | v.MaxResultCount = DefaultMaxResultCount
34 | }
35 |
36 | factory.Logger(c.Request().Context()).WithFields(logrus.Fields{
37 | "sortby": v.Sortby,
38 | "order": v.Order,
39 | "maxResultCount": v.MaxResultCount,
40 | "skipCount": v.SkipCount,
41 | }).Info("SearchInput")
42 |
43 | totalCount, items, err := models.Discount{}.GetAll(c.Request().Context(), v.Sortby, v.Order, v.SkipCount, v.MaxResultCount)
44 | if err != nil {
45 | return err
46 | }
47 | return c.Render(http.StatusOK, "discount/index", map[string]interface{}{
48 | "TotalCount": totalCount,
49 | "Discounts": items,
50 | "MaxResultCount": v.MaxResultCount,
51 | })
52 | }
53 | func (DiscountController) New(c echo.Context) error {
54 | return c.Render(http.StatusOK, "discount/new", map[string]interface{}{
55 | FlashName: getFlashMessage(c),
56 | "Form": &models.Discount{},
57 | })
58 | }
59 | func (DiscountController) Create(c echo.Context) error {
60 | var v DiscountInput
61 | if err := c.Bind(&v); err != nil {
62 | setFlashMessage(c, map[string]string{"error": err.Error()})
63 | return c.Redirect(http.StatusFound, "/discount/new")
64 | }
65 | if err := c.Validate(&v); err != nil {
66 | setFlashMessage(c, map[string]string{"error": err.Error()})
67 | return c.Redirect(http.StatusFound, "/discounts/new")
68 | }
69 | discount, err := v.ToModel()
70 | if err != nil {
71 | setFlashMessage(c, map[string]string{"error": err.Error()})
72 | return c.Redirect(http.StatusFound, "/discounts/new")
73 | }
74 | if _, err := discount.Create(c.Request().Context()); err != nil {
75 | return err
76 | }
77 | return c.Redirect(http.StatusFound, fmt.Sprintf("/discounts/%d", discount.Id))
78 | }
79 | func (DiscountController) GetOne(c echo.Context) error {
80 | id, err := strconv.ParseInt(c.Param("id"), 10, 64)
81 | if err != nil {
82 | return c.Render(http.StatusNotFound, "404", nil)
83 | }
84 | v, err := models.Discount{}.GetById(c.Request().Context(), id)
85 | if err != nil {
86 | return err
87 | }
88 | if v == nil {
89 | return c.Render(http.StatusNotFound, "404", nil)
90 | }
91 | return c.Render(http.StatusOK, "discount/show", map[string]interface{}{"Discount": v})
92 | }
93 |
94 | func (DiscountController) Edit(c echo.Context) error {
95 | id, err := strconv.ParseInt(c.Param("id"), 10, 64)
96 | if err != nil {
97 | return c.Render(http.StatusNotFound, "404", nil)
98 | }
99 | v, err := models.Discount{}.GetById(c.Request().Context(), id)
100 | if err != nil {
101 | return err
102 | }
103 | if v == nil {
104 | return c.Render(http.StatusNotFound, "404", nil)
105 | }
106 | return c.Render(http.StatusOK, "discount/edit", map[string]interface{}{
107 | FlashName: getFlashMessage(c),
108 | "Form": v,
109 | })
110 | }
111 | func (DiscountController) Update(c echo.Context) error {
112 | var v DiscountInput
113 | if err := c.Bind(&v); err != nil {
114 | setFlashMessage(c, map[string]string{"error": err.Error()})
115 | return c.Redirect(http.StatusFound, "/discount/new")
116 | }
117 | if err := c.Validate(&v); err != nil {
118 | setFlashMessage(c, map[string]string{"error": err.Error()})
119 | return c.Redirect(http.StatusFound, "/discounts/new")
120 | }
121 | discount, err := v.ToModel()
122 | if err != nil {
123 | setFlashMessage(c, map[string]string{"error": err.Error()})
124 | return c.Redirect(http.StatusFound, "/discounts/new")
125 | }
126 |
127 | id, err := strconv.ParseInt(c.Param("id"), 10, 64)
128 | if err != nil {
129 | return c.Render(http.StatusNotFound, "404", nil)
130 | }
131 | discount.Id = id
132 | if err := discount.Update(c.Request().Context()); err != nil {
133 | return err
134 | }
135 | return c.Redirect(http.StatusFound, fmt.Sprintf("/discounts/%d", discount.Id))
136 | }
137 |
--------------------------------------------------------------------------------
/controllers/discount_api_test.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "net/http/httptest"
8 | "strings"
9 | "testing"
10 | "time"
11 |
12 | "github.com/labstack/echo"
13 |
14 | "github.com/pangpanglabs/echosample/models"
15 | "github.com/pangpanglabs/goutils/test"
16 | )
17 |
18 | func Test_DiscountApiController_Create(t *testing.T) {
19 | req := httptest.NewRequest(echo.POST, "/api/discounts", strings.NewReader(`{"name":"discount name", "desc":"discount desc", "startAt":"2017-01-01","endAt":"2017-12-31","actionType":"Percentage","discountAmount":10,"enable":true}`))
20 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
21 | rec := httptest.NewRecorder()
22 | test.Ok(t, handleWithFilter(DiscountApiController{}.Create, echoApp.NewContext(req, rec)))
23 | test.Equals(t, http.StatusOK, rec.Code)
24 |
25 | var v struct {
26 | Result models.Discount `json:"result"`
27 | Success bool `json:"success"`
28 | }
29 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
30 | test.Equals(t, v.Result.Name, "discount name")
31 | test.Equals(t, v.Result.StartAt.Format("2006-01-02"), "2017-01-01")
32 | }
33 |
34 | func Test_DiscountApiController_Create2(t *testing.T) {
35 | req := httptest.NewRequest(echo.POST, "/api/discounts", strings.NewReader(`{"name":"discount name#2", "desc":"discount desc#2", "startAt":"2017-02-01","endAt":"2017-11-30","actionType":"Percentage","discountAmount":20,"enable":true}`))
36 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
37 | rec := httptest.NewRecorder()
38 | test.Ok(t, handleWithFilter(DiscountApiController{}.Create, echoApp.NewContext(req, rec)))
39 | test.Equals(t, http.StatusOK, rec.Code)
40 |
41 | var v struct {
42 | Result models.Discount `json:"result"`
43 | Success bool `json:"success"`
44 | }
45 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
46 | test.Equals(t, v.Result.Name, "discount name#2")
47 | test.Equals(t, v.Result.StartAt.Format("2006-01-02"), "2017-02-01")
48 | }
49 |
50 | func Test_DiscountApiController_Update(t *testing.T) {
51 | req := httptest.NewRequest(echo.PUT, "/api/discounts/1", strings.NewReader(`{"name":"discount name2", "desc":"discount desc2", "startAt":"2017-01-02","endAt":"2017-12-30","actionType":"Percentage","discountAmount":10,"enable":true}`))
52 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
53 | rec := httptest.NewRecorder()
54 | c := echoApp.NewContext(req, rec)
55 | c.SetPath("/api/discounts/:id")
56 | c.SetParamNames("id")
57 | c.SetParamValues("1")
58 | test.Ok(t, handleWithFilter(DiscountApiController{}.Update, c))
59 | test.Equals(t, http.StatusOK, rec.Code)
60 |
61 | var v struct {
62 | Result models.Discount `json:"result"`
63 | Success bool `json:"success"`
64 | }
65 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
66 | test.Equals(t, v.Result.Name, "discount name2")
67 | test.Equals(t, v.Result.StartAt.Format("2006-01-02"), "2017-01-02")
68 | test.Equals(t, v.Result.UpdatedAt.Format("2006-01-02"), time.Now().Format("2006-01-02"))
69 | }
70 |
71 | func Test_DiscountApiController_GetOne(t *testing.T) {
72 | // given
73 | req := httptest.NewRequest(echo.GET, "/api/discounts/1", nil)
74 | rec := httptest.NewRecorder()
75 | c := echoApp.NewContext(req, rec)
76 | c.SetPath("/api/discounts/:id")
77 | c.SetParamNames("id")
78 | c.SetParamValues("1")
79 |
80 | // when
81 | test.Ok(t, handleWithFilter(DiscountApiController{}.GetOne, c))
82 | test.Equals(t, http.StatusOK, rec.Code)
83 |
84 | // then
85 | var v struct {
86 | Result map[string]interface{} `json:"result"`
87 | Success bool `json:"success"`
88 | }
89 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
90 | test.Equals(t, v.Result["name"], "discount name2")
91 | test.Equals(t, strings.HasPrefix(v.Result["startAt"].(string), "2017-01-02"), true)
92 | test.Equals(t, strings.HasPrefix(v.Result["endAt"].(string), "2017-02-02"), true)
93 | }
94 |
95 | func Test_DiscountApiController_GetAll_SortByAsc(t *testing.T) {
96 | req := httptest.NewRequest(echo.GET, "/api/discounts?sortby=discount_amount&order=asc", nil)
97 | rec := httptest.NewRecorder()
98 | c := echoApp.NewContext(req, rec)
99 | test.Ok(t, handleWithFilter(DiscountApiController{}.GetAll, c))
100 | if rec.Code != http.StatusOK {
101 | fmt.Println(rec.Body.String())
102 | }
103 | test.Equals(t, http.StatusOK, rec.Code)
104 |
105 | var v struct {
106 | Result struct {
107 | TotalCount int
108 | Items []models.Discount
109 | } `json:"result"`
110 | Success bool `json:"success"`
111 | }
112 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
113 | test.Equals(t, v.Result.TotalCount, 2)
114 | test.Equals(t, v.Result.Items[0].DiscountAmount, float64(10))
115 | }
116 |
117 | func Test_DiscountApiController_GetAll_SortByDesc(t *testing.T) {
118 | req := httptest.NewRequest(echo.GET, "/api/discounts?sortby=discount_amount&order=desc", nil)
119 | rec := httptest.NewRecorder()
120 | c := echoApp.NewContext(req, rec)
121 | test.Ok(t, handleWithFilter(DiscountApiController{}.GetAll, c))
122 | if rec.Code != http.StatusOK {
123 | fmt.Println(rec.Body.String())
124 | }
125 | test.Equals(t, http.StatusOK, rec.Code)
126 |
127 | var v struct {
128 | Result struct {
129 | TotalCount int
130 | Items []models.Discount
131 | } `json:"result"`
132 | Success bool `json:"success"`
133 | }
134 | test.Ok(t, json.Unmarshal(rec.Body.Bytes(), &v))
135 | test.Equals(t, v.Result.TotalCount, 2)
136 | test.Equals(t, v.Result.Items[0].DiscountAmount, float64(20))
137 | }
138 |
--------------------------------------------------------------------------------