From 44ef52e0c373e4764fb0617499a6e6ae90243f9b Mon Sep 17 00:00:00 2001 From: Julia Ogris Date: Mon, 24 Jan 2022 15:51:35 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8C=20serve:=20Support=20for=20pluggab?= =?UTF-8?q?le=20evaluator=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for pluggable evaluator, so that we can run jig with other scripting languages such as JS. Introduce Evaluator interface assuming we will add methods for scaffolding with "jig bones". This PR is a continuation of @alecthomas ' RFC: https://github.com/foxygoat/jig/pull/25 Pull-Request: https://github.com/foxygoat/jig/pull/28 --- main.go | 4 ++-- serve/evaluator.go | 30 ++++++++++++++++++++++++ serve/method.go | 40 ++++++++++--------------------- serve/server.go | 56 +++++++++++--------------------------------- serve/server_test.go | 7 +++--- serve/stackedfs.go | 15 ++++++++++++ 6 files changed, 77 insertions(+), 75 deletions(-) create mode 100644 serve/evaluator.go diff --git a/main.go b/main.go index b22de5b..d317634 100644 --- a/main.go +++ b/main.go @@ -40,8 +40,8 @@ func main() { func (cs *cmdServe) Run() error { withLogger := serve.WithLogger(serve.NewLogger(os.Stderr, cs.LogLevel)) - withDirs := serve.WithDirs(cs.Dirs...) - s, err := serve.NewServer(withDirs, withLogger) + dirs := serve.NewFSFromDirs(cs.Dirs...) + s, err := serve.NewServer(serve.JsonnetEvaluator(), dirs, withLogger) if err != nil { return err } diff --git a/serve/evaluator.go b/serve/evaluator.go new file mode 100644 index 0000000..89bf804 --- /dev/null +++ b/serve/evaluator.go @@ -0,0 +1,30 @@ +package serve + +import ( + "io/fs" + + "github.com/google/go-jsonnet" +) + +type Evaluator interface { + Evaluate(method, input string, vfs fs.FS) (output string, err error) +} + +type EvaluatorFunc func(method, input string, vfs fs.FS) (output string, err error) + +func (ef EvaluatorFunc) Evaluate(method, input string, vfs fs.FS) (output string, err error) { + return ef(method, input, vfs) +} + +func JsonnetEvaluator() Evaluator { + return EvaluatorFunc(func(method, input string, vfs fs.FS) (output string, err error) { + vm := jsonnet.MakeVM() + vm.TLACode("input", input) + filename := method + ".jsonnet" + b, err := fs.ReadFile(vfs, filename) + if err != nil { + return "", err + } + return vm.EvaluateAnonymousSnippet(filename, string(b)) + }) +} diff --git a/serve/method.go b/serve/method.go index 068087c..b11a1fa 100644 --- a/serve/method.go +++ b/serve/method.go @@ -7,7 +7,6 @@ import ( "io" "io/fs" - "github.com/google/go-jsonnet" statuspb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -18,23 +17,16 @@ import ( ) type method struct { - desc protoreflect.MethodDescriptor - filename string - fs fs.FS - makeVM MakeVM + desc protoreflect.MethodDescriptor + fs fs.FS + eval Evaluator } -func newMethod(md protoreflect.MethodDescriptor, fs fs.FS, makeVM MakeVM) method { - pkg, svc := md.ParentFile().Package(), md.Parent().Name() - filename := fmt.Sprintf("%s.%s.%s.jsonnet", pkg, svc, md.Name()) - if makeVM == nil { - makeVM = jsonnet.MakeVM - } +func newMethod(md protoreflect.MethodDescriptor, fs fs.FS, eval Evaluator) method { return method{ - desc: md, - filename: filename, - fs: fs, - makeVM: makeVM, + desc: md, + fs: fs, + eval: eval, } } @@ -66,7 +58,7 @@ func (m method) unaryClientCall(ss grpc.ServerStream) error { return err } - return m.evalJsonnet(input, ss) + return m.evaluate(input, ss) } func (m method) streamingClientCall(ss grpc.ServerStream) error { @@ -88,7 +80,7 @@ func (m method) streamingClientCall(ss grpc.ServerStream) error { return err } - return m.evalJsonnet(input, ss) + return m.evaluate(input, ss) } func (m method) streamingBidiCall(ss grpc.ServerStream) error { @@ -102,27 +94,21 @@ func (m method) streamingBidiCall(ss grpc.ServerStream) error { break } - // For bidirectional streaming, we call jsonnet once for each message + // For bidirectional streaming, we call evaluator once for each message // on the input stream and stream out the results. input, err := makeInputJSON(msg, md) if err != nil { return err } - if err := m.evalJsonnet(input, ss); err != nil { + if err := m.evaluate(input, ss); err != nil { return err } } return nil } -func (m method) evalJsonnet(input string, ss grpc.ServerStream) error { - vm := m.makeVM() - vm.TLACode("input", input) - b, err := fs.ReadFile(m.fs, m.filename) - if err != nil { - return err - } - output, err := vm.EvaluateAnonymousSnippet(m.filename, string(b)) +func (m method) evaluate(input string, ss grpc.ServerStream) error { + output, err := m.eval.Evaluate(string(m.desc.FullName()), input, m.fs) if err != nil { return err } diff --git a/serve/server.go b/serve/server.go index 1ea563c..48b9f1e 100644 --- a/serve/server.go +++ b/serve/server.go @@ -1,5 +1,5 @@ -// Package serve implements the "jig serve" command, serving GRPC services -// defined in a protoset file using the jsonnet contained in a method directory. +// Package serve implements the "jig serve" command, serving GRPC +// services via an evaluator. package serve import ( @@ -11,7 +11,6 @@ import ( "strings" "foxygo.at/jig/reflection" - "github.com/google/go-jsonnet" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -25,26 +24,6 @@ import ( // Option is a functional option to configure Server type Option func(s *Server) error -// MakeVM is a constructor for a jsonnet VMs, exposed for custom configuration. -type MakeVM func() *jsonnet.VM - -func WithFS(dirs ...fs.FS) Option { - return func(s *Server) error { - s.fs = append(s.fs, dirs...) - return nil - } -} - -func WithDirs(dirs ...string) Option { - return func(s *Server) error { - for _, dir := range dirs { - vfs := os.DirFS(dir) - s.fs = append(s.fs, vfs) - } - return nil - } -} - func WithProtosets(protosets ...string) Option { return func(s *Server) error { s.protosets = append(s.protosets, protosets...) @@ -59,39 +38,32 @@ func WithLogger(logger Logger) Option { } } -func WithVM(makeVM MakeVM) Option { - return func(s *Server) error { - s.makeVM = makeVM - return nil - } -} - type Server struct { log Logger methods map[string]method gs *grpc.Server files *protoregistry.Files - fs stackedFS + fs fs.FS protosets []string - makeVM MakeVM + eval Evaluator } var errUnknownHandler = errors.New("Unknown handler") -// NewServer creates a new Server. Its API is currently unstable. -func NewServer(options ...Option) (*Server, error) { +// NewServer creates a new Server for given evaluator, e.g. Jsonnet and +// data Directories. +func NewServer(eval Evaluator, vfs fs.FS, options ...Option) (*Server, error) { s := &Server{ files: new(protoregistry.Files), log: NewLogger(os.Stderr, LogLevelError), + eval: eval, + fs: vfs, } for _, opt := range options { if err := opt(s); err != nil { return nil, err } } - if len(s.fs) == 0 { - return nil, fmt.Errorf("missing directory") - } if err := s.loadMethods(); err != nil { return nil, err } @@ -132,7 +104,7 @@ func (s *Server) loadMethods() error { for i := 0; i < sds.Len(); i++ { mds := sds.Get(i).Methods() for j := 0; j < mds.Len(); j++ { - m := newMethod(mds.Get(j), s.fs, s.makeVM) + m := newMethod(mds.Get(j), s.fs, s.eval) s.methods[m.fullMethod()] = m } } @@ -199,7 +171,7 @@ func (s *Server) intercept(srv interface{}, ss grpc.ServerStream, info *grpc.Str s.log.Debugf("%s: new request", info.FullMethod) // If the handler returns anything except errUnknownHandler, then we // have intercepted a real method and we are done now. Otherwise we - // dispatch the method to a jsonnet handler. + // dispatch the method to the evaluator. if err := handler(srv, ss); !errors.Is(err, errUnknownHandler) { if err != nil { s.log.Errorf("%s: %s", info.FullMethod, err) @@ -222,7 +194,7 @@ func (s *Server) intercept(srv interface{}, ss grpc.ServerStream, info *grpc.Str // unknownHandler returns a sentinel error so the interceptor knows when // calling it that is intercepting an unknown method and should dispatch -// it to jsonnet. +// it to the evaluator. func unknownHandler(_ interface{}, stream grpc.ServerStream) error { return errUnknownHandler } @@ -234,8 +206,8 @@ type TestServer struct { // NewTestServer starts and returns a new TestServer. // The caller should call Stop when finished, to shut it down. -func NewTestServer(options ...Option) *TestServer { - s, err := NewServer(options...) +func NewTestServer(eval Evaluator, vfs fs.FS, options ...Option) *TestServer { + s, err := NewServer(eval, vfs, options...) if err != nil { panic(fmt.Sprintf("failed to create TestServer: %v", err)) } diff --git a/serve/server_test.go b/serve/server_test.go index a6ae437..7481adf 100644 --- a/serve/server_test.go +++ b/serve/server_test.go @@ -13,9 +13,8 @@ import ( ) func newTestServer() *TestServer { - lopOpt := WithLogger(NewLogger(io.Discard, LogLevelError)) - fsOpt := WithFS(os.DirFS("testdata/greet")) - return NewTestServer(lopOpt, fsOpt) + withLogger := WithLogger(NewLogger(io.Discard, LogLevelError)) + return NewTestServer(JsonnetEvaluator(), os.DirFS("testdata/greet"), withLogger) } type testCase struct { @@ -127,7 +126,7 @@ var embedFS embed.FS func TestGreeterEmbedFS(t *testing.T) { methodFS, err := fs.Sub(embedFS, "testdata/greet") require.NoError(t, err) - ts := NewTestServer(WithFS(methodFS)) + ts := NewTestServer(JsonnetEvaluator(), methodFS) defer ts.Stop() c, err := client.New(ts.Addr()) diff --git a/serve/stackedfs.go b/serve/stackedfs.go index f11fca9..e2c104d 100644 --- a/serve/stackedfs.go +++ b/serve/stackedfs.go @@ -2,9 +2,24 @@ package serve import ( "io/fs" + "os" "sort" ) +// NewFS combines the top level directories of multiple fs.FS. +func NewFS(vfs ...fs.FS) fs.FS { + return stackedFS(vfs) +} + +// NewFSFromDirs combines the top level directories of multiple directories. +func NewFSFromDirs(dirs ...string) fs.FS { + result := make([]fs.FS, len(dirs)) + for i, dir := range dirs { + result[i] = os.DirFS(dir) + } + return stackedFS(result) +} + type stackedFS []fs.FS // Open opens the the first occurrence of named file.