Skip to content

Commit

Permalink
🔌 serve: Support for pluggable evaluator (#28)
Browse files Browse the repository at this point in the history
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: 
#25

Pull-Request: #28
  • Loading branch information
juliaogris authored Jan 24, 2022
1 parent 8ea14f9 commit 44ef52e
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 75 deletions.
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
30 changes: 30 additions & 0 deletions serve/evaluator.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
40 changes: 13 additions & 27 deletions serve/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
56 changes: 14 additions & 42 deletions serve/server.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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"
Expand All @@ -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...)
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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))
}
Expand Down
7 changes: 3 additions & 4 deletions serve/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
15 changes: 15 additions & 0 deletions serve/stackedfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 44ef52e

Please sign in to comment.