From 8bd1ab31f5c6622c5fc2f34a3e5bff240faf1c70 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Fri, 1 Sep 2017 10:46:54 -0600 Subject: [PATCH] Add field HTTP API. --- frame.go | 100 +++++++++++++++++++++++++++ handler.go | 76 ++++++++++++++++++++ handler_test.go | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ pilosa.go | 1 + 4 files changed, 356 insertions(+) diff --git a/frame.go b/frame.go index 73412a1d3..f9580cc6b 100644 --- a/frame.go +++ b/frame.go @@ -419,6 +419,58 @@ func (f *Frame) Field(name string) *Field { return nil } +// CreateField creates a new field on the schema. +func (f *Frame) CreateField(field *Field) error { + f.mu.Lock() + defer f.mu.Unlock() + + // Ensure frame supports fields. + if f.rangeEnabled { + return ErrFrameFieldsNotAllowed + } + + // Copy schema and append field. + schema := f.schema.Clone() + if err := schema.AddField(field); err != nil { + return err + } + f.schema = schema + + return nil +} + +// DeleteField deletes an existing field on the schema. +func (f *Frame) DeleteField(name string) error { + f.mu.Lock() + defer f.mu.Unlock() + + // Ensure frame supports fields. + if f.rangeEnabled { + return ErrFrameFieldsNotAllowed + } + + // Copy schema and remove field. + schema := f.schema.Clone() + if err := schema.DeleteField(name); err != nil { + return err + } + f.schema = schema + + // Remove views. + viewName := ViewFieldPrefix + name + if view := f.views[viewName]; view != nil { + delete(f.views, viewName) + + if err := view.Close(); err != nil { + return err + } else if err := os.RemoveAll(view.Path()); err != nil { + return err + } + } + + return nil +} + // TimeQuantum returns the time quantum for the frame. func (f *Frame) TimeQuantum() TimeQuantum { f.mu.Lock() @@ -849,6 +901,54 @@ type FrameSchema struct { Fields []*Field } +// Clone returns a copy of s. +func (s *FrameSchema) Clone() *FrameSchema { + other := &FrameSchema{Fields: make([]*Field, len(s.Fields))} + copy(other.Fields, s.Fields) + return other +} + +// HasField returns true if a field exists on the schema. +func (s *FrameSchema) HasField(name string) bool { + for _, f := range s.Fields { + if f.Name == name { + return true + } + } + return false +} + +// AddField adds a single field to the schema. +func (s *FrameSchema) AddField(field *Field) error { + if err := ValidateField(field); err != nil { + return err + } else if s.HasField(field.Name) { + return ErrFieldExists + } + + // Add field to list. + s.Fields = append(s.Fields, field) + + // Sort fields by name. + sort.Slice(s.Fields, func(i, j int) bool { + return s.Fields[i].Name < s.Fields[j].Name + }) + + return nil +} + +// DeleteField removes a single field from the schema. +func (s *FrameSchema) DeleteField(name string) error { + for i, field := range s.Fields { + if field.Name == name { + copy(s.Fields[i:], s.Fields[i+1:]) + s.Fields, s.Fields[len(s.Fields)-1] = s.Fields[:len(s.Fields)-1], nil + return nil + } + } + return ErrFieldNotFound +} + func encodeFrameSchema(schema *FrameSchema) *internal.FrameSchema { if schema == nil { return nil diff --git a/handler.go b/handler.go index 0aae927e0..fef22dfff 100644 --- a/handler.go +++ b/handler.go @@ -117,6 +117,8 @@ func NewRouter(handler *Handler) *mux.Router { router.HandleFunc("/index/{index}/frame/{frame}/attr/diff", handler.handlePostFrameAttrDiff).Methods("POST") router.HandleFunc("/index/{index}/frame/{frame}/restore", handler.handlePostFrameRestore).Methods("POST") router.HandleFunc("/index/{index}/frame/{frame}/time-quantum", handler.handlePatchFrameTimeQuantum).Methods("PATCH") + router.HandleFunc("/index/{index}/frame/{frame}/field/{field}", handler.handlePostFrameField).Methods("POST") + router.HandleFunc("/index/{index}/frame/{frame}/field/{field}", handler.handleDeleteFrameField).Methods("DELETE") router.HandleFunc("/index/{index}/frame/{frame}/views", handler.handleGetFrameViews).Methods("GET") router.HandleFunc("/index/{index}/frame/{frame}/view/{view}", handler.handleDeleteView).Methods("DELETE") router.HandleFunc("/index/{index}/input/{input-definition}", handler.handlePostInput).Methods("POST") @@ -769,6 +771,80 @@ type patchFrameTimeQuantumRequest struct { type patchFrameTimeQuantumResponse struct{} +// handlePostFrameField handles POST /frame/field request. +func (h *Handler) handlePostFrameField(w http.ResponseWriter, r *http.Request) { + indexName := mux.Vars(r)["index"] + frameName := mux.Vars(r)["frame"] + fieldName := mux.Vars(r)["field"] + + // Decode request. + var req postFrameFieldRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Retrieve frame by name. + f := h.Holder.Frame(indexName, frameName) + if f == nil { + http.Error(w, ErrFrameNotFound.Error(), http.StatusNotFound) + return + } + + // Create new field. + if err := f.CreateField(&Field{ + Name: fieldName, + Type: req.Type, + Min: req.Min, + Max: req.Max, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode response. + if err := json.NewEncoder(w).Encode(postFrameFieldResponse{}); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type postFrameFieldRequest struct { + Type string `json:"type,omitempty"` + Min int64 `json:"min,omitempty"` + Max int64 `json:"max,omitempty"` +} + +type postFrameFieldResponse struct{} + +// handleDeleteFrameField handles DELETE /frame/field request. +func (h *Handler) handleDeleteFrameField(w http.ResponseWriter, r *http.Request) { + indexName := mux.Vars(r)["index"] + frameName := mux.Vars(r)["frame"] + fieldName := mux.Vars(r)["field"] + + // Retrieve frame by name. + f := h.Holder.Frame(indexName, frameName) + if f == nil { + http.Error(w, ErrFrameNotFound.Error(), http.StatusNotFound) + return + } + + // Delete field. + if err := f.DeleteField(fieldName); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode response. + if err := json.NewEncoder(w).Encode(deleteFrameFieldResponse{}); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type deleteFrameFieldRequest struct{} + +type deleteFrameFieldResponse struct{} + // handleGetFrameViews handles GET /frame/views request. func (h *Handler) handleGetFrameViews(w http.ResponseWriter, r *http.Request) { indexName := mux.Vars(r)["index"] diff --git a/handler_test.go b/handler_test.go index dacb04b38..8159cf586 100644 --- a/handler_test.go +++ b/handler_test.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "errors" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -859,6 +860,176 @@ func TestHandler_Frame_AttrStore_Diff(t *testing.T) { } } +// Ensure the handler can create a new field on an existing frame. +func TestHandler_Frame_AddField(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + + s := test.NewServer() + s.Handler.Holder = hldr.Holder + defer s.Close() + + t.Run("OK", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}) + if err != nil { + t.Fatal(err) + } + + resp, err := http.Post( + s.URL+"/index/i/frame/f/field/x", + "application/json", + strings.NewReader(`{"type":"int","min":100,"max":200}`), + ) + if err != nil { + t.Fatal(err) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + + if field := f.Field("x"); !reflect.DeepEqual(field, &pilosa.Field{Name: "x", Type: "int", Min: 100, Max: 200}) { + t.Fatalf("unexpected field: %#v", field) + } + }) + + t.Run("ErrInvalidFieldType", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}); err != nil { + t.Fatal(err) + } + + resp, err := http.Post( + s.URL+"/index/i/frame/f/field/x", + "application/json", + strings.NewReader(`{"type":"bad_type","min":100,"max":200}`), + ) + if err != nil { + t.Fatal(err) + } else if body := MustReadAll(resp.Body); string(body) != `invalid field type`+"\n" { + t.Fatalf("unexpected body: %q", body) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + }) + + t.Run("ErrInvalidFieldRange", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}); err != nil { + t.Fatal(err) + } + + resp, err := http.Post( + s.URL+"/index/i/frame/f/field/x", + "application/json", + strings.NewReader(`{"type":"int","min":200,"max":100}`), + ) + if err != nil { + t.Fatal(err) + } else if body := MustReadAll(resp.Body); string(body) != `invalid field range`+"\n" { + t.Fatalf("unexpected body: %q", body) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + }) + + t.Run("ErrFieldAlreadyExists", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{ + RangeEnabled: true, + Fields: []*pilosa.Field{{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}}, + }); err != nil { + t.Fatal(err) + } + + resp, err := http.Post( + s.URL+"/index/i/frame/f/field/x", + "application/json", + strings.NewReader(`{"type":"int","min":0,"max":100}`), + ) + if err != nil { + t.Fatal(err) + } else if body := MustReadAll(resp.Body); string(body) != `field already exists`+"\n" { + t.Fatalf("unexpected body: %q", body) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + }) +} + +// Ensure the handler can delete existing fields. +func TestHandler_Frame_DeleteField(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + + s := test.NewServer() + s.Handler.Holder = hldr.Holder + defer s.Close() + + t.Run("OK", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}) + if err != nil { + t.Fatal(err) + } else if err := f.CreateField(&pilosa.Field{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}); err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("DELETE", s.URL+"/index/i/frame/f/field/x", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + + if field := f.Field("x"); field != nil { + t.Fatalf("expected nil field, got: %#v", field) + } + }) + + t.Run("ErrFieldNotFound", func(t *testing.T) { + idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}) + if err != nil { + t.Fatal(err) + } else if err := f.CreateField(&pilosa.Field{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}); err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("DELETE", s.URL+"/index/i/frame/f/field/y", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } else if body, err := ioutil.ReadAll(resp.Body); err != nil { + t.Fatal(err) + } else if strings.TrimSpace(string(body)) != `field not found` { + t.Fatalf("unexpected body: %q", body) + } else if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } else if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + }) +} + // Ensure the handler can backup a fragment and then restore it. func TestHandler_Fragment_BackupRestore(t *testing.T) { hldr := test.MustOpenHolder() @@ -1533,3 +1704,11 @@ func TestHandler_DeleteView(t *testing.T) { t.Fatal("expected nil view") } } + +func MustReadAll(r io.Reader) []byte { + buf, err := ioutil.ReadAll(r) + if err != nil { + panic(err) + } + return buf +} diff --git a/pilosa.go b/pilosa.go index 8a7810b6c..49bbd6c11 100644 --- a/pilosa.go +++ b/pilosa.go @@ -49,6 +49,7 @@ var ( ErrInputDefinitionNotFound = errors.New("input-definition not found") ErrFieldNotFound = errors.New("field not found") + ErrFieldExists = errors.New("field already exists") ErrFieldNameRequired = errors.New("field name required") ErrInvalidFieldType = errors.New("invalid field type") ErrInvalidFieldRange = errors.New("invalid field range")