initial release
This commit is contained in:
commit
2ee6db04e5
|
@ -0,0 +1 @@
|
|||
dim.toml
|
|
@ -0,0 +1,16 @@
|
|||
# Set the address and port the server should listen for incoming requests.
|
||||
listen = "localhost:8080"
|
||||
|
||||
# This section contains the database configuration.
|
||||
[db]
|
||||
# Type must be set to either 'mysql' or 'postgresql', so that the correct
|
||||
# database driver is used.
|
||||
type = "postgresql"
|
||||
|
||||
# Conn should contain the correct connection string to the database.
|
||||
# For mysql, use the following as a template:
|
||||
# conn = "user:password@tcp(localhost:5555)/dbname"
|
||||
#
|
||||
# For postgresql separate fields can be used, see
|
||||
# https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters
|
||||
# conn = "host=localhost user=dim password=foobar"
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath = flag.String("config", "dim.toml", "path to the config file")
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Listen string `toml:"listen"`
|
||||
DB struct {
|
||||
Type string `toml:"type"`
|
||||
Connection string `toml:"conn"`
|
||||
} `toml:"db"`
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
cfg := Config{}
|
||||
raw, err := toml.LoadFile(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("could not load config file '%s': %s", *configPath, err)
|
||||
return
|
||||
}
|
||||
if err := raw.Unmarshal(&cfg); err != nil {
|
||||
log.Fatalf("could not parse config file '%s': %s", *configPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.DB.Type != "mysql" && cfg.DB.Type != "postgres" {
|
||||
log.Fatalf(
|
||||
"unknown database type '%s' in config '%s'. Allowed is 'mysql' and 'postgresql'",
|
||||
cfg.DB.Type,
|
||||
*configPath,
|
||||
)
|
||||
return
|
||||
}
|
||||
if strings.Trim(cfg.DB.Connection, " \t\n\r") == "" {
|
||||
log.Fatalf("no database connection string in config '%s' set", *configPath)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := sql.Open(cfg.DB.Type, cfg.DB.Connection)
|
||||
if err != nil {
|
||||
log.Fatalf("could not open database connection: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := NewServer(db)
|
||||
if err != nil {
|
||||
log.Fatalf("could not create server instance: %s", err)
|
||||
return
|
||||
}
|
||||
s.Register("foobar", func(c *Context, req Request, res *Response) error {
|
||||
c.Logf(LevelInfo, "I received a request!")
|
||||
res.AddMessage(LevelInfo, "Is this working?")
|
||||
res.Result["foo"] = "seems so"
|
||||
return nil
|
||||
})
|
||||
|
||||
http.HandleFunc("/dim", s.Handle)
|
||||
log.Fatal(http.ListenAndServe(cfg.Listen, nil))
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
LevelError = `ERROR`
|
||||
LevelInfo = `INFO`
|
||||
)
|
||||
|
||||
type (
|
||||
// Server is the central handler of all incoming requests. It generates
|
||||
// the context to call handlers, which then process the requests.
|
||||
Server struct {
|
||||
db *sql.DB
|
||||
routes map[string]Handler
|
||||
}
|
||||
|
||||
// Handler is a function receiving a Context to process a request.
|
||||
// It is expected to investigate the Request and fill the Result.
|
||||
Handler func(c *Context, req Request, resp *Response) error
|
||||
|
||||
// Context is the pre filled global context every handler receives.
|
||||
// It contains a prepared transaction for usage and important details like
|
||||
// the user account.
|
||||
Context struct {
|
||||
id string
|
||||
req *http.Request
|
||||
w http.ResponseWriter
|
||||
|
||||
username string
|
||||
tx *sql.DB
|
||||
}
|
||||
|
||||
// Request contains the method name and parameters requested by the client.
|
||||
Request struct {
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
// Response can have messages and/or a result to return the to client.
|
||||
Response struct {
|
||||
Messages map[string][]string `json:"messages,omitempty"`
|
||||
Result map[string]interface{} `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
ident []byte
|
||||
)
|
||||
|
||||
// NewServer creates a new server handler.
|
||||
func NewServer(db *sql.DB) (*Server, error) {
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("database connection is not set")
|
||||
}
|
||||
return &Server{
|
||||
db: db,
|
||||
routes: map[string]Handler{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register takes a new handler which will be called when the name is called.
|
||||
func (s *Server) Register(name string, handler Handler) {
|
||||
if _, found := s.routes[name]; found {
|
||||
log.Fatalf("route with name %s already exists", name)
|
||||
}
|
||||
s.routes[name] = handler
|
||||
}
|
||||
|
||||
// Handle implements http.HandleFunc to serve content for the standard http
|
||||
// interface.
|
||||
func (s *Server) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write([]byte("only POST requests allowed"))
|
||||
return
|
||||
}
|
||||
id, err := newIdent()
|
||||
if err != nil {
|
||||
log.Printf("could not generate request id: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("sorry, can't currently process your request"))
|
||||
return
|
||||
}
|
||||
c := &Context{
|
||||
id: id,
|
||||
req: r,
|
||||
w: w,
|
||||
}
|
||||
|
||||
req := Request{}
|
||||
res := &Response{
|
||||
Messages: map[string][]string{},
|
||||
Result: map[string]interface{}{},
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
defer r.Body.Close()
|
||||
if err := dec.Decode(&req); err != nil {
|
||||
res.AddMessage(LevelError, fmt.Sprintf("could not parse payload: %s", err))
|
||||
c.w.WriteHeader(http.StatusBadRequest)
|
||||
c.render(res)
|
||||
return
|
||||
}
|
||||
|
||||
handler, found := s.routes[req.Method]
|
||||
if !found {
|
||||
res.AddMessage(LevelError, "method %s does not exist", req.Method)
|
||||
c.w.WriteHeader(http.StatusNotFound)
|
||||
c.render(res)
|
||||
return
|
||||
}
|
||||
c.Logf(LevelInfo, "method '%s' called with '%s'", req.Method, req.Params)
|
||||
if err := handler(c, req, res); err != nil {
|
||||
c.Logf(LevelError, "method '%s' returned an error: %s", req.Method, err)
|
||||
}
|
||||
c.render(res)
|
||||
}
|
||||
|
||||
// Render converts the Result to json and sends it back to the client.
|
||||
func (c *Context) render(res *Response) {
|
||||
enc := json.NewEncoder(c.w)
|
||||
if err := enc.Encode(res); err != nil {
|
||||
c.Logf(LevelError, "%s - could not encode result: %s\n%s", c.id, err, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Logf logs a message to stdout, outfitted with the request ID.
|
||||
func (c *Context) Logf(level, msg string, args ...interface{}) {
|
||||
log.Printf("%s - %s - %s", c.id, level, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
// Generate a useable request ID, so that it can be found in the logs.
|
||||
func newIdent() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
|
||||
}
|
||||
|
||||
// AddMessage adds a new message to the result.
|
||||
// When Render is called, these messages will be sent to the client.
|
||||
func (r *Response) AddMessage(level string, msg string, args ...interface{}) {
|
||||
r.Messages[level] = append(r.Messages[level], fmt.Sprintf(msg, args...))
|
||||
}
|
Loading…
Reference in New Issue