Skip to content

Commit

Permalink
feat(dispatch): Method attributes contain http endpoint and verb
Browse files Browse the repository at this point in the history
  • Loading branch information
dustmop committed Mar 19, 2021
1 parent b1fab21 commit 0036cb7
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 68 deletions.
7 changes: 7 additions & 0 deletions lib/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
22 changes: 10 additions & 12 deletions lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions lib/datasets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
114 changes: 65 additions & 49 deletions lib/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lib
import (
"context"
"fmt"
"net/http"
"reflect"
"strings"
"time"
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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++ {
Expand All @@ -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
Expand All @@ -259,92 +274,118 @@ 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
// should have 3 input parameters: (receiver, context.Context, input struct: same as impl])
// 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,
Func: i.Func,
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)
Expand All @@ -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))
Expand Down
Loading

0 comments on commit 0036cb7

Please sign in to comment.