From 0036cb7512783f20ac2946ae4d019ef512857578 Mon Sep 17 00:00:00 2001 From: Dustin Long Date: Mon, 15 Mar 2021 21:04:48 -0400 Subject: [PATCH] feat(dispatch): Method attributes contain http endpoint and verb --- lib/access.go | 7 +++ lib/config.go | 22 ++++----- lib/datasets.go | 22 +++++++++ lib/dispatch.go | 114 ++++++++++++++++++++++++------------------- lib/dispatch_test.go | 54 ++++++++++++++++++-- lib/fsi.go | 16 ++++++ lib/http.go | 2 +- lib/transform.go | 9 +++- 8 files changed, 178 insertions(+), 68 deletions(-) diff --git a/lib/access.go b/lib/access.go index 16ab38f81..3a6ada699 100644 --- a/lib/access.go +++ b/lib/access.go @@ -19,6 +19,13 @@ func (m AccessMethods) Name() string { return "access" } +// Attributes defines attributes for each method +func (m AccessMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "createauthtoken": {APIEndpoint("/auth/createauthtoken"), "GET"}, + } +} + // Access returns the authentication that Instance has registered func (inst *Instance) Access() AccessMethods { return AccessMethods{d: inst} diff --git a/lib/config.go b/lib/config.go index e0020f3c0..37f4986f2 100644 --- a/lib/config.go +++ b/lib/config.go @@ -21,6 +21,16 @@ func (m *ConfigMethods) Name() string { return "config" } +// Attributes defines attributes for each method +func (m *ConfigMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + // config methods are not allowed over HTTP nor RPC + "getconfig": {"", ""}, + "getconfigkeys": {"", ""}, + "setconfig": {"", ""}, + } +} + // Config returns the `Config` that the instance has registered func (inst *Instance) Config() *ConfigMethods { return &ConfigMethods{inst: inst} @@ -38,10 +48,6 @@ type GetConfigParams struct { // GetConfig returns the Config, or one of the specified fields of the Config, // as a slice of bytes the bytes can be formatted as json, concise json, or yaml func (m *ConfigMethods) GetConfig(ctx context.Context, p *GetConfigParams) ([]byte, error) { - if m.inst.http != nil { - return nil, ErrUnsupportedRPC - } - got, _, err := m.inst.Dispatch(ctx, dispatchMethodName(m, "getconfig"), p) if res, ok := got.([]byte); ok { return res, err @@ -52,9 +58,6 @@ func (m *ConfigMethods) GetConfig(ctx context.Context, p *GetConfigParams) ([]by // GetConfigKeys returns the Config key fields, or sub keys of the specified // fields of the Config, as a slice of bytes to be used for auto completion func (m *ConfigMethods) GetConfigKeys(ctx context.Context, p *GetConfigParams) ([]byte, error) { - if m.inst.http != nil { - return nil, ErrUnsupportedRPC - } got, _, err := m.inst.Dispatch(ctx, dispatchMethodName(m, "getconfigkeys"), p) if res, ok := got.([]byte); ok { return res, err @@ -64,11 +67,6 @@ func (m *ConfigMethods) GetConfigKeys(ctx context.Context, p *GetConfigParams) ( // SetConfig validates, updates and saves the config func (m *ConfigMethods) SetConfig(ctx context.Context, update *config.Config) (*bool, error) { - if m.inst.http != nil { - res := false - return &res, ErrUnsupportedRPC - } - got, _, err := m.inst.Dispatch(ctx, dispatchMethodName(m, "setconfig"), update) if res, ok := got.(*bool); ok { return res, err diff --git a/lib/datasets.go b/lib/datasets.go index d81487558..7bda3f545 100644 --- a/lib/datasets.go +++ b/lib/datasets.go @@ -50,6 +50,28 @@ func (m *DatasetMethods) Name() string { return "dataset" } +// Attributes defines attributes for each method +func (m *DatasetMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "changereport": {"/changes", "POST"}, + "daginfo": {"/dag/info", "GET"}, + "diff": {"/diff", "GET"}, + "get": {"/get", "GET"}, + "list": {"/list", "GET"}, + // TODO(dustmop): Needs its own endpoint + "listrawrefs": {"/list", "GET"}, + "manifest": {"/manifest", "GET"}, + "manifestmissing": {"/manifest/missing", "GET"}, + "pull": {"/pull", "POST"}, + "remove": {"/remove", "POST"}, + "rename": {"/rename", "POST"}, + "save": {"/save", "POST"}, + // TODO(dustmop): Needs its own endpoint + "stats": {"/get", "GET"}, + "validate": {"/validate", "GET"}, + } +} + // Dataset returns the DatasetMethods the instance has registered func (inst *Instance) Dataset() *DatasetMethods { return &DatasetMethods{inst: inst} diff --git a/lib/dispatch.go b/lib/dispatch.go index b352379a3..5fef0c187 100644 --- a/lib/dispatch.go +++ b/lib/dispatch.go @@ -3,6 +3,7 @@ package lib import ( "context" "fmt" + "net/http" "reflect" "strings" "time" @@ -29,6 +30,14 @@ type Cursor interface{} // with the context.Context replaced by a scope. type MethodSet interface { Name() string + Attributes() map[string]AttributeSet +} + +// AttributeSet is extra information about each method, such as: http endpoint, +// http verb, (TODO) permissions, and (TODO) other metadata +type AttributeSet struct { + endpoint APIEndpoint + verb string } // Dispatch is a system for handling calls to lib. Should only be called by top-level lib methods. @@ -80,14 +89,14 @@ func (inst *Instance) Dispatch(ctx context.Context, method string, param interfa } if c, ok := inst.regMethods.lookup(method); ok { - // TODO(dustmop): This is always using the "POST" verb currently. We need some - // mechanism of tagging methods as being read-only and "GET"-able. Once that - // exists, use it here to lookup the verb that should be used to invoke the rpc. + if c.Endpoint == "" { + return nil, nil, ErrUnsupportedRPC + } if c.OutType != nil { out := reflect.New(c.OutType) res = out.Interface() } - err = inst.http.Call(ctx, methodEndpoint(method), param, res) + err = inst.http.CallMethod(ctx, c.Endpoint, c.Verb, param, res) if err != nil { return nil, nil, err } @@ -193,6 +202,8 @@ type callable struct { InType reflect.Type OutType reflect.Type RetCursor bool + Endpoint APIEndpoint + Verb string } // RegisterMethods iterates the methods provided by the lib API, and makes them visible to dispatch @@ -210,6 +221,10 @@ func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interf implType := reflect.TypeOf(impl) msetType := reflect.TypeOf(methods) methodMap := inst.buildMethodMap(methods) + // Validate that the methodSet has the correct name + if methods.Name() != ourName { + regFail("registration wrong name, expect: %q, got: %q", ourName, methods.Name()) + } // Iterate methods on the implementation, register those that have the right signature num := implType.NumMethod() for k := 0; k < num; k++ { @@ -222,31 +237,31 @@ func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interf // should have 1-3 output parametres: ([output value]?, [cursor]?, error) f := i.Type if f.NumIn() != 3 { - panic(fmt.Sprintf("%s: bad number of inputs: %d", funcName, f.NumIn())) + regFail("%s: bad number of inputs: %d", funcName, f.NumIn()) } // First input must be the receiver inType := f.In(0) if inType != implType { - panic(fmt.Sprintf("%s: first input param should be impl, got %v", funcName, inType)) + regFail("%s: first input param should be impl, got %v", funcName, inType) } // Second input must be a scope inType = f.In(1) if inType.Name() != "scope" { - panic(fmt.Sprintf("%s: second input param should be scope, got %v", funcName, inType)) + regFail("%s: second input param should be scope, got %v", funcName, inType) } // Third input is a pointer to the input struct inType = f.In(2) if inType.Kind() != reflect.Ptr { - panic(fmt.Sprintf("%s: third input param must be a struct pointer, got %v", funcName, inType)) + regFail("%s: third input param must be a struct pointer, got %v", funcName, inType) } inType = inType.Elem() if inType.Kind() != reflect.Struct { - panic(fmt.Sprintf("%s: third input param must be a struct pointer, got %v", funcName, inType)) + regFail("%s: third input param must be a struct pointer, got %v", funcName, inType) } // Validate the output values of the implementation numOuts := f.NumOut() if numOuts < 1 || numOuts > 3 { - panic(fmt.Sprintf("%s: bad number of outputs: %d", funcName, numOuts)) + regFail("%s: bad number of outputs: %d", funcName, numOuts) } // Validate output values var outType reflect.Type @@ -259,14 +274,14 @@ func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interf // Second output must be a cursor outCursorType := f.Out(1) if outCursorType.Name() != "Cursor" { - panic(fmt.Sprintf("%s: second output val must be a cursor, got %v", funcName, outCursorType)) + regFail("%s: second output val must be a cursor, got %v", funcName, outCursorType) } returnsCursor = true } // Last output must be an error outErrType := f.Out(numOuts - 1) if outErrType.Name() != "error" { - panic(fmt.Sprintf("%s: last output val should be error, got %v", funcName, outErrType)) + regFail("%s: last output val should be error, got %v", funcName, outErrType) } // Validate the parameters to the method that matches the implementation @@ -274,59 +289,79 @@ func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interf // should have 1-3 output parametres: ([output value: same as impl], [cursor], error) m, ok := methodMap[i.Name] if !ok { - panic(fmt.Sprintf("method %s not found on MethodSet", i.Name)) + regFail("method %s not found on MethodSet", i.Name) } f = m.Type if f.NumIn() != 3 { - panic(fmt.Sprintf("%s: bad number of inputs: %d", funcName, f.NumIn())) + regFail("%s: bad number of inputs: %d", funcName, f.NumIn()) } // First input must be the receiver mType := f.In(0) if mType.Name() != msetType.Name() { - panic(fmt.Sprintf("%s: first input param should be impl, got %v", funcName, mType)) + regFail("%s: first input param should be impl, got %v", funcName, mType) } // Second input must be a context mType = f.In(1) if mType.Name() != "Context" { - panic(fmt.Sprintf("%s: second input param should be context.Context, got %v", funcName, mType)) + regFail("%s: second input param should be context.Context, got %v", funcName, mType) } // Third input is a pointer to the input struct mType = f.In(2) if mType.Kind() != reflect.Ptr { - panic(fmt.Sprintf("%s: third input param must be a pointer, got %v", funcName, mType)) + regFail("%s: third input param must be a pointer, got %v", funcName, mType) } mType = mType.Elem() if mType != inType { - panic(fmt.Sprintf("%s: third input param must match impl, expect %v, got %v", funcName, inType, mType)) + regFail("%s: third input param must match impl, expect %v, got %v", funcName, inType, mType) } // Validate the output values of the implementation msetNumOuts := f.NumOut() if msetNumOuts < 1 || msetNumOuts > 3 { - panic(fmt.Sprintf("%s: bad number of outputs: %d", funcName, f.NumOut())) + regFail("%s: bad number of outputs: %d", funcName, f.NumOut()) } // First output, if there's more than 1, matches the impl output if msetNumOuts == 2 || msetNumOuts == 3 { mType = f.Out(0) if mType != outType { - panic(fmt.Sprintf("%s: first output val must match impl, expect %v, got %v", funcName, outType, mType)) + regFail("%s: first output val must match impl, expect %v, got %v", funcName, outType, mType) } } // Second output, if there are three, must be a cursor if msetNumOuts == 3 { mType = f.Out(1) if mType.Name() != "Cursor" { - panic(fmt.Sprintf("%s: second output val must match a cursor, got %v", funcName, mType)) + regFail("%s: second output val must match a cursor, got %v", funcName, mType) } } // Last output must be an error mType = f.Out(msetNumOuts - 1) if mType.Name() != "error" { - panic(fmt.Sprintf("%s: last output val should be error, got %v", funcName, mType)) + regFail("%s: last output val should be error, got %v", funcName, mType) } // Remove this method from the methodSetMap now that it has been processed delete(methodMap, i.Name) + var endpoint APIEndpoint + var httpVerb string + // Additional attributes for the method are found in the Attributes + amap := methods.Attributes() + methodAttrs, ok := amap[lowerName] + if !ok { + regFail("not in Attributes: %s.%s", ourName, lowerName) + } + endpoint = methodAttrs.endpoint + httpVerb = methodAttrs.verb + // If both these are empty string, RPC is not allowed for this method + if endpoint != "" || httpVerb != "" { + if !strings.HasPrefix(string(endpoint), "/") { + regFail("%s: endpoint URL must start with /, got %q", lowerName, endpoint) + } + if httpVerb != http.MethodGet && httpVerb != http.MethodPost && httpVerb != http.MethodPut { + regFail("%s: unknown http verb, got %q", lowerName, httpVerb) + } + } + // Save the method to the registration table reg[funcName] = callable{ Impl: impl, @@ -334,17 +369,23 @@ func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interf InType: inType, OutType: outType, RetCursor: returnsCursor, + Endpoint: endpoint, + Verb: httpVerb, } log.Debugf("%d: registered %s(*%s) %v", k, funcName, inType, outType) } for k := range methodMap { - if k != "Name" { - panic(fmt.Sprintf("%s: did not find implementation for method %s", msetType, k)) + if k != "Name" && k != "Attributes" { + regFail("%s: did not find implementation for method %s", msetType, k) } } } +func regFail(fstr string, vals ...interface{}) { + panic(fmt.Sprintf(fstr, vals...)) +} + func (inst *Instance) buildMethodMap(impl interface{}) map[string]reflect.Method { result := make(map[string]reflect.Method) implType := reflect.TypeOf(impl) @@ -361,31 +402,6 @@ func dispatchMethodName(m MethodSet, funcName string) string { return fmt.Sprintf("%s.%s", m.Name(), lowerName) } -// methodEndpoint returns a method name and returns the API endpoint for it -func methodEndpoint(method string) APIEndpoint { - // TODO(dustmop): This is here temporarily. /fsi/write/ works differently than - // other methods; their http API endpoints are only their method name, for - // exmaple /status/. This should be replaced with an explicit mapping from - // method names to endpoints. - if method == "fsi.write" { - return "/fsi/write/" - } - if method == "fsi.createlink" { - return "/fsi/createlink/" - } - if method == "fsi.unlink" { - return "/fsi/unlink/" - } - if method == "dataset.list" { - return "/list" - } - pos := strings.Index(method, ".") - prefix := method[:pos] - _ = prefix - res := "/" + method[pos+1:] + "/" - return APIEndpoint(res) -} - func dispatchReturnError(got interface{}, err error) error { if got != nil { log.Errorf("type mismatch: %v of type %s", got, reflect.TypeOf(got)) diff --git a/lib/dispatch_test.go b/lib/dispatch_test.go index e7247bee7..95fdaa25c 100644 --- a/lib/dispatch_test.go +++ b/lib/dispatch_test.go @@ -134,7 +134,7 @@ func TestVariadicReturnsWorkOverHTTP(t *testing.T) { t.Errorf("%s", err) } - // Call the last method + // Call a method successfully val, _, err := clientFruit.Date(ctx, &fruitParams{}) if err != nil { t.Errorf("%s", err) @@ -142,6 +142,16 @@ func TestVariadicReturnsWorkOverHTTP(t *testing.T) { if val != "January 1st" { t.Errorf("value mismatch, expect: January 1st, got: %s", val) } + + // Call a method not supported over RPC + val, _, err = clientFruit.Entawak(ctx, &fruitParams{}) + if err == nil { + t.Fatal("expected to get error but did not get one") + } + expectErr = "method is not suported over RPC" + if err.Error() != expectErr { + t.Errorf("error mismatch, expect: %s, got: %s", expectErr, err) + } } func serverConnectAndListen(t *testing.T, servInst *Instance, port int) (*HTTPClient, func()) { @@ -153,14 +163,18 @@ func serverConnectAndListen(t *testing.T, servInst *Instance, port int) (*HTTPCl handler := func(w http.ResponseWriter, r *http.Request) { method := "" - if r.URL.Path == "/apple/" { + if r.URL.Path == "/apple" { method = "fruit.apple" - } else if r.URL.Path == "/banana/" { + } else if r.URL.Path == "/banana" { method = "fruit.banana" - } else if r.URL.Path == "/cherry/" { + } else if r.URL.Path == "/cherry" { method = "fruit.cherry" - } else if r.URL.Path == "/date/" { + } else if r.URL.Path == "/date" { method = "fruit.date" + } else if r.URL.Path == "/entawak" { + method = "fruit.entawak" + } else { + t.Fatalf("404: Not Found %q", r.URL.Path) } p := servInst.NewInputParam(method) res, _, err := servInst.Dispatch(r.Context(), method, p) @@ -227,6 +241,13 @@ func (m *animalMethods) Name() string { return "animal" } +func (m *animalMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "cat": {"", ""}, + "dog": {"", ""}, + } +} + type animalParams struct { Name string } @@ -318,6 +339,17 @@ func (m *fruitMethods) Name() string { return "fruit" } +func (m *fruitMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "apple": {"/apple", "GET"}, + "banana": {"/banana", "GET"}, + "cherry": {"/cherry", "GET"}, + "date": {"/date", "GET"}, + // entawak cannot be called over RPC + "entawak": {"", ""}, + } +} + type fruitParams struct { Name string } @@ -348,6 +380,14 @@ func (m *fruitMethods) Date(ctx context.Context, p *fruitParams) (string, Cursor return "", nil, dispatchReturnError(got, err) } +func (m *fruitMethods) Entawak(ctx context.Context, p *fruitParams) (string, Cursor, error) { + got, cur, err := m.d.Dispatch(ctx, dispatchMethodName(m, "entawak"), p) + if res, ok := got.(string); ok { + return res, cur, err + } + return "", nil, dispatchReturnError(got, err) +} + // Implementation for fruit type fruitImpl struct{} @@ -368,3 +408,7 @@ func (fruitImpl) Date(scp scope, p *fruitParams) (string, Cursor, error) { var cur Cursor return "January 1st", cur, nil } + +func (fruitImpl) Entawak(scp scope, p *fruitParams) (string, Cursor, error) { + return "mentawa", nil, nil +} diff --git a/lib/fsi.go b/lib/fsi.go index d65dd1a50..48bac7b3c 100644 --- a/lib/fsi.go +++ b/lib/fsi.go @@ -28,6 +28,22 @@ func (m *FSIMethods) Name() string { return "fsi" } +// Attributes defines attributes for each method +func (m *FSIMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "createlink": {"/fsi/createlink", "POST"}, + "unlink": {"/fsi/unlink", "POST"}, + "status": {"/status", "GET"}, + "whatchanged": {"/whatchanged", "GET"}, + "checkout": {"/checkout", "POST"}, + "write": {"/fsi/write", "POST"}, + "restore": {"/restore", "POST"}, + "init": {"/init", "POST"}, + "caninitdatasetworkdir": {"/caninitdatasetworkdir", "GET"}, + "ensureref": {"/ensureref", "POST"}, + } +} + // Filesys returns the FSIMethods that Instance has registered func (inst *Instance) Filesys() *FSIMethods { return &FSIMethods{d: inst} diff --git a/lib/http.go b/lib/http.go index c936b7184..29e119fb6 100644 --- a/lib/http.go +++ b/lib/http.go @@ -19,7 +19,7 @@ import ( ) // ErrUnsupportedRPC is an error for when running a method that is not supported via HTTP RPC -var ErrUnsupportedRPC = errors.New("Warning: method is not suported over RPC") +var ErrUnsupportedRPC = errors.New("method is not suported over RPC") const jsonMimeType = "application/json" diff --git a/lib/transform.go b/lib/transform.go index 18ad24dde..d0476fbeb 100644 --- a/lib/transform.go +++ b/lib/transform.go @@ -18,11 +18,18 @@ type TransformMethods struct { d dispatcher } -// Name returns the name of this method gropu +// Name returns the name of this method group func (m *TransformMethods) Name() string { return "transform" } +// Attributes defines attributes for each method +func (m *TransformMethods) Attributes() map[string]AttributeSet { + return map[string]AttributeSet{ + "apply": {"/apply", "POST"}, + } +} + // Transform returns the TransformMethods that Instance has registered func (inst *Instance) Transform() *TransformMethods { return &TransformMethods{d: inst}