Created
January 3, 2017 11:26
-
-
Save nhooyr/daca5ef167316200bc347a1dacd73132 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"flag" | |
"fmt" | |
"net/http" | |
"runtime" | |
"strconv" | |
"time" | |
"github.com/jackc/pgx" | |
"github.com/nhooyr/lily" | |
"github.com/nhooyr/lily/wrappers" | |
"github.com/nhooyr/log" | |
) | |
type announcement struct { | |
ID int `json:"id,omitempty"` | |
Date time.Time `json:"date"` | |
Title string `json:"title"` | |
Body string `json:"body"` | |
} | |
type database interface { | |
getAllAnnouncements() ([]*announcement, error) | |
getAnnouncements(count int) ([]*announcement, error) | |
getAnnouncementsAfter(id int) ([]*announcement, error) | |
getAnnouncementsBefore(id int, count int) ([]*announcement, error) | |
insertAnnouncement(*announcement) error | |
updateAnnouncement(*announcement) error | |
deleteAnnouncement(id int) error | |
} | |
const maxPageSize = 50 | |
type pgSQL struct { | |
*pgx.ConnPool | |
} | |
func scanRows(rows *pgx.Rows) (anns []*announcement, err error) { | |
defer rows.Close() | |
for rows.Next() { | |
ann := new(announcement) | |
err = rows.Scan(&ann.ID, &ann.Date, &ann.Title, &ann.Body) | |
if err != nil { | |
return nil, err | |
} | |
anns = append(anns, ann) | |
} | |
return anns, err | |
} | |
// TODO retire | |
func (db pgSQL) getAllAnnouncements() ([]*announcement, error) { | |
// language=PostgreSQL | |
rows, err := db.Query("SELECT * FROM announcements ORDER BY id DESC") | |
if err != nil { | |
return nil, err | |
} | |
return scanRows(rows) | |
} | |
func (db pgSQL) getAnnouncements(count int) ([]*announcement, error) { | |
// language=PostgreSQL | |
rows, err := db.Query("SELECT * FROM announcements ORDER BY id DESC LIMIT $1", count) | |
if err != nil { | |
return nil, err | |
} | |
return scanRows(rows) | |
} | |
func (db pgSQL) getAnnouncementsAfter(id int) ([]*announcement, error) { | |
// language=PostgreSQL | |
rows, err := db.Query("SELECT * FROM announcements WHERE id > $1 ORDER BY id DESC LIMIT $2", id, maxPageSize) | |
if err != nil { | |
return nil, err | |
} | |
return scanRows(rows) | |
} | |
func (db pgSQL) getAnnouncementsBefore(id int, count int) ([]*announcement, error) { | |
// language=PostgreSQL | |
rows, err := db.Query("SELECT * FROM announcements WHERE id < $1 ORDER BY id DESC LIMIT $2", id, count) | |
if err != nil { | |
return nil, err | |
} | |
return scanRows(rows) | |
} | |
func (db pgSQL) insertAnnouncement(ann *announcement) error { | |
// language=PostgreSQL | |
_, err := db.Exec("INSERT INTO announcements (date, title, body) VALUES ($1, $2, $3)", ann.Date, ann.Title, ann.Body) | |
return err | |
} | |
func (db pgSQL) updateAnnouncement(ann *announcement) error { | |
// language=PostgreSQL | |
_, err := db.Exec("UPDATE announcements SET date = $1, title = $2, body = $3 WHERE id = $4", ann.Date, ann.Title, ann.Body, ann.ID) | |
return err | |
} | |
func (db pgSQL) deleteAnnouncement(id int) error { | |
// language=PostgreSQL | |
_, err := db.Exec("DELETE FROM announcements WHERE id = $1", id) | |
return err | |
} | |
type app struct { | |
db database | |
} | |
func (a app) get(r *http.Request) ([]*announcement, error) { | |
v := r.URL.Query() | |
qCount := v.Get("count") | |
if qCount == "" { | |
qAfter := v.Get("after") | |
if qAfter == "" { | |
// TODO remove this soon | |
return a.db.getAllAnnouncements() | |
} | |
after, err := strconv.Atoi(qAfter) | |
if err != nil { | |
return nil, lily.WrapError(err, http.StatusBadRequest) | |
} | |
return a.db.getAnnouncementsAfter(after) | |
} | |
count, err := strconv.Atoi(qCount) | |
if err != nil { | |
return nil, lily.WrapError(err, http.StatusBadRequest) | |
} | |
if count > maxPageSize { | |
return nil, lily.NewError("count is larger than maxPageSize", http.StatusBadRequest) | |
} | |
qBefore := v.Get("before") | |
if qBefore == "" { | |
return a.db.getAnnouncements(count) | |
} | |
before, err := strconv.Atoi(qBefore) | |
if err != nil { | |
return nil, lily.WrapError(err, http.StatusBadRequest) | |
} | |
return a.db.getAnnouncementsBefore(before, count) | |
} | |
func (a app) getHandler(w http.ResponseWriter, r *http.Request) (err error) { | |
anns, err := a.get(r) | |
if err != nil { | |
return err | |
} | |
w.Header().Set("Content-Type", "application/json") | |
e := json.NewEncoder(w) | |
e.SetEscapeHTML(false) | |
e.Encode(anns) | |
return nil | |
} | |
func (a app) postHandler(w http.ResponseWriter, r *http.Request) error { | |
var ann *announcement | |
err := json.NewDecoder(r.Body).Decode(&ann) | |
if err != nil { | |
return fmt.Errorf("error decoding json: %v", err) | |
} | |
return a.db.insertAnnouncement(ann) | |
} | |
func (a app) putHandler(w http.ResponseWriter, r *http.Request) error { | |
var ann *announcement | |
err := json.NewDecoder(r.Body).Decode(&ann) | |
if err != nil { | |
return fmt.Errorf("error decoding json: %v", err) | |
} | |
ann.ID, err = strconv.Atoi(lily.ParamFrom(r, "id")) | |
if err != nil { | |
return err | |
} | |
return a.db.updateAnnouncement(ann) | |
} | |
func (a app) deleteHandler(w http.ResponseWriter, r *http.Request) error { | |
id, err := strconv.Atoi(lily.ParamFrom(r, "id")) | |
if err != nil { | |
return err | |
} | |
return a.db.deleteAnnouncement(id) | |
} | |
func (a app) loginHandler(w http.ResponseWriter, r *http.Request) error { | |
return nil | |
} | |
func (a app) authWrapper(next lily.Handler) lily.Handler { | |
return func(w http.ResponseWriter, r *http.Request) error { | |
return next(w, r) | |
} | |
} | |
// TODO CSRF | |
func main() { | |
addr := flag.String("addr", "localhost:8080", "listening address") | |
flag.Parse() | |
p, err := pgx.NewConnPool(pgx.ConnPoolConfig{ | |
ConnConfig: pgx.ConnConfig{ | |
Host: "localhost", | |
User: "radiograydon", | |
Password: "enqvbtenlqba", | |
Database: "radiograydon", | |
}, | |
MaxConnections: runtime.NumCPU(), | |
}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
a := &app{db: pgSQL{p}} | |
m := lily.NewMux(nil) | |
m.Get("/", lily.ServeFile) | |
m.Get("/css/main.css", lily.ServeFile) | |
m.Get("/js/main.js", lily.ServeFile) | |
m.Post("/", a.loginHandler) | |
// TODO retire soon | |
m.With(wrappers.GzipHandler).Get("/announcements", a.getHandler) | |
m.Sub("/api/v1/announcements", func(m *lily.Mux) { | |
m.With(wrappers.GzipHandler).Get("/", a.getHandler) | |
m.Group(func(m *lily.Mux) { | |
m.Use(a.authWrapper) | |
m.Post("/", a.postHandler) | |
m.Sub("/:id", func(m *lily.Mux) { | |
m.Put("/", a.putHandler) | |
m.Delete("/", a.deleteHandler) | |
}) | |
}) | |
}) | |
log.Fatal(lily.ListenAndServe(lily.NewServer(m), *addr)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment