├── README.md ├── static ├── index.html └── app.js ├── main_test.go └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # echo-example 2 | 3 | Example application of Echo (Go web framework). This use validator and translator. 4 | 5 | ## Usage 6 | 7 | Set $DSN for PostgreSQL connection string. 8 | 9 | ## Installation 10 | 11 | ``` 12 | $ git clone https://github.com/mattn/echo-example 13 | $ cd echo-example 14 | $ go get -d 15 | $ go build 16 | ``` 17 | 18 | ## License 19 | 20 | MIT 21 | 22 | ## Author 23 | 24 | Yasuhiro Matsumoto (a.k.a. mattn) 25 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | コメント一覧 6 | 7 | 8 | 9 | 10 |
11 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | const app = new Vue({ 2 | el: '#app', 3 | data: { 4 | comments: [], 5 | name: '', 6 | text: '', 7 | }, 8 | created() { this.update() }, 9 | methods: { 10 | add: () => { 11 | const payload = {'name': app.name, 'text': app.text} 12 | axios.post('/api/comments', payload) 13 | .then(() => { 14 | app.name = '' 15 | app.text = '' 16 | app.update() 17 | }) 18 | .catch((err) => { 19 | alert(err.response.data.error) 20 | }) 21 | }, 22 | update: () => { 23 | axios.get('/api/comments') 24 | .then((response) => app.comments = response.data || []) 25 | .catch((error) => console.log(error)); 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/labstack/echo" 14 | _ "github.com/mattn/go-sqlite3" 15 | "gopkg.in/gorp.v2" 16 | ) 17 | 18 | func init() { 19 | dbDriver = "sqlite3" 20 | } 21 | 22 | func testSetupDB() (*gorp.DbMap, error) { 23 | os.Remove("test.sqlite") 24 | 25 | old := os.Getenv("DSN") 26 | defer os.Setenv("DSN", old) 27 | os.Setenv("DSN", "test.sqlite") 28 | return setupDB() 29 | } 30 | 31 | func TestInsertCommentWithoutComment(t *testing.T) { 32 | dbmap, err := setupDB() 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | controller := &Controller{dbmap: dbmap} 38 | 39 | req := httptest.NewRequest(http.MethodPost, "/api/comments", strings.NewReader(` 40 | { 41 | "name": "job" 42 | } 43 | `)) 44 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 45 | e := setupEcho() 46 | rec := httptest.NewRecorder() 47 | c := e.NewContext(req, rec) 48 | 49 | err = controller.InsertComment(c) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | var errMsg Error 54 | err = json.NewDecoder(rec.Body).Decode(&errMsg) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | got := errMsg.Error 59 | want := "コメントは必須フィールドです" 60 | if got != want { 61 | log.Fatalf("want %v but got %v", want, got) 62 | } 63 | } 64 | 65 | func TestInsertCommentWithComment(t *testing.T) { 66 | dbmap, err := setupDB() 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | controller := &Controller{dbmap: dbmap} 72 | 73 | req := httptest.NewRequest(http.MethodPost, "/api/comments", strings.NewReader(` 74 | { 75 | "name": "job", 76 | "text": "hello" 77 | } 78 | `)) 79 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 80 | e := setupEcho() 81 | rec := httptest.NewRecorder() 82 | c := e.NewContext(req, rec) 83 | 84 | err = controller.InsertComment(c) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | if rec.Code != 201 { 89 | t.Fatal("should be succeeded") 90 | } 91 | if rec.Body.Len() > 0 { 92 | log.Fatal("response body should be empty") 93 | } 94 | } 95 | 96 | func TestGetComment(t *testing.T) { 97 | dbmap, err := setupDB() 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | controller := &Controller{dbmap: dbmap} 103 | 104 | req := httptest.NewRequest(http.MethodGet, "/api/comments/1", nil) 105 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 106 | e := setupEcho() 107 | rec := httptest.NewRecorder() 108 | c := e.NewContext(req, rec) 109 | c.SetPath("/api/comments/:id") 110 | c.SetParamNames("id") 111 | c.SetParamValues(fmt.Sprint(1)) 112 | 113 | err = controller.GetComment(c) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | if rec.Code != 404 { 118 | t.Fatal("should be 404") 119 | } 120 | 121 | comment := Comment{ 122 | Text: "hello", 123 | } 124 | if dbmap.Insert(&comment); err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | rec = httptest.NewRecorder() 129 | c = e.NewContext(req, rec) 130 | c.SetPath("/api/comments/:id") 131 | c.SetParamNames("id") 132 | c.SetParamValues(fmt.Sprint(1)) 133 | 134 | err = controller.GetComment(c) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | err = json.NewDecoder(rec.Body).Decode(&comment) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | want := "hello" 144 | got := comment.Text 145 | if got != want { 146 | log.Fatalf("want %v but got %v", want, got) 147 | } 148 | } 149 | 150 | func TestListComment(t *testing.T) { 151 | dbmap, err := setupDB() 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | 156 | controller := &Controller{dbmap: dbmap} 157 | 158 | req := httptest.NewRequest(http.MethodGet, "/api/comments", nil) 159 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 160 | e := setupEcho() 161 | rec := httptest.NewRecorder() 162 | c := e.NewContext(req, rec) 163 | c.SetPath("/api/comments/:id") 164 | c.SetParamNames("id") 165 | c.SetParamValues(fmt.Sprint(1)) 166 | 167 | err = controller.ListComments(c) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | if rec.Code != 200 { 172 | t.Fatal("should be 200") 173 | } 174 | var comments []Comment 175 | err = json.NewDecoder(rec.Body).Decode(&comments) 176 | if err != nil { 177 | t.Fatal(err) 178 | } 179 | if len(comments) > 0 { 180 | t.Fatal("should be empty") 181 | } 182 | 183 | comment := Comment{ 184 | Text: "hello", 185 | } 186 | if dbmap.Insert(&comment); err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | rec = httptest.NewRecorder() 191 | c = e.NewContext(req, rec) 192 | 193 | err = controller.ListComments(c) 194 | if err != nil { 195 | t.Fatal(err) 196 | } 197 | 198 | err = json.NewDecoder(rec.Body).Decode(&comments) 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | if len(comments) == 0 { 203 | t.Fatal("should not be empty") 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "os" 9 | "reflect" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-playground/locales/ja_JP" 14 | ut "github.com/go-playground/universal-translator" 15 | "github.com/labstack/echo" 16 | _ "github.com/lib/pq" 17 | "gopkg.in/go-playground/validator.v9" 18 | ja "gopkg.in/go-playground/validator.v9/translations/ja" 19 | "gopkg.in/gorp.v2" 20 | ) 21 | 22 | var dbDriver = "postgres" 23 | 24 | // Validator is implementation of validation of rquest values. 25 | type Validator struct { 26 | trans ut.Translator 27 | validator *validator.Validate 28 | } 29 | 30 | // Validate do validation for request value. 31 | func (v *Validator) Validate(i interface{}) error { 32 | err := v.validator.Struct(i) 33 | if err == nil { 34 | return nil 35 | } 36 | errs := err.(validator.ValidationErrors) 37 | msg := "" 38 | for _, v := range errs.Translate(v.trans) { 39 | if msg != "" { 40 | msg += ", " 41 | } 42 | msg += v 43 | } 44 | return errors.New(msg) 45 | } 46 | 47 | // Error indicate response erorr 48 | type Error struct { 49 | Error string `json:"error"` 50 | } 51 | 52 | // Comment is a struct to hold unit of request and response. 53 | type Comment struct { 54 | Id int64 `json:"id" db:"id,primarykey,autoincrement"` 55 | Name string `json:"name" form:"name" db:"name,notnull,size:200"` 56 | Text string `json:"text" form:"text" validate:"required,max=20" db:"text,notnull,size:399"` 57 | Created time.Time `json:"created" db:"created,notnull"` 58 | Updated time.Time `json:"updated" db:"updated,notnull"` 59 | } 60 | 61 | // PreInsert update fields Created and Updated. 62 | func (c *Comment) PreInsert(s gorp.SqlExecutor) error { 63 | if c.Name == "" { 64 | c.Name = "名無し" 65 | } 66 | c.Created = time.Now() 67 | c.Updated = c.Created 68 | return nil 69 | } 70 | 71 | // PreUpdate update field Updated. 72 | func (c *Comment) PreUpdate(s gorp.SqlExecutor) error { 73 | c.Updated = time.Now() 74 | return nil 75 | } 76 | 77 | func setupDB() (*gorp.DbMap, error) { 78 | db, err := sql.Open(dbDriver, os.Getenv("DSN")) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | var diarect gorp.Dialect = gorp.PostgresDialect{} 84 | 85 | // for testing 86 | if dbDriver == "sqlite3" { 87 | diarect = gorp.SqliteDialect{} 88 | } 89 | dbmap := &gorp.DbMap{Db: db, Dialect: diarect} 90 | dbmap.AddTableWithName(Comment{}, "comments").SetKeys(true, "id") 91 | err = dbmap.CreateTablesIfNotExists() 92 | if err != nil { 93 | return nil, err 94 | } 95 | return dbmap, nil 96 | } 97 | 98 | func setupEcho() *echo.Echo { 99 | e := echo.New() 100 | e.Debug = true 101 | e.Logger.SetOutput(os.Stderr) 102 | 103 | // setup japanese translation 104 | japanese := ja_JP.New() 105 | uni := ut.New(japanese, japanese) 106 | trans, _ := uni.GetTranslator("ja") 107 | validate := validator.New() 108 | err := ja.RegisterDefaultTranslations(validate, trans) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | // register japanese translation for input field 114 | validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 115 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 116 | switch name { 117 | case "name": 118 | return "お名前" 119 | case "text": 120 | return "コメント" 121 | case "-": 122 | return "" 123 | } 124 | return name 125 | }) 126 | 127 | e.Validator = &Validator{validator: validate, trans: trans} 128 | return e 129 | } 130 | 131 | // Controller is a controller for this application. 132 | type Controller struct { 133 | dbmap *gorp.DbMap 134 | } 135 | 136 | // GetComment is GET handler to return record. 137 | func (controller *Controller) GetComment(c echo.Context) error { 138 | var comment Comment 139 | // fetch record specified by parameter id 140 | err := controller.dbmap.SelectOne(&comment, 141 | "SELECT * FROM comments WHERE id = $1", c.Param("id")) 142 | if err != nil { 143 | if err != sql.ErrNoRows { 144 | c.Logger().Error("SelectOne: ", err) 145 | return c.String(http.StatusBadRequest, "SelectOne: "+err.Error()) 146 | } 147 | return c.String(http.StatusNotFound, "Not Found") 148 | } 149 | return c.JSON(http.StatusOK, comment) 150 | } 151 | 152 | // ListComments is GET handler to return records. 153 | func (controller *Controller) ListComments(c echo.Context) error { 154 | var comments []Comment 155 | // fetch last 10 records 156 | _, err := controller.dbmap.Select(&comments, 157 | "SELECT * FROM comments ORDER BY created desc LIMIT 10") 158 | if err != nil { 159 | c.Logger().Error("Select: ", err) 160 | return c.String(http.StatusBadRequest, "Select: "+err.Error()) 161 | } 162 | return c.JSON(http.StatusOK, comments) 163 | } 164 | 165 | // InsertComment is POST handler to insert record. 166 | func (controller *Controller) InsertComment(c echo.Context) error { 167 | var comment Comment 168 | // bind request to comment struct 169 | if err := c.Bind(&comment); err != nil { 170 | c.Logger().Error("Bind: ", err) 171 | return c.String(http.StatusBadRequest, "Bind: "+err.Error()) 172 | } 173 | // validate request 174 | if err := c.Validate(&comment); err != nil { 175 | c.Logger().Error("Validate: ", err) 176 | return c.JSON(http.StatusBadRequest, &Error{Error: err.Error()}) 177 | } 178 | // insert record 179 | if err := controller.dbmap.Insert(&comment); err != nil { 180 | c.Logger().Error("Insert: ", err) 181 | return c.String(http.StatusBadRequest, "Insert: "+err.Error()) 182 | } 183 | c.Logger().Infof("inserted comment: %v", comment.Id) 184 | return c.NoContent(http.StatusCreated) 185 | } 186 | 187 | func main() { 188 | dbmap, err := setupDB() 189 | if err != nil { 190 | log.Fatal(err) 191 | } 192 | controller := &Controller{dbmap: dbmap} 193 | 194 | e := setupEcho() 195 | 196 | e.GET("/api/comments/:id", controller.GetComment) 197 | e.GET("/api/comments", controller.ListComments) 198 | e.POST("/api/comments", controller.InsertComment) 199 | e.Static("/", "static/") 200 | e.Logger.Fatal(e.Start(":8989")) 201 | } 202 | --------------------------------------------------------------------------------