Skip to content

Instantly share code, notes, and snippets.

@nhooyr
Created January 3, 2017 11:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhooyr/daca5ef167316200bc347a1dacd73132 to your computer and use it in GitHub Desktop.
Save nhooyr/daca5ef167316200bc347a1dacd73132 to your computer and use it in GitHub Desktop.
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