tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/duplex/fn/handler.go (about)

     1  package fn
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"runtime"
     7  	"strings"
     8  
     9  	"tractor.dev/toolkit-go/duplex/mux"
    10  	"tractor.dev/toolkit-go/duplex/rpc"
    11  )
    12  
    13  // HandlerFrom uses reflection to return a handler from either a function or
    14  // methods from a struct. When a struct is used, HandlerFrom creates a RespondMux
    15  // registering each method as a handler using its method name. From there, methods
    16  // are treated just like functions.
    17  //
    18  // The registered methods can be limited by providing an interface type parameter:
    19  //
    20  //	h := HandlerFrom[interface{
    21  //		OnlyTheseMethods()
    22  //		WillBeRegistered()
    23  //	}](myHandlerImplementation)
    24  //
    25  // If a struct method matches the HandlerFunc signature, the method will be called
    26  // directly with the handler arguments. Otherwise it will be wrapped as described below.
    27  //
    28  // Function handlers expect an array to use as arguments. If the incoming argument
    29  // array is too large or too small, the handler returns an error. Functions can opt-in
    30  // to take a final Call pointer argument, allowing the handler to give it the Call value
    31  // being processed. Functions can return nothing which the handler returns as nil, or
    32  // a single value which can be an error, or two values where one value is an error.
    33  // In the latter case, the value is returned if the error is nil, otherwise just the
    34  // error is returned. Handlers based on functions that return more than two values will
    35  // simply ignore the remaining values.
    36  //
    37  // Structs that implement the Handler interface will be added as a catch-all handler
    38  // along with their individual methods. This lets you implement dynamic methods.
    39  func HandlerFrom[T any](v T) rpc.Handler {
    40  	rv := reflect.Indirect(reflect.ValueOf(v))
    41  	switch rv.Type().Kind() {
    42  	case reflect.Func:
    43  		return fromFunc(reflect.ValueOf(v))
    44  	case reflect.Struct:
    45  		// assume T is an interface
    46  		t := reflect.TypeOf((*T)(nil)).Elem()
    47  		if t.NumMethod() == 0 {
    48  			// could be inferred empty interface / any
    49  			// so then just use TypeOf v
    50  			t = reflect.TypeOf(v)
    51  		}
    52  		return fromMethods(v, t)
    53  	default:
    54  		panic("must be func or struct")
    55  	}
    56  }
    57  
    58  // Args is the expected argument value for calls made to HandlerFrom handlers.
    59  // Since it is just a slice of empty interface values, you can alternatively use
    60  // more specific slice types ([]int{}, etc) if all arguments are of the same type.
    61  type Args []any
    62  
    63  var handlerFuncType = reflect.TypeOf((*rpc.HandlerFunc)(nil)).Elem()
    64  
    65  func fromMethods(rcvr interface{}, t reflect.Type) rpc.Handler {
    66  	// If `t` is an interface, `Convert()` wraps the value with that interface
    67  	// type. This makes sure that the Method(i) indexes match for getting both the
    68  	// name and implementation.
    69  	rcvrval := reflect.ValueOf(rcvr).Convert(t)
    70  	mux := rpc.NewRespondMux()
    71  	for i := 0; i < t.NumMethod(); i++ {
    72  		m := rcvrval.Method(i)
    73  		var h rpc.Handler
    74  		if m.CanConvert(handlerFuncType) {
    75  			h = m.Convert(handlerFuncType).Interface().(rpc.HandlerFunc)
    76  		} else {
    77  			h = fromFunc(m)
    78  		}
    79  		mux.Handle(t.Method(i).Name, h)
    80  	}
    81  	h, ok := rcvr.(rpc.Handler)
    82  	if ok {
    83  		mux.Handle("/", h)
    84  	}
    85  	return mux
    86  }
    87  
    88  var callRef = reflect.TypeOf((*rpc.Call)(nil))
    89  
    90  func fromFunc(fn reflect.Value) rpc.Handler {
    91  	fntyp := fn.Type()
    92  	// if the last argument in fn is an rpc.Call, add our call to fnParams
    93  	expectsCallParam := fntyp.NumIn() > 0 && fntyp.In(fntyp.NumIn()-1) == callRef
    94  
    95  	// if the last arg or first return in fn is a channel, we'll make a channel to stream back
    96  	expectsChanParam := fntyp.NumIn() > 0 && fntyp.In(fntyp.NumIn()-1).Kind() == reflect.Chan
    97  	var chanType reflect.Type
    98  	var chanStream bool
    99  	if expectsChanParam {
   100  		chanType = fntyp.In(fntyp.NumIn() - 1)
   101  		chanStream = true
   102  	}
   103  	if fntyp.NumOut() > 0 && fntyp.Out(0).Kind() == reflect.Chan {
   104  		chanType = fntyp.Out(0)
   105  		chanStream = true
   106  	}
   107  
   108  	return rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) {
   109  		var params []any
   110  
   111  		defer func() {
   112  			if p := recover(); p != nil {
   113  				r.Return(fmt.Errorf("panic: %s [%s] %s(%s)", p, identifyPanic(), c.Selector(), params))
   114  			}
   115  		}()
   116  
   117  		var ch any
   118  		if err := c.Receive(&params); err != nil {
   119  			r.Return(fmt.Errorf("fn: args: %s", err.Error()))
   120  			return
   121  		}
   122  		if expectsCallParam {
   123  			params = append(params, c)
   124  		} else if expectsChanParam {
   125  			// TODO: somehow pass buffer via CallHeader?
   126  			_ = chanType
   127  			//ch = reflect.MakeChan(chanType, 512).Interface() // not supported by tinygo 0.28.1
   128  			//params = append(params, ch)
   129  		}
   130  		ret, err := Call(fn.Interface(), params)
   131  		if err != nil {
   132  			r.Return(err)
   133  			return
   134  		}
   135  		if chanStream && fntyp.NumOut() > 0 && fntyp.Out(0).Kind() == reflect.Chan {
   136  			ch = ret[0]
   137  			ret = ret[1:]
   138  		}
   139  		if chanStream {
   140  			c, _ := r.Continue(ret...)
   141  			go streamValues(r, c, reflect.ValueOf(ch))
   142  			return
   143  		}
   144  		r.Return(ret...)
   145  	})
   146  }
   147  
   148  // streamValues will receive on the reflected Go channel and send over the
   149  // duplex channel until an error is returned or either are closed.
   150  func streamValues(r rpc.Responder, ch mux.Channel, valueCh reflect.Value) {
   151  	defer ch.Close()
   152  	for {
   153  		v, ok := valueCh.Recv()
   154  		if !ok {
   155  			return
   156  		}
   157  		if err := r.Send(v.Interface()); err != nil {
   158  			return
   159  		}
   160  	}
   161  }
   162  
   163  func identifyPanic() string {
   164  	var name, file string
   165  	var line int
   166  	var pc [16]uintptr
   167  
   168  	n := runtime.Callers(3, pc[:])
   169  	for _, pc := range pc[:n] {
   170  		fn := runtime.FuncForPC(pc)
   171  		if fn == nil {
   172  			continue
   173  		}
   174  		file, line = fn.FileLine(pc)
   175  		name = fn.Name()
   176  		if !strings.HasPrefix(name, "runtime.") && !strings.HasPrefix(name, "reflect.") {
   177  			break
   178  		}
   179  	}
   180  
   181  	switch {
   182  	case name != "":
   183  		return fmt.Sprintf("%v:%v", name, line)
   184  	case file != "":
   185  		return fmt.Sprintf("%v:%v", file, line)
   186  	}
   187  
   188  	return fmt.Sprintf("pc:%x", pc)
   189  }