diff --git a/assets/data.json b/assets/data.json new file mode 100644 index 00000000..7878a209 --- /dev/null +++ b/assets/data.json @@ -0,0 +1,5 @@ +{ + "a": 1, + "b": 2, + "c": 3 +} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 63e4dc31..00000000 --- a/cmd/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import "fmt" - -func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") -} diff --git a/cmd/server/main.go b/cmd/server/main.go index 63e4dc31..041bb247 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,8 +1,18 @@ package main -import "fmt" +import ( + "log" + "net/http" + + "github.com/rog-golang-buddies/rapidmidiex/www" +) func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") + if err := run(); err != nil { + log.Fatalln(err) + } +} + +func run() error { + return http.ListenAndServe(":8081", www.NewService()) } diff --git a/go.mod b/go.mod index 99b78ec0..eae7579f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,13 @@ module github.com/rog-golang-buddies/rapidmidiex go 1.18 + +require ( + github.com/go-chi/chi/v5 v5.0.7 + gotest.tools v2.2.0+incompatible +) + +require ( + github.com/google/go-cmp v0.5.8 // indirect + github.com/pkg/errors v0.9.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..2f05744f --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/www/routes.go b/www/routes.go new file mode 100644 index 00000000..c435e741 --- /dev/null +++ b/www/routes.go @@ -0,0 +1,36 @@ +package www + +import "net/http" + +// This can be used as a ping-pong test to check if the server is up and running. +// GET /ping +func (s Service) handlePing() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, "pong", http.StatusOK) + } +} + +func (s Service) routes() { + s.m.Handle("/assets/*", s.fileServer("/assets/", "assets")) + + s.m.Get("/ping", s.handlePing()) + + s.m.Get("/ws/jam/{id}", s.handleTransmitMIDI()) + s.m.Get("/ws/signal/{id}", s.handleP2PSignal()) +} + +// Transmitting MIDI and other Jam Session-specific messages between musicians +// GET /ws/jam/{id} +func (s Service) handleTransmitMIDI() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.notImplemented(w, r) + } +} + +// Initialize WebRTC connections between peers in a specific Jam Session +// GET /ws/signal/{id} +func (s Service) handleP2PSignal() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.notImplemented(w, r) + } +} diff --git a/www/routes_test.go b/www/routes_test.go new file mode 100644 index 00000000..68e68e37 --- /dev/null +++ b/www/routes_test.go @@ -0,0 +1,50 @@ +package www + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gotest.tools/assert" // easy to use testing framework +) + +func TestRoutes(t *testing.T) { + type testcase struct { + Name string + PathName string + Method string + ContentType string + Content string + Status int + } + + tt := []testcase{ + // {Name: "serving assets", PathName: "/assets/data.json", Method: "GET", Status: http.StatusOK}, // httptest can't test this + {Name: "ping pong test", PathName: "/ping", Method: "GET", Status: http.StatusOK}, + {Name: "transmitting midi", PathName: "/ws/jam/0", Method: "GET", Status: http.StatusNotImplemented}, + {Name: "creating p2p connection", PathName: "/ws/signal/0", Method: "GET", Status: http.StatusNotImplemented}, + } + + srv := httptest.NewServer(NewService()) + + for _, tc := range tt { + if tc.ContentType == "" { + tc.ContentType = "application/json" + } + if tc.Status == 0 { + tc.Status = http.StatusOK + } + t.Run(tc.Name, func(t *testing.T) { + req, err := http.NewRequest(tc.Method, srv.URL+tc.PathName, strings.NewReader(tc.Content)) + assert.Equal(t, err, nil) + + req.Header.Add("Content-Type", tc.ContentType) + + res, err := srv.Client().Do(req) + assert.Equal(t, err, nil) + + assert.Equal(t, res.StatusCode, tc.Status) + }) + } +} diff --git a/www/service.go b/www/service.go new file mode 100644 index 00000000..e05f7e79 --- /dev/null +++ b/www/service.go @@ -0,0 +1,57 @@ +package www + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" +) + +type Service struct { + m *chi.Mux +} + +func NewService() *Service { + s := &Service{ + m: chi.NewMux(), + } + s.routes() + return s +} + +func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.m.ServeHTTP(w, r) +} + +// Send data to the client in json format +func (s Service) respond(rw http.ResponseWriter, r *http.Request, data interface{}, status int) { + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + rw.WriteHeader(status) + if data != nil { + err := json.NewEncoder(rw).Encode(data) + if err != nil { + http.Error(rw, "Could not encode in json", status) + } + } +} + +// Decode the request body into the given struct +func (s Service) decode(rw http.ResponseWriter, r *http.Request, data interface{}) (err error) { + return json.NewDecoder(r.Body).Decode(data) +} + +// When the request is successful but no data to send +func (s Service) created(rw http.ResponseWriter, r *http.Request, id string) { + rw.Header().Add("Location", "//"+r.Host+r.URL.Path+"/"+id) + s.respond(rw, r, nil, http.StatusCreated) +} + +// Endpoint is currently in progress +func (s Service) notImplemented(rw http.ResponseWriter, r *http.Request) { + s.respond(rw, r, nil, http.StatusNotImplemented) +} + +// File server helper +func (s Service) fileServer(prefix, dirname string) http.Handler { + return http.StripPrefix(prefix, http.FileServer(http.Dir(dirname))) +}