func/pkg/functions/signatures.go

148 lines
3.5 KiB
Go

package functions
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
)
type Signature int
const (
UnknownSignature Signature = iota
InstancedHTTP
InstancedCloudevent
StaticHTTP
StaticCloudevent
)
func (s Signature) String() string {
return []string{
"unknown",
"instanced-http",
"instanced-cloudevent",
"static-http",
"static-cloudevent",
}[s]
}
var signatureMap = map[bool]map[string]Signature{
true: {
"http": InstancedHTTP,
"cloudevent": InstancedCloudevent},
false: {
"http": StaticHTTP,
"cloudevent": StaticCloudevent},
}
func signature(instanced bool, invoke string) Signature {
if invoke == "" {
invoke = "http"
}
s, ok := signatureMap[instanced][invoke]
if !ok {
return UnknownSignature
}
return s
}
// detectors check for the existence of certain method signatures in the
// source code at the given root.
type detector interface {
Detect(dir string) (static, instanced bool, err error)
}
// functionSignature returns the signature implemented by the given function
func functionSignature(f Function) (s Signature, err error) {
d, err := detectorFor(f.Runtime)
if err != nil {
return UnknownSignature, err
}
static, instanced, err := d.Detect(f.Root)
if err != nil {
return
}
// Function must implement either a static handler or the instanced handler
// but not both.
if static && instanced {
return s, fmt.Errorf("function may not implement both the static and instanced method signatures simultaneously")
} else if !static && !instanced {
return s, fmt.Errorf("function does not appear to implement any known method signatures")
} else if instanced {
return signature(true, f.Invoke), nil
} else {
return signature(false, f.Invoke), nil
}
}
// detectorFor runtime returns a signature detector for a given runtime
func detectorFor(runtime string) (detector, error) {
switch runtime {
case "go":
return &goDetector{}, nil
case "python":
return &pythonDetector{}, nil
case "rust":
return nil, errors.New("the Rust signature detector is not yet available")
case "node":
return nil, errors.New("the Node.js signature detector is not yet available")
case "quarkus":
return nil, errors.New("the TypeScript signature detector is not yet available")
default:
return nil, fmt.Errorf("unable to detect the signature of the unrecognized runtime language %q", runtime)
}
}
// GO
type goDetector struct{}
func (d goDetector) Detect(dir string) (static, instanced bool, err error) {
files, err := os.ReadDir(dir)
if err != nil {
err = fmt.Errorf("signature detector encountered an error when scanning the function's source code %w", err)
return
}
for _, file := range files {
filename := filepath.Join(dir, file.Name())
if file.IsDir() || !strings.HasSuffix(filename, ".go") {
continue
}
if d.hasFunctionDeclaration(filename, "New") {
instanced = true
}
if d.hasFunctionDeclaration(filename, "Handle") {
static = true
}
}
return
}
func (d goDetector) hasFunctionDeclaration(filename, function string) bool {
astFile, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.SkipObjectResolution)
if err != nil {
return false
}
for _, decl := range astFile.Decls {
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
if funcDecl.Name.Name == function {
return true
}
}
}
return false
}
// PYTHON
type pythonDetector struct{}
func (d pythonDetector) Detect(dir string) (bool, bool, error) {
return false, false, errors.New("the Python method signature detector is not yet available.")
}