├── 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 |
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 |
--------------------------------------------------------------------------------