From 06e048def6aaf93ce1e69cbe81e505e687c51806 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Tue, 22 Mar 2016 11:22:55 -0600 Subject: [PATCH] add schema endpoint This commit adds a new HTTP handler to return a list of all databases and frames in the index: GET /schema This returns JSON in the following format: { "dbs":[ { "name":"d0", "frames":[ {"name":"f0"}, {"name":"f1"} ] } ] } Fixes #61 --- db.go | 21 +++++++++++++++++++++ frame.go | 6 ++++++ handler.go | 40 ++++++++++++++++++++++++++++++++++++++++ handler_test.go | 25 +++++++++++++++++++++++++ index.go | 15 +++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/db.go b/db.go index ca5b2f419..40fcd42e7 100644 --- a/db.go +++ b/db.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "sync" ) @@ -130,6 +131,20 @@ func (db *DB) Frame(name string) *Frame { func (db *DB) frame(name string) *Frame { return db.frames[name] } +// Frames returns a list of all frames in the database. +func (db *DB) Frames() []*Frame { + db.mu.Lock() + defer db.mu.Unlock() + + a := make([]*Frame, 0, len(db.frames)) + for _, f := range db.frames { + a = append(a, f) + } + sort.Sort(frameSlice(a)) + + return a +} + // CreateFrameIfNotExists returns a frame in the database by name. func (db *DB) CreateFrameIfNotExists(name string) (*Frame, error) { db.mu.Lock() @@ -152,3 +167,9 @@ func (db *DB) createFrameIfNotExists(name string) (*Frame, error) { return f, nil } + +type dbSlice []*DB + +func (p dbSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p dbSlice) Len() int { return len(p) } +func (p dbSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } diff --git a/frame.go b/frame.go index 9dd48d615..386c07298 100644 --- a/frame.go +++ b/frame.go @@ -186,3 +186,9 @@ func (f *Frame) createFragmentIfNotExists(slice uint64) (*Fragment, error) { return frag, nil } + +type frameSlice []*Frame + +func (p frameSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p frameSlice) Len() int { return len(p) } +func (p frameSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } diff --git a/handler.go b/handler.go index d1beb5123..d26158ea7 100644 --- a/handler.go +++ b/handler.go @@ -52,6 +52,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := time.Now() switch r.URL.Path { + case "/schema": + switch r.Method { + case "GET": + h.handleGetSchema(w, r) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } case "/query": switch r.Method { case "POST": @@ -108,6 +115,39 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.logger().Printf("%s %s %.03fs", r.Method, r.URL.String(), time.Since(t).Seconds()) } +// handleGetSchema handles GET /schema requests. +func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) { + // Construct schema based on databases and frames. + var resp getSchemaResponse + for _, db := range h.Index.DBs() { + respDB := getSchemaDB{Name: db.Name()} + for _, frame := range db.Frames() { + respDB.Frames = append(respDB.Frames, getSchemaFrame{ + Name: frame.Name(), + }) + } + resp.DBs = append(resp.DBs, respDB) + } + + // Write JSON to response. + if err := json.NewEncoder(w).Encode(resp); err != nil { + h.logger().Printf("write schema response error: %s", err) + } +} + +type getSchemaResponse struct { + DBs []getSchemaDB `json:"dbs"` +} + +type getSchemaDB struct { + Name string `json:"name"` + Frames []getSchemaFrame `json:"frames"` +} + +type getSchemaFrame struct { + Name string `json:"name"` +} + // handlePostQuery handles /query requests. func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) { // Parse incoming request. diff --git a/handler_test.go b/handler_test.go index c56c0a4da..520967097 100644 --- a/handler_test.go +++ b/handler_test.go @@ -26,6 +26,31 @@ func TestHandler_NotFound(t *testing.T) { } } +// Ensure the handler can return the schema. +func TestHandler_Schema(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + if _, err := idx.CreateFrameIfNotExists("d0", "f1"); err != nil { + t.Fatal(err) + } + if _, err := idx.CreateFrameIfNotExists("d1", "f0"); err != nil { + t.Fatal(err) + } + if _, err := idx.CreateFrameIfNotExists("d0", "f0"); err != nil { + t.Fatal(err) + } + + h := NewHandler() + h.Index = idx.Index + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewHTTPRequest("GET", "/schema", nil)) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status code: %d", w.Code) + } else if body := w.Body.String(); body != `{"dbs":[{"name":"d0","frames":[{"name":"f0"},{"name":"f1"}]},{"name":"d1","frames":[{"name":"f0"}]}]}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } +} + // Ensure the handler can accept URL arguments. func TestHandler_Query_Args_URL(t *testing.T) { h := NewHandler() diff --git a/index.go b/index.go index 56a2a5ef9..ffb287270 100644 --- a/index.go +++ b/index.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "sync" ) @@ -95,6 +96,20 @@ func (i *Index) DB(name string) *DB { func (i *Index) db(name string) *DB { return i.dbs[name] } +// DBs returns a list of all databases in the index. +func (i *Index) DBs() []*DB { + i.mu.Lock() + defer i.mu.Unlock() + + a := make([]*DB, 0, len(i.dbs)) + for _, db := range i.dbs { + a = append(a, db) + } + sort.Sort(dbSlice(a)) + + return a +} + // CreateDBIfNotExists returns a database by name. // The database is created if it does not already exist. func (i *Index) CreateDBIfNotExists(name string) (*DB, error) {