diff --git a/broadcast_test.go b/broadcast_test.go index ee2d720a9..a2842a919 100644 --- a/broadcast_test.go +++ b/broadcast_test.go @@ -35,3 +35,57 @@ func testMessageMarshal(t *testing.T, m proto.Message) { t.Fatalf("unexpected message marshalling: %s", unmarshalled) } } + +// Ensure that BroadcastReceiver can register a BroadcastHandler. +func TestBroadcast_BroadcastReceiver(t *testing.T) { + + s := pilosa.NewServer() + + sbr := NewSimpleBroadcastReceiver() + sbh := NewSimpleBroadcastHandler() + + s.BroadcastReceiver = sbr + s.BroadcastReceiver.Start(sbh) + + msg := &internal.DeleteDBMessage{ + DB: "d", + } + + s.BroadcastReceiver.(*SimpleBroadcastReceiver).Receive(msg) + + // Make sure the message received is what was sentd + if !reflect.DeepEqual(sbh.receivedMessage, msg) { + t.Fatalf("unexpected message: %s", sbh.receivedMessage) + } +} + +type SimpleBroadcastReceiver struct { + broadcastHandler pilosa.BroadcastHandler +} + +func NewSimpleBroadcastReceiver() *SimpleBroadcastReceiver { + return &SimpleBroadcastReceiver{} +} + +func (r *SimpleBroadcastReceiver) Start(h pilosa.BroadcastHandler) error { + r.broadcastHandler = h + return nil +} + +func (r *SimpleBroadcastReceiver) Receive(pb proto.Message) error { + r.broadcastHandler.ReceiveMessage(pb) + return nil +} + +type SimpleBroadcastHandler struct { + receivedMessage proto.Message +} + +func NewSimpleBroadcastHandler() *SimpleBroadcastHandler { + return &SimpleBroadcastHandler{} +} + +func (h *SimpleBroadcastHandler) ReceiveMessage(pb proto.Message) error { + h.receivedMessage = pb.(proto.Message) + return nil +} diff --git a/handler.go b/handler.go index a8bea8de1..419d3353d 100644 --- a/handler.go +++ b/handler.go @@ -21,6 +21,7 @@ import ( "github.com/gorilla/mux" "github.com/pilosa/pilosa/internal" "github.com/pilosa/pilosa/pql" + "reflect" ) // Handler represents an HTTP handler. @@ -255,55 +256,62 @@ type postDBRequest struct { Options DBOptions `json:"options"` } +//_postDBRequest is necessary to avoid recursion while decoding. +type _postDBRequest postDBRequest + // Custom Unmarshal JSON to validate request body when creating a new database func (p *postDBRequest) UnmarshalJSON(b []byte) error { - var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { + + // m is an overflow map used to capture additional, unexpected keys. + m := make(map[string]interface{}) + if err := json.Unmarshal(b, &m); err != nil { return err } - for key, value := range data { - switch key { - case "options": - value, err := validateOptions(data, "columnLabel") - if err != nil { - return err - } - if value == "" { - p.Options = DBOptions{} - } else { - p.Options = DBOptions{ColumnLabel: value} - } + validDBOptions := getValidOptions(DBOptions{}) + err := validateOptions(m, validDBOptions) + if err != nil { + return err + } + // Unmarshal expected values. + var _p _postDBRequest + if err := json.Unmarshal(b, &_p); err != nil { + return err + } + + p.Options = _p.Options + + return nil +} + +// Raise errors for any unknown key +func validateOptions(data map[string]interface{}, validDBOptions []string) error { + for k, v := range data { + switch k { + case "options": + options, ok := v.(map[string]interface{}) + if !ok { + return errors.New("options is not map[string]interface{}") + } + for kk, vv := range options { + if !foundItem(validDBOptions, kk) { + return fmt.Errorf("Unknown key: %v:%v", kk, vv) + } + } default: - return fmt.Errorf("Unknown key: %v:%v", key, value) + return fmt.Errorf("Unknown key: %v:%v", k, v) } } return nil } -func validateOptions(data map[string]interface{}, field string) (string, error) { - options, ok := data["options"].(map[string]interface{}) - if !ok { - return "", errors.New("options is not map[string]interface{}") - } - var optionValue string - if len(options) == 0 { - optionValue = "" - } else { - for k, v := range options { - switch k { - case field: - val, ok := options[field].(string) - if !ok { - return "", fmt.Errorf("invalid option %v: {%v:%v}", field, k, v) - } - optionValue = val - default: - return "", fmt.Errorf("invalid key for options {%v:%v}", k, v) - } +func foundItem(items []string, item string) bool { + for _, i := range items { + if item == i { + return true } } - return optionValue, nil + return false } type postDBResponse struct{} @@ -526,32 +534,45 @@ func (h *Handler) handlePostFrame(w http.ResponseWriter, r *http.Request) { } } -// Custom Unmarshal JSON to validate request body when creating a new frame +type _postFrameRequest postFrameRequest + +// Custom Unmarshal JSON to validate request body when creating a new frame. If there's new FrameOptions, +// adding it to validFrameOptions to make sure the new option is validated, otherwise the request will be failed func (p *postFrameRequest) UnmarshalJSON(b []byte) error { - var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { + // m is an overflow map used to capture additional, unexpected keys. + m := make(map[string]interface{}) + if err := json.Unmarshal(b, &m); err != nil { return err } - for key, value := range data { - switch key { - case "options": - value, err := validateOptions(data, "rowLabel") - if err != nil { - return err - } - if value == "" { - p.Options = FrameOptions{} - } else { - p.Options = FrameOptions{RowLabel: value} - } - default: - return fmt.Errorf("Unknown key: {%v:%v}", key, value) - } + + validFrameOptions := getValidOptions(FrameOptions{}) + err := validateOptions(m, validFrameOptions) + if err != nil { + return err } + + // Unmarshal expected values. + var _p _postFrameRequest + if err := json.Unmarshal(b, &_p); err != nil { + return err + } + + p.Options = _p.Options return nil } +func getValidOptions(option interface{}) []string { + validOptions := []string{} + val := reflect.ValueOf(option) + for i := 0; i < val.Type().NumField(); i++ { + jsonTag := val.Type().Field(i).Tag.Get("json") + s := strings.Split(jsonTag, ",") + validOptions = append(validOptions, s[0]) + } + return validOptions +} + type postFrameRequest struct { Options FrameOptions `json:"options"` } diff --git a/handler_internal_test.go b/handler_internal_test.go index 8a7bc910f..92221f1d7 100644 --- a/handler_internal_test.go +++ b/handler_internal_test.go @@ -17,7 +17,7 @@ func TestPostDBRequestUnmarshalJSON(t *testing.T) { {json: `{"options": 4}`, err: "options is not map[string]interface{}"}, {json: `{"option": {}}`, err: "Unknown key: option:map[]"}, {json: `{"options": {"columnLabel": "test"}}`, expected: postDBRequest{Options: DBOptions{ColumnLabel: "test"}}}, - {json: `{"options": {"columnLabl": "test"}}`, err: "invalid key for options {columnLabl:test}"}, + {json: `{"options": {"columnLabl": "test"}}`, err: "Unknown key: columnLabl:test"}, } for _, test := range tests { actual := &postDBRequest{} @@ -51,9 +51,12 @@ func TestPostFrameRequestUnmarshalJSON(t *testing.T) { }{ {json: `{"options": {}}`, expected: postFrameRequest{Options: FrameOptions{}}}, {json: `{"options": 4}`, err: "options is not map[string]interface{}"}, - {json: `{"option": {}}`, err: "Unknown key: {option:map[]}"}, + {json: `{"option": {}}`, err: "Unknown key: option:map[]"}, {json: `{"options": {"rowLabel": "test"}}`, expected: postFrameRequest{Options: FrameOptions{RowLabel: "test"}}}, - {json: `{"options": {"rowLabl": "test"}}`, err: "invalid key for options {rowLabl:test}"}, + {json: `{"options": {"rowLabl": "test"}}`, err: "Unknown key: rowLabl:test"}, + {json: `{"options": {"rowLabel": "test", "inverseEnabled": true}}`, expected: postFrameRequest{Options: FrameOptions{RowLabel: "test", InverseEnabled: true}}}, + {json: `{"options": {"rowLabel": "test", "inverseEnabled": true, "cacheType": "type"}}`, expected: postFrameRequest{Options: FrameOptions{RowLabel: "test", InverseEnabled: true, CacheType: "type"}}}, + {json: `{"options": {"rowLabel": "test", "inverse": true, "cacheType": "type"}}`, err: "Unknown key: inverse:true"}, } for _, test := range tests { actual := &postFrameRequest{}